1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

auto enable/disable extensions -- part 1

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman 2020-11-03 08:08:52 +02:00
parent cefa8d8880
commit addc55e577
5 changed files with 86 additions and 33 deletions

View File

@ -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 { LensMainExtension } from "./lens-main-extension"
import type { LensRendererExtension } from "./lens-renderer-extension" import type { LensRendererExtension } from "./lens-renderer-extension"
import path from "path" import path from "path"
import { broadcastIpc } from "../common/ipc" 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 logger from "../main/logger"
import { app, ipcRenderer, remote } from "electron" import { app, ipcRenderer, remote } from "electron"
import { BaseStore } from "../common/base-store";
import * as registries from "./registries"; import * as registries from "./registries";
type ExtensionManifestPath = string; // path to package.json export interface ExtensionLoaderStoreModel {
extensions: LensExtensionStoreModel[]
}
export interface InstalledExtension { export interface InstalledExtension {
manifestPath: string;
manifest: LensExtensionManifest; manifest: LensExtensionManifest;
manifestPath: string;
isBundled?: boolean; // defined in package.json isBundled?: boolean; // defined in package.json
} }
@ -21,19 +24,23 @@ export function extensionPackagesRoot() {
return path.join((app || remote.app).getPath("userData")) return path.join((app || remote.app).getPath("userData"))
} }
export class ExtensionLoader { export class ExtensionLoader extends BaseStore<ExtensionLoaderStoreModel> {
@observable extensions = observable.map<ExtensionManifestPath, InstalledExtension>([], { deep: false }); @observable extensions = observable.map<LensExtensionId, InstalledExtension>([], { deep: false });
@observable instances = observable.map<ExtensionManifestPath, LensExtension>([], { deep: false }) @observable instances = observable.map<LensExtensionId, LensExtension>([], { deep: false })
@observable state = observable.map<LensExtensionId, LensExtensionStoreModel>();
constructor() { constructor() {
super({
configName: "lens-extensions",
});
if (ipcRenderer) { if (ipcRenderer) {
ipcRenderer.on("extensions:loaded", (event, extensions: InstalledExtension[]) => { ipcRenderer.on("extensions:loaded", (event, extensions: InstalledExtension[]) => {
extensions.forEach((ext) => { extensions.forEach((ext) => {
if (!this.getByManifest(ext.manifestPath)) { if (!this.extensions.has(ext.manifestPath)) {
this.extensions.set(ext.manifestPath, ext) this.extensions.set(ext.manifestPath, ext)
} }
}) })
}) });
} }
} }
@ -43,14 +50,16 @@ export class ExtensionLoader {
loadOnMain() { loadOnMain() {
logger.info('[EXTENSIONS-LOADER]: load on main') logger.info('[EXTENSIONS-LOADER]: load on main')
this.autoloadExtensions((extension: LensMainExtension) => { this.autoInitExtensions();
this.autoEnableExtensions((extension: LensMainExtension) => {
extension.registerTo(registries.menuRegistry, extension.appMenus) extension.registerTo(registries.menuRegistry, extension.appMenus)
}) })
} }
loadOnClusterManagerRenderer() { loadOnClusterManagerRenderer() {
logger.info('[EXTENSIONS-LOADER]: load on main renderer (cluster manager)') 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.globalPageRegistry, extension.globalPages)
extension.registerTo(registries.appPreferenceRegistry, extension.appPreferences) extension.registerTo(registries.appPreferenceRegistry, extension.appPreferences)
extension.registerTo(registries.clusterFeatureRegistry, extension.clusterFeatures) extension.registerTo(registries.clusterFeatureRegistry, extension.clusterFeatures)
@ -60,14 +69,32 @@ export class ExtensionLoader {
loadOnClusterRenderer() { loadOnClusterRenderer() {
logger.info('[EXTENSIONS-LOADER]: load on cluster renderer (dashboard)') 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.clusterPageRegistry, extension.clusterPages)
extension.registerTo(registries.kubeObjectMenuRegistry, extension.kubeObjectMenuItems) extension.registerTo(registries.kubeObjectMenuRegistry, extension.kubeObjectMenuItems)
extension.registerTo(registries.kubeObjectDetailRegistry, extension.kubeObjectDetailItems) 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) => { return reaction(() => this.extensions.toJS(), (installedExtensions) => {
for (const [id, ext] of installedExtensions) { for (const [id, ext] of installedExtensions) {
let instance = this.instances.get(ext.manifestPath) let instance = this.instances.get(ext.manifestPath)
@ -79,17 +106,14 @@ export class ExtensionLoader {
try { try {
const LensExtensionClass: LensExtensionConstructor = extensionModule.default; const LensExtensionClass: LensExtensionConstructor = extensionModule.default;
instance = new LensExtensionClass(ext); instance = new LensExtensionClass(ext);
instance.enable() this.instances.set(ext.manifestPath, instance);
callback(instance)
this.instances.set(ext.manifestPath, instance)
} catch (err) { } catch (err) {
logger.error(`[EXTENSIONS-LOADER]: activating extension error`, { ext, err }) logger.error(`[EXTENSIONS-LOADER]: init extension instance error`, { ext, err })
} }
} }
} }
}, { }, {
fireImmediately: true, fireImmediately: true,
delay: 0,
}) })
} }
@ -110,26 +134,31 @@ export class ExtensionLoader {
} }
} }
getByManifest(manifestPath: ExtensionManifestPath): InstalledExtension {
return this.extensions.get(manifestPath);
}
broadcastExtensions(frameId?: number) { broadcastExtensions(frameId?: number) {
broadcastIpc({ broadcastIpc({
channel: "extensions:loaded", channel: "extensions:loaded",
frameId: frameId, frameId: frameId,
frameOnly: !!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({ return toJS({
extensions: Array.from(this.extensions).map(([id, instance]) => instance), extensions: this.userExtensions.map(ext => ext.toJSON())
}, { }, {
recurseEverything: true, recurseEverything: true,
}) })
} }
} }
export const extensionLoader = new ExtensionLoader() export const extensionLoader: ExtensionLoader = ExtensionLoader.getInstance();

View File

@ -1,10 +1,17 @@
import { observable, toJS } from "mobx"; import { action, observable, toJS } from "mobx";
import logger from "../main/logger"; import logger from "../main/logger";
import { BaseRegistry } from "./registries/base-registry"; import { BaseRegistry } from "./registries/base-registry";
import type { InstalledExtension } from "./extension-loader"; import type { InstalledExtension } from "./extension-loader";
export type LensExtensionId = string; // path to manifest (package.json)
export type LensExtensionConstructor = new (init: InstalledExtension) => LensExtension; export type LensExtensionConstructor = new (init: InstalledExtension) => LensExtension;
export interface LensExtensionStoreModel {
id: LensExtensionId;
name: string;
isEnabled?: boolean;
}
export interface LensExtensionManifest { export interface LensExtensionManifest {
name: string; name: string;
version: string; version: string;
@ -27,6 +34,10 @@ export class LensExtension {
this.isBundled = !!isBundled this.isBundled = !!isBundled
} }
get id(): LensExtensionId {
return this.manifestPath;
}
get name() { get name() {
return this.manifest.name return this.manifest.name
} }
@ -39,13 +50,17 @@ export class LensExtension {
return this.manifest.description return this.manifest.description
} }
@action
async enable() { async enable() {
if (this.isEnabled) return;
this.isEnabled = true; this.isEnabled = true;
logger.info(`[EXTENSION]: enabled ${this.name}@${this.version}`); logger.info(`[EXTENSION]: enabled ${this.name}@${this.version}`);
this.onActivate(); this.onActivate();
} }
@action
async disable() { async disable() {
if (!this.isEnabled) return;
this.onDeactivate(); this.onDeactivate();
this.isEnabled = false; this.isEnabled = false;
this.disposers.forEach(cleanUp => cleanUp()); this.disposers.forEach(cleanUp => cleanUp());
@ -53,6 +68,14 @@ export class LensExtension {
logger.info(`[EXTENSION]: disabled ${this.name}@${this.version}`); 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() { protected onActivate() {
// mock // mock
} }
@ -69,12 +92,10 @@ export class LensExtension {
}; };
} }
toJSON() { toJSON(): LensExtensionStoreModel {
return toJS({ return toJS({
id: this.id,
name: this.name, name: this.name,
version: this.version,
description: this.description,
manifestPath: this.manifestPath,
isEnabled: this.isEnabled, isEnabled: this.isEnabled,
}, { }, {
recurseEverything: true, recurseEverything: true,

View File

@ -53,6 +53,7 @@ app.on("ready", async () => {
userStore.load(), userStore.load(),
clusterStore.load(), clusterStore.load(),
workspaceStore.load(), workspaceStore.load(),
extensionLoader.load(),
]); ]);
// find free port // find free port

View File

@ -13,6 +13,7 @@ import { i18nStore } from "./i18n";
import { themeStore } from "./theme.store"; import { themeStore } from "./theme.store";
import { App } from "./components/app"; import { App } from "./components/app";
import { LensApp } from "./lens-app"; import { LensApp } from "./lens-app";
import { extensionLoader } from "../extensions/extension-loader";
type AppComponent = React.ComponentType & { type AppComponent = React.ComponentType & {
init?(): Promise<void>; init?(): Promise<void>;
@ -34,6 +35,7 @@ export async function bootstrap(App: AppComponent) {
userStore.load(), userStore.load(),
workspaceStore.load(), workspaceStore.load(),
clusterStore.load(), clusterStore.load(),
extensionLoader.load(),
i18nStore.init(), i18nStore.init(),
themeStore.init(), themeStore.init(),
]); ]);

View File

@ -67,9 +67,9 @@ export class Extensions extends React.Component {
) )
} }
return extensions.map(ext => { return extensions.map(ext => {
const { manifestPath, name, description, isEnabled } = ext; const { id, name, description, isEnabled } = ext;
return ( return (
<div key={manifestPath} className="extension flex gaps align-center"> <div key={id} className="extension flex gaps align-center">
<div className="box grow flex column gaps"> <div className="box grow flex column gaps">
<div className="package"> <div className="package">
Name: <code className="name">{name}</code> Name: <code className="name">{name}</code>