UI for enabling/disabling extensions (#1208)

* Extensions page and menu item

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Basic extension list view

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Adding get userExtensions filter

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Using WizardLayout at extension page

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Adding search to extension page

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Few style fixes

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* clean up

Signed-off-by: Roman <ixrock@gmail.com>

* added folder-icon to open extensions in finder, refactoring

Signed-off-by: Roman <ixrock@gmail.com>

* remove export warnings in dev:main, tooltip.getPosition() fix

Signed-off-by: Roman <ixrock@gmail.com>

* refactoring base lens-extension.ts, added `isBundled` flag

Signed-off-by: Roman <ixrock@gmail.com>

* added enabled/disable buttons

Signed-off-by: Roman <ixrock@gmail.com>

* auto enable/disable extensions -- part 1

Signed-off-by: Roman <ixrock@gmail.com>

* auto enable/disable extensions -- part 2

Signed-off-by: Roman <ixrock@gmail.com>

* auto enable/disable extensions -- part 3

Signed-off-by: Roman <ixrock@gmail.com>

* auto enable/disable extensions -- part 4

Signed-off-by: Roman <ixrock@gmail.com>

* refactoring & fixes

Signed-off-by: Roman <ixrock@gmail.com>

* fix: use page-layout with fullsize viewport

Signed-off-by: Roman <ixrock@gmail.com>

Co-authored-by: Alex Andreev <alex.andreev.email@gmail.com>
This commit is contained in:
RomanandAlex Andreev authored and GitHub committed 2020-11-04 19:25:36 +02:00
1 parent d5214e47c1
commit f9578ba407
17 files changed
+376 -191

No files matched your search

+2 -1
View File
@@ -1 +1,2 @@
export { ClusterFeature as Feature, ClusterFeatureStatus as FeatureStatus } from "../cluster-feature"
export { ClusterFeature as Feature } from "../cluster-feature"
export type { ClusterFeatureStatus as FeatureStatus } from "../cluster-feature"
+5 -3
View File
@@ -1,4 +1,6 @@
export { ExtensionStore } from "../extension-store"
export { clusterStore, ClusterModel } from "../../common/cluster-store"
export { Cluster } from "../../main/cluster"
export { workspaceStore, Workspace, WorkspaceModel } from "../../common/workspace-store"
export { clusterStore } from "../../common/cluster-store"
export type { ClusterModel } from "../../common/cluster-store"
export { Cluster } from "../../main/cluster"
export { workspaceStore, Workspace } from "../../common/workspace-store"
export type { WorkspaceModel } from "../../common/workspace-store"
+51 -65
View File
@@ -1,20 +1,13 @@
import type { ExtensionId, ExtensionManifest, ExtensionModel, LensExtension } from "./lens-extension"
import type { LensExtension, LensExtensionConstructor, LensExtensionId } from "./lens-extension"
import type { LensMainExtension } from "./lens-main-extension"
import type { LensRendererExtension } from "./lens-renderer-extension"
import type { InstalledExtension } from "./extension-manager";
import path from "path"
import { broadcastIpc } from "../common/ipc"
import { observable, reaction, toJS, } from "mobx"
import { computed, observable, reaction, when } from "mobx"
import logger from "../main/logger"
import { app, ipcRenderer, remote } from "electron"
import {
appPreferenceRegistry, clusterFeatureRegistry, clusterPageRegistry, globalPageRegistry,
kubeObjectDetailRegistry, kubeObjectMenuRegistry, menuRegistry, statusBarRegistry
} from "./registries";
export interface InstalledExtension extends ExtensionModel {
manifestPath: string;
manifest: ExtensionManifest;
}
import * as registries from "./registries";
// lazy load so that we get correct userData
export function extensionPackagesRoot() {
@@ -22,69 +15,82 @@ export function extensionPackagesRoot() {
}
export class ExtensionLoader {
@observable extensions = observable.map<ExtensionId, InstalledExtension>([], { deep: false });
@observable instances = observable.map<ExtensionId, LensExtension>([], { deep: false })
@observable isLoaded = false;
protected extensions = observable.map<LensExtensionId, InstalledExtension>([], { deep: false });
protected instances = observable.map<LensExtensionId, LensExtension>([], { deep: false })
constructor() {
if (ipcRenderer) {
ipcRenderer.on("extensions:loaded", (event, extensions: InstalledExtension[]) => {
this.isLoaded = true;
extensions.forEach((ext) => {
if (!this.getById(ext.manifestPath)) {
if (!this.extensions.has(ext.manifestPath)) {
this.extensions.set(ext.manifestPath, ext)
}
})
})
});
}
}
@computed get userExtensions(): LensExtension[] {
return [...this.instances.values()].filter(ext => !ext.isBundled)
}
async init() {
const { extensionManager } = await import("./extension-manager");
const installedExtensions = await extensionManager.load();
this.extensions.replace(installedExtensions);
this.isLoaded = true;
this.loadOnMain();
}
loadOnMain() {
logger.info('[EXTENSIONS-LOADER]: load on main')
this.autoloadExtensions((extension: LensMainExtension) => {
extension.registerTo(menuRegistry, extension.appMenus)
})
this.autoInitExtensions((extension: LensMainExtension) => [
registries.menuRegistry.add(...extension.appMenus)
]);
}
loadOnClusterManagerRenderer() {
logger.info('[EXTENSIONS-LOADER]: load on main renderer (cluster manager)')
this.autoloadExtensions((extension: LensRendererExtension) => {
extension.registerTo(globalPageRegistry, extension.globalPages)
extension.registerTo(appPreferenceRegistry, extension.appPreferences)
extension.registerTo(clusterFeatureRegistry, extension.clusterFeatures)
extension.registerTo(statusBarRegistry, extension.statusBarItems)
})
this.autoInitExtensions((extension: LensRendererExtension) => [
registries.globalPageRegistry.add(...extension.globalPages),
registries.appPreferenceRegistry.add(...extension.appPreferences),
registries.clusterFeatureRegistry.add(...extension.clusterFeatures),
registries.statusBarRegistry.add(...extension.statusBarItems),
]);
}
loadOnClusterRenderer() {
logger.info('[EXTENSIONS-LOADER]: load on cluster renderer (dashboard)')
this.autoloadExtensions((extension: LensRendererExtension) => {
extension.registerTo(clusterPageRegistry, extension.clusterPages)
extension.registerTo(kubeObjectMenuRegistry, extension.kubeObjectMenuItems)
extension.registerTo(kubeObjectDetailRegistry, extension.kubeObjectDetailItems)
})
this.autoInitExtensions((extension: LensRendererExtension) => [
registries.clusterPageRegistry.add(...extension.clusterPages),
registries.kubeObjectMenuRegistry.add(...extension.kubeObjectMenuItems),
registries.kubeObjectDetailRegistry.add(...extension.kubeObjectDetailItems),
]);
}
protected autoloadExtensions(callback: (instance: LensExtension) => void) {
protected autoInitExtensions(register: (ext: LensExtension) => Function[]) {
return reaction(() => this.extensions.toJS(), (installedExtensions) => {
for(const [id, ext] of installedExtensions) {
let instance = this.instances.get(ext.id)
for (const [id, ext] of installedExtensions) {
let instance = this.instances.get(ext.manifestPath)
if (!instance) {
const extensionModule = this.requireExtension(ext)
if (!extensionModule) {
continue
}
const LensExtensionClass = extensionModule.default;
instance = new LensExtensionClass({ ...ext.manifest, manifestPath: ext.manifestPath, id: ext.manifestPath }, ext.manifest);
try {
instance.enable()
callback(instance)
} finally {
this.instances.set(ext.id, instance)
const LensExtensionClass: LensExtensionConstructor = extensionModule.default;
instance = new LensExtensionClass(ext);
instance.whenEnabled(() => register(instance));
this.instances.set(ext.manifestPath, instance);
} catch (err) {
logger.error(`[EXTENSIONS-LOADER]: init extension instance error`, { ext, err })
}
}
}
}, {
fireImmediately: true,
delay: 0,
})
}
@@ -105,37 +111,17 @@ export class ExtensionLoader {
}
}
getById(id: ExtensionId): InstalledExtension {
return this.extensions.get(id);
}
async removeById(id: ExtensionId) {
const extension = this.getById(id);
if (extension) {
const instance = this.instances.get(extension.id)
if (instance) {
await instance.disable()
}
this.extensions.delete(id);
}
}
broadcastExtensions(frameId?: number) {
async broadcastExtensions(frameId?: number) {
await when(() => this.isLoaded);
broadcastIpc({
channel: "extensions:loaded",
frameId: frameId,
frameOnly: !!frameId,
args: [this.toJSON().extensions],
})
}
toJSON() {
return toJS({
extensions: Array.from(this.extensions).map(([id, instance]) => instance),
}, {
recurseEverything: true,
args: [
Array.from(this.extensions.toJS().values())
],
})
}
}
export const extensionLoader = new ExtensionLoader()
export const extensionLoader = new ExtensionLoader();
+18 -14
View File
@@ -1,12 +1,18 @@
import type { ExtensionManifest } from "./lens-extension"
import type { LensExtensionId, LensExtensionManifest } from "./lens-extension"
import path from "path"
import os from "os"
import fs from "fs-extra"
import child_process from "child_process";
import logger from "../main/logger"
import { extensionPackagesRoot, InstalledExtension } from "./extension-loader"
import * as child_process from 'child_process';
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
}
type Dependencies = {
[name: string]: string;
}
@@ -51,7 +57,7 @@ export class ExtensionManager {
return path.join(this.extensionPackagesRoot, "package.json")
}
async load() {
async load(): Promise<Map<LensExtensionId, InstalledExtension>> {
logger.info("[EXTENSION-MANAGER] loading extensions from " + this.extensionPackagesRoot)
if (fs.existsSync(path.join(this.extensionPackagesRoot, "package-lock.json"))) {
await fs.remove(path.join(this.extensionPackagesRoot, "package-lock.json"))
@@ -71,8 +77,8 @@ export class ExtensionManager {
return await this.loadExtensions();
}
async getExtensionByManifest(manifestPath: string): Promise<InstalledExtension> {
let manifestJson: ExtensionManifest;
protected async getByManifest(manifestPath: string): Promise<InstalledExtension> {
let manifestJson: LensExtensionManifest;
try {
fs.accessSync(manifestPath, fs.constants.F_OK); // check manifest file for existence
manifestJson = __non_webpack_require__(manifestPath)
@@ -80,11 +86,8 @@ export class ExtensionManager {
logger.info("[EXTENSION-MANAGER] installed extension " + manifestJson.name)
return {
id: manifestJson.name,
version: manifestJson.version,
name: manifestJson.name,
manifestPath: path.join(this.nodeModulesPath, manifestJson.name, "package.json"),
manifest: manifestJson
manifest: manifestJson,
}
} catch (err) {
logger.error(`[EXTENSION-MANAGER]: can't install extension at ${manifestPath}: ${err}`, { manifestJson });
@@ -109,10 +112,10 @@ export class ExtensionManager {
async loadExtensions() {
const bundledExtensions = await this.loadBundledExtensions()
const localExtensions = await this.loadFromFolder(this.localFolderPath)
await fs.writeFile(path.join(this.packageJsonPath), JSON.stringify(this.packagesJson, null, 2), {mode: 0o600})
await fs.writeFile(path.join(this.packageJsonPath), JSON.stringify(this.packagesJson, null, 2), { mode: 0o600 })
await this.installPackages()
const extensions = bundledExtensions.concat(localExtensions)
return new Map(extensions.map(ext => [ext.id, ext]));
return new Map(extensions.map(ext => [ext.manifestPath, ext]));
}
async loadBundledExtensions() {
@@ -126,8 +129,9 @@ export class ExtensionManager {
}
const absPath = path.resolve(folderPath, fileName);
const manifestPath = path.resolve(absPath, "package.json");
const ext = await this.getExtensionByManifest(manifestPath).catch(() => null)
const ext = await this.getByManifest(manifestPath).catch(() => null)
if (ext) {
ext.isBundled = true;
extensions.push(ext)
}
}
@@ -152,7 +156,7 @@ export class ExtensionManager {
continue
}
const manifestPath = path.resolve(absPath, "package.json");
const ext = await this.getExtensionByManifest(manifestPath).catch(() => null)
const ext = await this.getByManifest(manifestPath).catch(() => null)
if (ext) {
extensions.push(ext)
}
+2 -2
View File
@@ -15,7 +15,7 @@ export class ExtensionStore<T = any> extends BaseStore<T> {
await super.load()
}
protected storePath() {
return path.join(super.storePath(), "extension-store", this.extension.name)
protected cwd() {
return path.join(super.cwd(), "extension-store", this.extension.name)
}
}
+93 -80
View File
@@ -1,75 +1,111 @@
import { readJsonSync } from "fs-extra";
import { action, observable, toJS } from "mobx";
import type { InstalledExtension } from "./extension-manager";
import { action, reaction } from "mobx";
import logger from "../main/logger";
import { BaseRegistry } from "./registries/base-registry";
import { ExtensionStore } from "./extension-store";
export type ExtensionId = string | ExtensionPackageJsonPath;
export type ExtensionPackageJsonPath = string;
export type ExtensionVersion = string | number;
export type LensExtensionId = string; // path to manifest (package.json)
export type LensExtensionConstructor = new (...args: ConstructorParameters<typeof LensExtension>) => LensExtension;
export interface ExtensionModel {
id: ExtensionId;
version: ExtensionVersion;
export interface LensExtensionManifest {
name: string;
manifestPath: string;
version: string;
description?: string;
enabled?: boolean;
updateUrl?: string;
main?: string; // path to %ext/dist/main.js
renderer?: string; // path to %ext/dist/renderer.js
}
export interface ExtensionManifest extends ExtensionModel {
main?: string;
renderer?: string;
description?: string; // todo: add more fields similar to package.json + some extra
export interface LensExtensionStoreModel {
isEnabled: boolean;
}
export class LensExtension implements ExtensionModel {
public id: ExtensionId;
public updateUrl: string;
protected disposers: (() => void)[] = [];
export class LensExtension<S extends ExtensionStore<LensExtensionStoreModel> = any> {
protected store: S;
readonly manifest: LensExtensionManifest;
readonly manifestPath: string;
readonly isBundled: boolean;
@observable name = "";
@observable description = "";
@observable version: ExtensionVersion = "0.0.0";
@observable manifest: ExtensionManifest;
@observable manifestPath: string;
@observable isEnabled = false;
constructor({ manifest, manifestPath, isBundled }: InstalledExtension) {
this.manifest = manifest
this.manifestPath = manifestPath
this.isBundled = !!isBundled
this.init();
}
constructor(model: ExtensionModel, manifest: ExtensionManifest) {
this.importModel(model, manifest);
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 {
return this.manifestPath;
}
get name() {
return this.manifest.name
}
get version() {
return this.manifest.version
}
get description() {
return this.manifest.description
}
@action
async importModel({ enabled, manifestPath, ...model }: ExtensionModel, manifest?: ExtensionManifest) {
try {
this.manifest = manifest || await readJsonSync(manifestPath, { throws: true })
this.manifestPath = manifestPath;
Object.assign(this, model);
} catch (err) {
logger.error(`[EXTENSION]: cannot read manifest at ${manifestPath}`, { ...model, err: String(err) })
this.disable();
}
}
async migrate(appVersion: string) {
// mock
}
async enable() {
this.isEnabled = true;
logger.info(`[EXTENSION]: enabled ${this.name}@${this.version}`);
if (this.isEnabled) return;
this.store.data.isEnabled = true;
this.onActivate();
logger.info(`[EXTENSION]: enabled ${this.name}@${this.version}`);
}
@action
async disable() {
if (!this.isEnabled) return;
this.store.data.isEnabled = false;
this.onDeactivate();
this.isEnabled = false;
this.disposers.forEach(cleanUp => cleanUp());
this.disposers.length = 0;
logger.info(`[EXTENSION]: disabled ${this.name}@${this.version}`);
}
// todo: add more hooks
toggle(enable?: boolean) {
if (typeof enable === "boolean") {
enable ? this.enable() : this.disable()
} else {
this.isEnabled ? this.disable() : this.enable()
}
}
async whenEnabled(handlers: () => Function[]) {
const disposers: Function[] = [];
const unregisterHandlers = () => {
disposers.forEach(unregister => unregister())
disposers.length = 0;
}
const cancelReaction = reaction(() => this.isEnabled, isEnabled => {
if (isEnabled) {
disposers.push(...handlers());
} else {
unregisterHandlers();
}
}, {
fireImmediately: true
})
return () => {
unregisterHandlers();
cancelReaction();
}
}
protected onActivate() {
// mock
}
@@ -77,37 +113,14 @@ export class LensExtension implements ExtensionModel {
protected onDeactivate() {
// mock
}
}
registerTo<T = any>(registry: BaseRegistry<T>, items: T[] = []) {
const disposers = items.map(item => registry.add(item));
this.disposers.push(...disposers);
return () => {
this.disposers = this.disposers.filter(disposer => !disposers.includes(disposer))
};
}
getMeta() {
return toJS({
id: this.id,
manifest: this.manifest,
manifestPath: this.manifestPath,
enabled: this.isEnabled
}, {
recurseEverything: true
})
}
toJSON(): ExtensionModel {
return toJS({
id: this.id,
name: this.name,
version: this.version,
description: this.description,
manifestPath: this.manifestPath,
enabled: this.isEnabled,
updateUrl: this.updateUrl,
}, {
recurseEverything: true,
})
function createBaseStore() {
return class extends ExtensionStore<LensExtensionStoreModel> {
constructor() {
super({
configName: "state"
});
}
}
}
+11 -5
View File
@@ -1,5 +1,5 @@
// Base class for extensions-api registries
import { observable } from "mobx";
import { action, observable } from "mobx";
export class BaseRegistry<T = any> {
protected items = observable<T>([], { deep: false });
@@ -8,10 +8,16 @@ export class BaseRegistry<T = any> {
return this.items.toJS();
}
add(item: T) {
this.items.push(item);
return () => {
@action
add(...items: T[]) {
this.items.push(...items);
return () => this.remove(...items);
}
@action
remove(...items: T[]) {
items.forEach(item => {
this.items.remove(item); // works because of {deep: false};
}
})
}
}