mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Introduce clearer boundry between extensions (#7164)
- Bundled extensions are always enabled, and are always compatible - Have bundled extensions be loaded asyncronously to support typescript dynamic import (which is typed) as opposed to require Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
351f9d492f
commit
058494bc73
@ -209,7 +209,7 @@ export abstract class LensProtocolRouter {
|
||||
return name;
|
||||
}
|
||||
|
||||
if (!this.dependencies.isExtensionEnabled(extension)) {
|
||||
if (!extension.isBundled && !this.dependencies.isExtensionEnabled(extension.id)) {
|
||||
this.dependencies.logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not enabled`);
|
||||
|
||||
return name;
|
||||
|
||||
@ -113,13 +113,13 @@ describe("ExtensionLoader", () => {
|
||||
});
|
||||
|
||||
it("renderer updates extension after ipc broadcast", async () => {
|
||||
expect(extensionLoader.userExtensions).toEqual(new Map());
|
||||
expect(extensionLoader.userExtensions.get()).toEqual(new Map());
|
||||
|
||||
await extensionLoader.init();
|
||||
await delay(10);
|
||||
|
||||
// Assert the extensions after the extension broadcast event
|
||||
expect(extensionLoader.userExtensions).toEqual(
|
||||
expect(extensionLoader.userExtensions.get()).toEqual(
|
||||
new Map([
|
||||
["manifest/path", {
|
||||
absolutePath: "/test/1",
|
||||
|
||||
@ -10,7 +10,7 @@ import { broadcastMessage, ipcMainHandle, ipcRendererOn } from "../../common/ipc
|
||||
import { toJS } from "../../common/utils";
|
||||
import { isErrnoException } from "@k8slens/utilities";
|
||||
import type { ExtensionLoader } from "../extension-loader";
|
||||
import type { InstalledExtension, LensExtensionId, LensExtensionManifest } from "@k8slens/legacy-extensions";
|
||||
import type { InstalledExtension, LensExtensionId, LensExtensionManifest, ExternalInstalledExtension } from "@k8slens/legacy-extensions";
|
||||
import type { ExtensionInstallationStateStore } from "../extension-installation-state-store/extension-installation-state-store";
|
||||
import { extensionDiscoveryStateChannel } from "../../common/ipc/extension-handling";
|
||||
import { requestInitialExtensionDiscovery } from "../../renderer/ipc";
|
||||
@ -73,10 +73,6 @@ interface ExtensionDiscoveryChannelMessage {
|
||||
*/
|
||||
const isDirectoryLike = (lstat: Stats) => lstat.isDirectory() || lstat.isSymbolicLink();
|
||||
|
||||
interface LoadFromFolderOptions {
|
||||
isBundled?: boolean;
|
||||
}
|
||||
|
||||
interface ExtensionDiscoveryEvents {
|
||||
add: (ext: InstalledExtension) => void;
|
||||
remove: (extId: LensExtensionId) => void;
|
||||
@ -271,7 +267,7 @@ export class ExtensionDiscovery {
|
||||
* @param extensionId The ID of the extension to uninstall.
|
||||
*/
|
||||
async uninstallExtension(extensionId: LensExtensionId): Promise<void> {
|
||||
const extension = this.extensions.get(extensionId) ?? this.dependencies.extensionLoader.getExtension(extensionId);
|
||||
const extension = this.extensions.get(extensionId) ?? this.dependencies.extensionLoader.getExtensionById(extensionId);
|
||||
|
||||
if (!extension) {
|
||||
return void this.dependencies.logger.warn(`${logModule} could not uninstall extension, not found`, { id: extensionId });
|
||||
@ -330,24 +326,26 @@ export class ExtensionDiscovery {
|
||||
* Returns InstalledExtension from path to package.json file.
|
||||
* Also updates this.packagesJson.
|
||||
*/
|
||||
protected async getByManifest(manifestPath: string, { isBundled = false } = {}): Promise<InstalledExtension | null> {
|
||||
protected async loadExtensionFromFolder(folderPath: string): Promise<ExternalInstalledExtension | null> {
|
||||
const manifestPath = this.dependencies.joinPaths(folderPath, manifestFilename);
|
||||
|
||||
try {
|
||||
const manifest = await this.dependencies.readJsonFile(manifestPath) as unknown as LensExtensionManifest;
|
||||
const id = isBundled ? manifestPath : this.getInstalledManifestPath(manifest.name);
|
||||
const isEnabled = this.dependencies.isExtensionEnabled({ id, isBundled });
|
||||
const id = this.getInstalledManifestPath(manifest.name);
|
||||
const isEnabled = this.dependencies.isExtensionEnabled(id);
|
||||
const extensionDir = this.dependencies.getDirnameOfPath(manifestPath);
|
||||
const npmPackage = this.dependencies.joinPaths(extensionDir, `${manifest.name}-${manifest.version}.tgz`);
|
||||
const absolutePath = this.dependencies.isProduction && await this.dependencies.pathExists(npmPackage)
|
||||
? npmPackage
|
||||
: extensionDir;
|
||||
const isCompatible = isBundled || this.dependencies.isCompatibleExtension(manifest);
|
||||
const isCompatible = this.dependencies.isCompatibleExtension(manifest);
|
||||
|
||||
return {
|
||||
id,
|
||||
absolutePath,
|
||||
manifestPath: id,
|
||||
manifest,
|
||||
isBundled,
|
||||
isBundled: false,
|
||||
isEnabled,
|
||||
isCompatible,
|
||||
};
|
||||
@ -363,14 +361,14 @@ export class ExtensionDiscovery {
|
||||
}
|
||||
}
|
||||
|
||||
async ensureExtensions(): Promise<Map<LensExtensionId, InstalledExtension>> {
|
||||
async ensureExtensions(): Promise<Map<LensExtensionId, ExternalInstalledExtension>> {
|
||||
const userExtensions = await this.loadFromFolder(this.localFolderPath);
|
||||
|
||||
return this.extensions = new Map(userExtensions.map(extension => [extension.id, extension]));
|
||||
}
|
||||
|
||||
async loadFromFolder(folderPath: string): Promise<InstalledExtension[]> {
|
||||
const extensions: InstalledExtension[] = [];
|
||||
async loadFromFolder(folderPath: string): Promise<ExternalInstalledExtension[]> {
|
||||
const extensions: ExternalInstalledExtension[] = [];
|
||||
const paths = await this.dependencies.readDirectory(folderPath);
|
||||
|
||||
for (const fileName of paths) {
|
||||
@ -403,16 +401,6 @@ export class ExtensionDiscovery {
|
||||
return extensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads extension from absolute path, updates this.packagesJson to include it and returns the extension.
|
||||
* @param folderPath Folder path to extension
|
||||
*/
|
||||
async loadExtensionFromFolder(folderPath: string, { isBundled = false }: LoadFromFolderOptions = {}): Promise<InstalledExtension | null> {
|
||||
const manifestPath = this.dependencies.joinPaths(folderPath, manifestFilename);
|
||||
|
||||
return this.getByManifest(manifestPath, { isBundled });
|
||||
}
|
||||
|
||||
toJSON(): ExtensionDiscoveryChannelMessage {
|
||||
return toJS({
|
||||
isLoaded: this.isLoaded,
|
||||
|
||||
@ -3,11 +3,14 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { LensExtensionConstructor, InstalledExtension } from "@k8slens/legacy-extensions";
|
||||
import type { LensExtensionConstructor, BundledInstalledExtension, ExternalInstalledExtension, BundledLensExtensionConstructor } from "@k8slens/legacy-extensions";
|
||||
import { getInjectionToken } from "@ogre-tools/injectable";
|
||||
import type { LensExtension } from "../lens-extension";
|
||||
|
||||
export type CreateExtensionInstance = (ExtensionClass: LensExtensionConstructor, extension: InstalledExtension) => LensExtension;
|
||||
export interface CreateExtensionInstance {
|
||||
(ExtensionClass: LensExtensionConstructor, extension: ExternalInstalledExtension): LensExtension;
|
||||
(ExtensionClass: BundledLensExtensionConstructor, extension: BundledInstalledExtension): LensExtension;
|
||||
}
|
||||
|
||||
export const createExtensionInstanceInjectionToken = getInjectionToken<CreateExtensionInstance>({
|
||||
id: "create-extension-instance-token",
|
||||
|
||||
@ -6,9 +6,10 @@
|
||||
import { ipcMain, ipcRenderer } from "electron";
|
||||
import { isEqual } from "lodash";
|
||||
import type { ObservableMap } from "mobx";
|
||||
import { action, computed, makeObservable, toJS, observable, observe, reaction, when } from "mobx";
|
||||
import { runInAction, action, computed, toJS, observable, reaction, when } from "mobx";
|
||||
import { broadcastMessage, ipcMainOn, ipcRendererOn, ipcMainHandle } from "../../common/ipc";
|
||||
import { isDefined } from "@k8slens/utilities";
|
||||
import { isDefined, iter } from "@k8slens/utilities";
|
||||
import type { ExternalInstalledExtension, InstalledExtension, LensExtensionConstructor, LensExtensionId, BundledExtension } from "@k8slens/legacy-extensions";
|
||||
import type { LensExtension } from "../lens-extension";
|
||||
import { extensionLoaderFromMainChannel, extensionLoaderFromRendererChannel } from "../../common/ipc/extension-handling";
|
||||
import { requestExtensionLoaderInitialState } from "../../renderer/ipc";
|
||||
@ -19,7 +20,6 @@ import type { Extension } from "./extension/extension.injectable";
|
||||
import type { Logger } from "../../common/logger";
|
||||
import type { JoinPaths } from "../../common/path/join-paths.injectable";
|
||||
import type { GetDirnameOfPath } from "../../common/path/get-dirname.injectable";
|
||||
import type { LensExtensionId, BundledExtension, InstalledExtension, LensExtensionConstructor } from "@k8slens/legacy-extensions";
|
||||
import type { UpdateExtensionsState } from "../../features/extensions/enabled/common/update-state.injectable";
|
||||
|
||||
const logModule = "[EXTENSIONS-LOADER]";
|
||||
@ -60,51 +60,21 @@ export class ExtensionLoader {
|
||||
*/
|
||||
protected readonly nonInstancesByName = observable.set<string>();
|
||||
|
||||
/**
|
||||
* This is updated by the `observe` in the constructor. DO NOT write directly to it
|
||||
*/
|
||||
protected readonly instancesByName = observable.map<string, LensExtension>();
|
||||
protected readonly instancesByName = computed(() => new Map((
|
||||
iter.chain(this.dependencies.extensionInstances.entries())
|
||||
.map(([, instance]) => [instance.name, instance])
|
||||
)));
|
||||
|
||||
private readonly onRemoveExtensionId = new EventEmitter<[string]>();
|
||||
|
||||
@observable isLoaded = false;
|
||||
readonly isLoaded = observable.box(false);
|
||||
|
||||
get whenLoaded() {
|
||||
return when(() => this.isLoaded);
|
||||
}
|
||||
constructor(protected readonly dependencies: Dependencies) {}
|
||||
|
||||
constructor(protected readonly dependencies: Dependencies) {
|
||||
makeObservable(this);
|
||||
|
||||
observe(this.dependencies.extensionInstances, change => {
|
||||
switch (change.type) {
|
||||
case "add":
|
||||
if (this.instancesByName.has(change.newValue.name)) {
|
||||
throw new TypeError("Extension names must be unique");
|
||||
}
|
||||
|
||||
this.instancesByName.set(change.newValue.name, change.newValue);
|
||||
break;
|
||||
case "delete":
|
||||
this.instancesByName.delete(change.oldValue.name);
|
||||
break;
|
||||
case "update":
|
||||
throw new Error("Extension instances shouldn't be updated");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@computed get userExtensions(): Map<LensExtensionId, InstalledExtension> {
|
||||
const extensions = this.toJSON();
|
||||
|
||||
extensions.forEach((ext, extId) => {
|
||||
if (ext.isBundled) {
|
||||
extensions.delete(extId);
|
||||
}
|
||||
});
|
||||
|
||||
return extensions;
|
||||
}
|
||||
readonly userExtensions = computed(() => new Map((
|
||||
this.extensions.toJSON()
|
||||
.filter(([, extension]) => !extension.isBundled)
|
||||
)));
|
||||
|
||||
/**
|
||||
* Get the extension instance by its manifest name
|
||||
@ -120,19 +90,18 @@ export class ExtensionLoader {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.instancesByName.get(name);
|
||||
return this.instancesByName.get().get(name);
|
||||
}
|
||||
|
||||
// Transform userExtensions to a state object for storing into ExtensionsStore
|
||||
@computed get storeState() {
|
||||
return Array.from(this.userExtensions)
|
||||
.map(([extId, extension]) => [extId, {
|
||||
readonly storeState = computed(() => Array.from(
|
||||
this.userExtensions.get(),
|
||||
([extId, extension]) => [extId, {
|
||||
enabled: extension.isEnabled,
|
||||
name: extension.manifest.name,
|
||||
}] as const);
|
||||
}
|
||||
}] as const,
|
||||
));
|
||||
|
||||
@action
|
||||
async init() {
|
||||
if (ipcMain) {
|
||||
await this.initMain();
|
||||
@ -140,7 +109,7 @@ export class ExtensionLoader {
|
||||
await this.initRenderer();
|
||||
}
|
||||
|
||||
await Promise.all([this.whenLoaded]);
|
||||
await when(() => this.isLoaded.get());
|
||||
|
||||
// broadcasting extensions between main/renderer processes
|
||||
reaction(() => this.toJSON(), () => this.broadcastExtensions(), {
|
||||
@ -148,8 +117,7 @@ export class ExtensionLoader {
|
||||
});
|
||||
|
||||
reaction(
|
||||
() => this.storeState,
|
||||
|
||||
() => this.storeState.get(),
|
||||
(state) => {
|
||||
this.dependencies.updateExtensionsState(state);
|
||||
},
|
||||
@ -199,18 +167,20 @@ export class ExtensionLoader {
|
||||
setIsEnabled(lensExtensionId: LensExtensionId, isEnabled: boolean) {
|
||||
const extension = this.extensions.get(lensExtensionId);
|
||||
|
||||
assert(extension, `Must register extension ${lensExtensionId} with before enabling it`);
|
||||
assert(extension, `Extension "${lensExtensionId}" must be registered before it can be enabled.`);
|
||||
assert(!extension.isBundled, `Cannot change the enabled state of a bundled extension`);
|
||||
|
||||
extension.isEnabled = isEnabled;
|
||||
}
|
||||
|
||||
protected async initMain() {
|
||||
this.isLoaded = true;
|
||||
runInAction(() => {
|
||||
this.isLoaded.set(true);
|
||||
});
|
||||
|
||||
await this.autoInitExtensions();
|
||||
|
||||
ipcMainHandle(extensionLoaderFromMainChannel, () => {
|
||||
return Array.from(this.toJSON());
|
||||
});
|
||||
ipcMainHandle(extensionLoaderFromMainChannel, () => [...this.toJSON()]);
|
||||
|
||||
ipcMainOn(extensionLoaderFromRendererChannel, (event, extensions: [LensExtensionId, InstalledExtension][]) => {
|
||||
this.syncExtensions(extensions);
|
||||
@ -219,7 +189,9 @@ export class ExtensionLoader {
|
||||
|
||||
protected async initRenderer() {
|
||||
const extensionListHandler = (extensions: [LensExtensionId, InstalledExtension][]) => {
|
||||
this.isLoaded = true;
|
||||
runInAction(() => {
|
||||
this.isLoaded.set(true);
|
||||
});
|
||||
this.syncExtensions(extensions);
|
||||
|
||||
const receivedExtensionIds = extensions.map(([lensExtensionId]) => lensExtensionId);
|
||||
@ -255,10 +227,10 @@ export class ExtensionLoader {
|
||||
}
|
||||
|
||||
protected async loadBundledExtensions() {
|
||||
return this.dependencies.bundledExtensions
|
||||
.map(extension => {
|
||||
const bundledExtensions = await Promise.all((this.dependencies.bundledExtensions
|
||||
.map(async extension => {
|
||||
try {
|
||||
const LensExtensionClass = extension[this.dependencies.extensionEntryPointName]();
|
||||
const LensExtensionClass = await extension[this.dependencies.extensionEntryPointName]();
|
||||
|
||||
if (!LensExtensionClass) {
|
||||
return null;
|
||||
@ -291,7 +263,9 @@ export class ExtensionLoader {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(isDefined);
|
||||
));
|
||||
|
||||
return bundledExtensions.filter(isDefined);
|
||||
}
|
||||
|
||||
protected async loadExtensions(extensions: ExtensionBeingActivated[]): Promise<ExtensionLoading[]> {
|
||||
@ -332,6 +306,7 @@ export class ExtensionLoader {
|
||||
// 4. Return ExtensionLoading[]
|
||||
|
||||
return [...installedExtensions.entries()]
|
||||
.filter((entry): entry is [string, ExternalInstalledExtension] => !entry[1].isBundled)
|
||||
.map(([extId, extension]) => {
|
||||
const alreadyInit = this.dependencies.extensionInstances.has(extId) || this.nonInstancesByName.has(extension.manifest.name);
|
||||
|
||||
@ -391,7 +366,7 @@ export class ExtensionLoader {
|
||||
return loadedExtensions;
|
||||
}
|
||||
|
||||
protected requireExtension(extension: InstalledExtension): LensExtensionConstructor | null {
|
||||
protected requireExtension(extension: ExternalInstalledExtension): LensExtensionConstructor | null {
|
||||
const extRelativePath = extension.manifest[this.dependencies.extensionEntryPointName];
|
||||
|
||||
if (!extRelativePath) {
|
||||
@ -411,7 +386,7 @@ export class ExtensionLoader {
|
||||
return null;
|
||||
}
|
||||
|
||||
getExtension(extId: LensExtensionId) {
|
||||
getExtensionById(extId: LensExtensionId) {
|
||||
return this.extensions.get(extId);
|
||||
}
|
||||
|
||||
|
||||
@ -9,7 +9,6 @@ import type { LensExtensionDependencies } from "./lens-extension-set-dependencie
|
||||
import type { ProtocolHandlerRegistration } from "../common/protocol-handler/registration";
|
||||
import type { InstalledExtension, LegacyLensExtension, LensExtensionId, LensExtensionManifest } from "@k8slens/legacy-extensions";
|
||||
|
||||
|
||||
export const lensExtensionDependencies = Symbol("lens-extension-dependencies");
|
||||
export const Disposers = Symbol("disposers");
|
||||
|
||||
@ -42,14 +41,12 @@ export class LensExtension<
|
||||
[Disposers] = disposer();
|
||||
|
||||
constructor({ id, manifest, manifestPath, isBundled }: InstalledExtension) {
|
||||
makeObservable(this);
|
||||
|
||||
// id is the name of the manifest
|
||||
this.id = id;
|
||||
|
||||
this.manifest = manifest;
|
||||
this.manifest = manifest as LensExtensionManifest;
|
||||
this.manifestPath = manifestPath;
|
||||
this.isBundled = !!isBundled;
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
get name() {
|
||||
|
||||
@ -376,7 +376,7 @@ exports[`extensions - navigation using application menu when navigating to exten
|
||||
<p>
|
||||
Add new features via Lens Extensions. Check out the
|
||||
<a
|
||||
href="https://docs.k8slens.dev/extensions/"
|
||||
href="https://docs.k8slens.dev/extensions/lens-extensions"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
|
||||
@ -5,19 +5,14 @@
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import enabledExtensionsStateInjectable from "./state.injectable";
|
||||
|
||||
export interface IsEnabledExtensionDescriptor {
|
||||
readonly id: string;
|
||||
readonly isBundled: boolean;
|
||||
}
|
||||
|
||||
export type IsExtensionEnabled = (desc: IsEnabledExtensionDescriptor) => boolean;
|
||||
export type IsExtensionEnabled = (id: string) => boolean;
|
||||
|
||||
const isExtensionEnabledInjectable = getInjectable({
|
||||
id: "is-extension-enabled",
|
||||
instantiate: (di): IsExtensionEnabled => {
|
||||
const state = di.inject(enabledExtensionsStateInjectable);
|
||||
|
||||
return ({ id, isBundled }) => isBundled || (state.get(id)?.enabled ?? false);
|
||||
return (id) => (state.get(id)?.enabled ?? false);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -24,7 +24,7 @@ const createExtensionInstanceInjectable = getInjectable({
|
||||
};
|
||||
|
||||
return (ExtensionClass, extension) => {
|
||||
const instance = new ExtensionClass(extension) as LensMainExtension;
|
||||
const instance = new ExtensionClass(extension as any) as LensMainExtension;
|
||||
|
||||
(instance as Writable<LensMainExtension>)[lensExtensionDependencies] = deps;
|
||||
|
||||
|
||||
@ -10,16 +10,12 @@ import { noop } from "@k8slens/utilities";
|
||||
import type { LensProtocolRouterMain } from "../lens-protocol-router-main/lens-protocol-router-main";
|
||||
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
||||
import lensProtocolRouterMainInjectable from "../lens-protocol-router-main/lens-protocol-router-main.injectable";
|
||||
import getConfigurationFileModelInjectable from "../../../common/get-configuration-file-model/get-configuration-file-model.injectable";
|
||||
import { LensExtension } from "../../../extensions/lens-extension";
|
||||
import type { ObservableMap } from "mobx";
|
||||
import { runInAction } from "mobx";
|
||||
import extensionInstancesInjectable from "../../../extensions/extension-loader/extension-instances.injectable";
|
||||
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import broadcastMessageInjectable from "../../../common/ipc/broadcast-message.injectable";
|
||||
import pathExistsSyncInjectable from "../../../common/fs/path-exists-sync.injectable";
|
||||
import pathExistsInjectable from "../../../common/fs/path-exists.injectable";
|
||||
import readJsonSyncInjectable from "../../../common/fs/read-json-sync.injectable";
|
||||
import writeJsonSyncInjectable from "../../../common/fs/write-json-sync.injectable";
|
||||
import type { LensExtensionId } from "@k8slens/legacy-extensions";
|
||||
import type { LensExtensionState } from "../../../features/extensions/enabled/common/state.injectable";
|
||||
import enabledExtensionsStateInjectable from "../../../features/extensions/enabled/common/state.injectable";
|
||||
@ -39,16 +35,8 @@ describe("protocol router tests", () => {
|
||||
beforeEach(async () => {
|
||||
const di = getDiForUnitTesting();
|
||||
|
||||
di.override(pathExistsInjectable, () => () => { throw new Error("tried call pathExists without override"); });
|
||||
di.override(pathExistsSyncInjectable, () => () => { throw new Error("tried call pathExistsSync without override"); });
|
||||
di.override(readJsonSyncInjectable, () => () => { throw new Error("tried call readJsonSync without override"); });
|
||||
di.override(writeJsonSyncInjectable, () => () => { throw new Error("tried call writeJsonSync without override"); });
|
||||
|
||||
enabledExtensions = di.inject(enabledExtensionsStateInjectable);
|
||||
|
||||
di.permitSideEffects(getConfigurationFileModelInjectable);
|
||||
|
||||
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
|
||||
di.override(directoryForUserDataInjectable, () => "/some-directory-for-user-data");
|
||||
|
||||
broadcastMessageMock = jest.fn();
|
||||
di.override(broadcastMessageInjectable, () => broadcastMessageMock);
|
||||
@ -56,7 +44,9 @@ describe("protocol router tests", () => {
|
||||
extensionInstances = di.inject(extensionInstancesInjectable);
|
||||
lpr = di.inject(lensProtocolRouterMainInjectable);
|
||||
|
||||
lpr.rendererLoaded = true;
|
||||
runInAction(() => {
|
||||
lpr.rendererLoaded.set(true);
|
||||
});
|
||||
});
|
||||
|
||||
it("should broadcast invalid protocol on non-lens URLs", async () => {
|
||||
@ -69,7 +59,19 @@ describe("protocol router tests", () => {
|
||||
expect(broadcastMessageMock).toBeCalledWith(ProtocolHandlerInvalid, "invalid host", "lens://foobar");
|
||||
});
|
||||
|
||||
it("should not throw when has valid host", async () => {
|
||||
it("should broadcast internal route when called with valid host", async () => {
|
||||
lpr.addInternalHandler("/", noop);
|
||||
|
||||
try {
|
||||
expect(await lpr.route("lens://app")).toBeUndefined();
|
||||
} catch (error) {
|
||||
expect(throwIfDefined(error)).not.toThrow();
|
||||
}
|
||||
|
||||
expect(broadcastMessageMock).toHaveBeenCalledWith(ProtocolHandlerInternal, "lens://app", "matched");
|
||||
});
|
||||
|
||||
it("should broadcast external route when called with valid host", async () => {
|
||||
const extId = uuid.v4();
|
||||
const ext = new LensExtension({
|
||||
id: extId,
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
import * as proto from "../../../common/protocol-handler";
|
||||
import URLParse from "url-parse";
|
||||
import type { LensExtension } from "../../../extensions/lens-extension";
|
||||
import { observable, when, makeObservable } from "mobx";
|
||||
import { observable, when } from "mobx";
|
||||
import type { LensProtocolRouterDependencies, RouteAttempt } from "../../../common/protocol-handler";
|
||||
import { ProtocolHandlerInvalid } from "../../../common/protocol-handler";
|
||||
import { disposer, noop } from "@k8slens/utilities";
|
||||
@ -39,17 +39,15 @@ export interface LensProtocolRouterMainDependencies extends LensProtocolRouterDe
|
||||
}
|
||||
|
||||
export class LensProtocolRouterMain extends proto.LensProtocolRouter {
|
||||
private missingExtensionHandlers: FallbackHandler[] = [];
|
||||
private readonly missingExtensionHandlers: FallbackHandler[] = [];
|
||||
|
||||
// TODO: This is used to solve out-of-place temporal dependency. Remove, and solve otherwise.
|
||||
@observable rendererLoaded = false;
|
||||
readonly rendererLoaded = observable.box(false);
|
||||
|
||||
protected disposers = disposer();
|
||||
protected readonly disposers = disposer();
|
||||
|
||||
constructor(protected readonly dependencies: LensProtocolRouterMainDependencies) {
|
||||
super(dependencies);
|
||||
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
public cleanup() {
|
||||
@ -118,13 +116,12 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter {
|
||||
protected _routeToInternal(url: URLParse<Record<string, string | undefined>>): RouteAttempt {
|
||||
const rawUrl = url.toString(); // for sending to renderer
|
||||
const attempt = super._routeToInternal(url);
|
||||
const broadcastToRenderer = () => this.dependencies.broadcastMessage(proto.ProtocolHandlerInternal, rawUrl, attempt);
|
||||
|
||||
const sendRoutingToRenderer = () => this.dependencies.broadcastMessage(proto.ProtocolHandlerInternal, rawUrl, attempt);
|
||||
|
||||
if (this.rendererLoaded) {
|
||||
sendRoutingToRenderer();
|
||||
if (this.rendererLoaded.get()) {
|
||||
broadcastToRenderer();
|
||||
} else {
|
||||
this.disposers.push(when(() => this.rendererLoaded, sendRoutingToRenderer));
|
||||
this.disposers.push(when(() => this.rendererLoaded.get(), broadcastToRenderer));
|
||||
}
|
||||
|
||||
return attempt;
|
||||
@ -141,13 +138,12 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter {
|
||||
* argument.
|
||||
*/
|
||||
const attempt = await super._routeToExtension(new URLParse(url.toString(), true));
|
||||
const broadcastToRenderer = () => this.dependencies.broadcastMessage(proto.ProtocolHandlerExtension, rawUrl, attempt);
|
||||
|
||||
const sendRoutingToRenderer = () => this.dependencies.broadcastMessage(proto.ProtocolHandlerExtension, rawUrl, attempt);
|
||||
|
||||
if (this.rendererLoaded) {
|
||||
sendRoutingToRenderer();
|
||||
if (this.rendererLoaded.get()) {
|
||||
broadcastToRenderer();
|
||||
} else {
|
||||
this.disposers.push(when(() => this.rendererLoaded, sendRoutingToRenderer));
|
||||
this.disposers.push(when(() => this.rendererLoaded.get(), broadcastToRenderer));
|
||||
}
|
||||
|
||||
return attempt;
|
||||
|
||||
@ -16,7 +16,7 @@ const flagRendererAsLoadedInjectable = getInjectable({
|
||||
|
||||
runInAction(() => {
|
||||
// Todo: remove this kludge which enables out-of-place temporal dependency.
|
||||
lensProtocolRouterMain.rendererLoaded = true;
|
||||
lensProtocolRouterMain.rendererLoaded.set(true);
|
||||
});
|
||||
},
|
||||
}),
|
||||
|
||||
@ -16,7 +16,7 @@ const flagRendererAsNotLoadedInjectable = getInjectable({
|
||||
|
||||
runInAction(() => {
|
||||
// Todo: remove this kludge which enables out-of-place temporal dependency.
|
||||
lensProtocolRouterMain.rendererLoaded = false;
|
||||
lensProtocolRouterMain.rendererLoaded.set(false);
|
||||
});
|
||||
|
||||
return undefined;
|
||||
|
||||
@ -86,7 +86,7 @@ const attemptInstall = ({
|
||||
}
|
||||
|
||||
const extensionFolder = getExtensionDestFolder(name);
|
||||
const installedExtension = extensionLoader.getExtension(validatedRequest.id);
|
||||
const installedExtension = extensionLoader.getExtensionById(validatedRequest.id);
|
||||
|
||||
if (installedExtension) {
|
||||
const { version: oldVersion } = installedExtension.manifest;
|
||||
|
||||
@ -73,7 +73,7 @@ const unpackExtensionInjectable = getInjectable({
|
||||
await fse.move(unpackedRootFolder, extensionFolder, { overwrite: true });
|
||||
|
||||
// wait for the loader has actually install it
|
||||
await when(() => extensionLoader.userExtensions.has(id));
|
||||
await when(() => extensionLoader.userExtensions.get().has(id));
|
||||
|
||||
// Enable installed extensions by default.
|
||||
extensionLoader.setIsEnabled(id, true);
|
||||
|
||||
@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { LensExtensionId } from "@k8slens/legacy-extensions";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import extensionLoaderInjectable from "../../../extensions/extension-loader/extension-loader.injectable";
|
||||
|
||||
export type DisableExtension = (extId: LensExtensionId) => void;
|
||||
|
||||
const disableExtensionInjectable = getInjectable({
|
||||
id: "disable-extension",
|
||||
|
||||
instantiate: (di): DisableExtension => {
|
||||
const extensionLoader = di.inject(extensionLoaderInjectable);
|
||||
|
||||
return (extId) => {
|
||||
const ext = extensionLoader.getExtensionById(extId);
|
||||
|
||||
if (ext && !ext.isBundled) {
|
||||
ext.isEnabled = false;
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default disableExtensionInjectable;
|
||||
@ -1,18 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable";
|
||||
import { disableExtension } from "./disable-extension";
|
||||
|
||||
const disableExtensionInjectable = getInjectable({
|
||||
id: "disable-extension",
|
||||
|
||||
instantiate: (di) =>
|
||||
disableExtension({
|
||||
extensionLoader: di.inject(extensionLoaderInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
export default disableExtensionInjectable;
|
||||
@ -1,20 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { LensExtensionId } from "@k8slens/legacy-extensions";
|
||||
import type { ExtensionLoader } from "../../../../extensions/extension-loader";
|
||||
|
||||
interface Dependencies {
|
||||
extensionLoader: ExtensionLoader;
|
||||
}
|
||||
|
||||
export const disableExtension =
|
||||
({ extensionLoader }: Dependencies) =>
|
||||
(id: LensExtensionId) => {
|
||||
const extension = extensionLoader.getExtension(id);
|
||||
|
||||
if (extension) {
|
||||
extension.isEnabled = false;
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { LensExtensionId } from "@k8slens/legacy-extensions";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import extensionLoaderInjectable from "../../../extensions/extension-loader/extension-loader.injectable";
|
||||
|
||||
export type EnableExtension = (extId: LensExtensionId) => void;
|
||||
|
||||
const enableExtensionInjectable = getInjectable({
|
||||
id: "enable-extension",
|
||||
|
||||
instantiate: (di): EnableExtension => {
|
||||
const extensionLoader = di.inject(extensionLoaderInjectable);
|
||||
|
||||
return (extId) => {
|
||||
const ext = extensionLoader.getExtensionById(extId);
|
||||
|
||||
if (ext && !ext.isBundled) {
|
||||
ext.isEnabled = true;
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default enableExtensionInjectable;
|
||||
@ -1,18 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable";
|
||||
import { enableExtension } from "./enable-extension";
|
||||
|
||||
const enableExtensionInjectable = getInjectable({
|
||||
id: "enable-extension",
|
||||
|
||||
instantiate: (di) =>
|
||||
enableExtension({
|
||||
extensionLoader: di.inject(extensionLoaderInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
export default enableExtensionInjectable;
|
||||
@ -1,20 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { LensExtensionId } from "@k8slens/legacy-extensions";
|
||||
import type { ExtensionLoader } from "../../../../extensions/extension-loader";
|
||||
|
||||
interface Dependencies {
|
||||
extensionLoader: ExtensionLoader;
|
||||
}
|
||||
|
||||
export const enableExtension =
|
||||
({ extensionLoader }: Dependencies) =>
|
||||
(id: LensExtensionId) => {
|
||||
const extension = extensionLoader.getExtension(id);
|
||||
|
||||
if (extension) {
|
||||
extension.isEnabled = true;
|
||||
}
|
||||
};
|
||||
@ -4,89 +4,24 @@
|
||||
*/
|
||||
|
||||
import styles from "./extensions.module.scss";
|
||||
import type { IComputedValue } from "mobx";
|
||||
import {
|
||||
makeObservable,
|
||||
observable,
|
||||
reaction,
|
||||
when,
|
||||
} from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import React from "react";
|
||||
import { DropFileInput } from "../input";
|
||||
import { Install } from "./install";
|
||||
import { ExtensionInstall } from "./install";
|
||||
import { InstalledExtensions } from "./installed-extensions";
|
||||
import { Notice } from "./notice";
|
||||
import { SettingLayout } from "../layout/setting-layout";
|
||||
import { docsUrl } from "../../../common/vars";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
|
||||
import userExtensionsInjectable from "./user-extensions/user-extensions.injectable";
|
||||
import enableExtensionInjectable from "./enable-extension/enable-extension.injectable";
|
||||
import disableExtensionInjectable from "./disable-extension/disable-extension.injectable";
|
||||
import type { ConfirmUninstallExtension } from "./confirm-uninstall-extension.injectable";
|
||||
import confirmUninstallExtensionInjectable from "./confirm-uninstall-extension.injectable";
|
||||
import type { InstallExtensionFromInput } from "./install-extension-from-input.injectable";
|
||||
import installExtensionFromInputInjectable from "./install-extension-from-input.injectable";
|
||||
import installFromSelectFileDialogInjectable from "./install-from-select-file-dialog.injectable";
|
||||
import type { InstallOnDrop } from "./install-on-drop.injectable";
|
||||
import installOnDropInjectable from "./install-on-drop.injectable";
|
||||
import { supportedExtensionFormats } from "./supported-extension-formats";
|
||||
import extensionInstallationStateStoreInjectable from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
||||
import type { ExtensionInstallationStateStore } from "../../../extensions/extension-installation-state-store/extension-installation-state-store";
|
||||
import Gutter from "../gutter/gutter";
|
||||
import type { InstalledExtension, LensExtensionId } from "@k8slens/legacy-extensions";
|
||||
|
||||
interface Dependencies {
|
||||
userExtensions: IComputedValue<InstalledExtension[]>;
|
||||
enableExtension: (id: LensExtensionId) => void;
|
||||
disableExtension: (id: LensExtensionId) => void;
|
||||
confirmUninstallExtension: ConfirmUninstallExtension;
|
||||
installExtensionFromInput: InstallExtensionFromInput;
|
||||
installFromSelectFileDialog: () => Promise<void>;
|
||||
installOnDrop: InstallOnDrop;
|
||||
extensionInstallationStateStore: ExtensionInstallationStateStore;
|
||||
}
|
||||
|
||||
@observer
|
||||
class NonInjectedExtensions extends React.Component<Dependencies> {
|
||||
@observable installPath = "";
|
||||
|
||||
constructor(props: Dependencies) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
disposeOnUnmount(this, [
|
||||
reaction(() => this.props.userExtensions.get().length, (curSize, prevSize) => {
|
||||
if (curSize > prevSize) {
|
||||
disposeOnUnmount(this, [
|
||||
when(() => !this.props.extensionInstallationStateStore.anyInstalling, () => this.installPath = ""),
|
||||
]);
|
||||
}
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
render() {
|
||||
const userExtensions = this.props.userExtensions.get();
|
||||
|
||||
return (
|
||||
<DropFileInput onDropFiles={this.props.installOnDrop}>
|
||||
<SettingLayout
|
||||
className="Extensions"
|
||||
contentGaps={false}
|
||||
data-testid="extensions-page"
|
||||
>
|
||||
<section>
|
||||
<h1>Extensions</h1>
|
||||
|
||||
const ExtensionsNotice = () => (
|
||||
<Notice className={styles.notice}>
|
||||
<p>
|
||||
{"Add new features via Lens Extensions. Check out the "}
|
||||
<a
|
||||
href={`${docsUrl}/extensions/`}
|
||||
href={`${docsUrl}/extensions/lens-extensions`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
@ -103,39 +38,32 @@ class NonInjectedExtensions extends React.Component<Dependencies> {
|
||||
.
|
||||
</p>
|
||||
</Notice>
|
||||
);
|
||||
|
||||
<Install
|
||||
supportedFormats={supportedExtensionFormats}
|
||||
onChange={value => (this.installPath = value)}
|
||||
installFromInput={() => this.props.installExtensionFromInput(this.installPath)}
|
||||
installFromSelectFileDialog={this.props.installFromSelectFileDialog}
|
||||
installPath={this.installPath}
|
||||
/>
|
||||
interface Dependencies {
|
||||
installOnDrop: InstallOnDrop;
|
||||
}
|
||||
|
||||
const NonInjectedExtensions = ({ installOnDrop }: Dependencies) => (
|
||||
<DropFileInput onDropFiles={installOnDrop}>
|
||||
<SettingLayout
|
||||
className="Extensions"
|
||||
contentGaps={false}
|
||||
data-testid="extensions-page"
|
||||
>
|
||||
<section>
|
||||
<h1>Extensions</h1>
|
||||
<ExtensionsNotice />
|
||||
<ExtensionInstall />
|
||||
<Gutter size="md" />
|
||||
|
||||
<InstalledExtensions
|
||||
extensions={userExtensions}
|
||||
enable={this.props.enableExtension}
|
||||
disable={this.props.disableExtension}
|
||||
uninstall={this.props.confirmUninstallExtension}
|
||||
/>
|
||||
<InstalledExtensions />
|
||||
</section>
|
||||
</SettingLayout>
|
||||
</DropFileInput>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const Extensions = withInjectables<Dependencies>(NonInjectedExtensions, {
|
||||
getProps: (di) => ({
|
||||
userExtensions: di.inject(userExtensionsInjectable),
|
||||
enableExtension: di.inject(enableExtensionInjectable),
|
||||
disableExtension: di.inject(disableExtensionInjectable),
|
||||
confirmUninstallExtension: di.inject(confirmUninstallExtensionInjectable),
|
||||
installExtensionFromInput: di.inject(installExtensionFromInputInjectable),
|
||||
installOnDrop: di.inject(installOnDropInjectable),
|
||||
installFromSelectFileDialog: di.inject(installFromSelectFileDialogInjectable),
|
||||
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import styles from "./install.module.scss";
|
||||
import React from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { prevDefault } from "@k8slens/utilities";
|
||||
import { Button } from "../button";
|
||||
import { Icon } from "../icon";
|
||||
@ -16,17 +16,16 @@ import type { ExtensionInstallationStateStore } from "../../../extensions/extens
|
||||
import extensionInstallationStateStoreInjectable from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import { unionInputValidatorsAsync } from "../input/input_validators";
|
||||
|
||||
export interface InstallProps {
|
||||
installPath: string;
|
||||
supportedFormats: string[];
|
||||
onChange: (path: string) => void;
|
||||
installFromInput: () => void;
|
||||
installFromSelectFileDialog: () => void;
|
||||
}
|
||||
import { supportedExtensionFormats } from "./supported-extension-formats";
|
||||
import type { InstallExtensionFromInput } from "./install-extension-from-input.injectable";
|
||||
import type { InstallFromSelectFileDialog } from "./install-from-select-file-dialog.injectable";
|
||||
import installExtensionFromInputInjectable from "./install-extension-from-input.injectable";
|
||||
import installFromSelectFileDialogInjectable from "./install-from-select-file-dialog.injectable";
|
||||
|
||||
interface Dependencies {
|
||||
extensionInstallationStateStore: ExtensionInstallationStateStore;
|
||||
installState: ExtensionInstallationStateStore;
|
||||
installExtensionFromInput: InstallExtensionFromInput;
|
||||
installFromSelectFileDialog: InstallFromSelectFileDialog;
|
||||
}
|
||||
|
||||
const installInputValidator = unionInputValidatorsAsync(
|
||||
@ -38,31 +37,40 @@ const installInputValidator = unionInputValidatorsAsync(
|
||||
InputValidators.isPath,
|
||||
);
|
||||
|
||||
const NonInjectedInstall: React.FC<Dependencies & InstallProps> = ({
|
||||
installPath,
|
||||
supportedFormats,
|
||||
onChange,
|
||||
installFromInput,
|
||||
const installTitle = `Name or file path or URL to an extension package (${supportedExtensionFormats.join(", ")})`;
|
||||
|
||||
const NonInjectedInstall = observer(({
|
||||
installExtensionFromInput,
|
||||
installFromSelectFileDialog,
|
||||
extensionInstallationStateStore,
|
||||
}) => (
|
||||
installState,
|
||||
}: Dependencies) => {
|
||||
const [installPath, setInstallPath] = useState("");
|
||||
const prevAnyInstalling = useRef(installState.anyInstalling);
|
||||
|
||||
useEffect(() => {
|
||||
const currentlyInstalling = installState.anyInstalling;
|
||||
const previouslyInstalling = prevAnyInstalling.current;
|
||||
|
||||
if (!currentlyInstalling && previouslyInstalling) {
|
||||
prevAnyInstalling.current = false;
|
||||
setInstallPath("");
|
||||
}
|
||||
}, [installState.anyInstalling]);
|
||||
|
||||
return (
|
||||
<section>
|
||||
<SubTitle
|
||||
title={`Name or file path or URL to an extension package (${supportedFormats.join(
|
||||
", ",
|
||||
)})`}
|
||||
/>
|
||||
<SubTitle title={installTitle} />
|
||||
<div className={styles.inputs}>
|
||||
<div>
|
||||
<Input
|
||||
theme="round-black"
|
||||
disabled={extensionInstallationStateStore.anyPreInstallingOrInstalling}
|
||||
placeholder={"Name or file path or URL"}
|
||||
disabled={installState.anyPreInstallingOrInstalling}
|
||||
placeholder="Name or file path or URL"
|
||||
showErrorsAsTooltip={{ preferredPositions: TooltipPosition.BOTTOM }}
|
||||
validators={installPath ? installInputValidator : undefined}
|
||||
value={installPath}
|
||||
onChange={onChange}
|
||||
onSubmit={installFromInput}
|
||||
onChange={setInstallPath}
|
||||
onSubmit={() => installExtensionFromInput(installPath)}
|
||||
iconRight={(
|
||||
<Icon
|
||||
className={styles.icon}
|
||||
@ -79,11 +87,9 @@ const NonInjectedInstall: React.FC<Dependencies & InstallProps> = ({
|
||||
className={styles.button}
|
||||
primary
|
||||
label="Install"
|
||||
disabled={
|
||||
extensionInstallationStateStore.anyPreInstallingOrInstalling
|
||||
}
|
||||
waiting={extensionInstallationStateStore.anyPreInstallingOrInstalling}
|
||||
onClick={installFromInput}
|
||||
disabled={installState.anyPreInstallingOrInstalling}
|
||||
waiting={installState.anyPreInstallingOrInstalling}
|
||||
onClick={() => installExtensionFromInput(installPath)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -92,17 +98,14 @@ const NonInjectedInstall: React.FC<Dependencies & InstallProps> = ({
|
||||
: you can drag and drop a tarball file to this area
|
||||
</small>
|
||||
</section>
|
||||
);
|
||||
);
|
||||
});
|
||||
|
||||
export const Install = withInjectables<Dependencies, InstallProps>(
|
||||
observer(NonInjectedInstall),
|
||||
{
|
||||
export const ExtensionInstall = withInjectables<Dependencies>(NonInjectedInstall, {
|
||||
getProps: (di, props) => ({
|
||||
extensionInstallationStateStore: di.inject(
|
||||
extensionInstallationStateStoreInjectable,
|
||||
),
|
||||
|
||||
...props,
|
||||
installState: di.inject(extensionInstallationStateStoreInjectable),
|
||||
installExtensionFromInput: di.inject(installExtensionFromInputInjectable),
|
||||
installFromSelectFileDialog: di.inject(installFromSelectFileDialogInjectable),
|
||||
}),
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@ -4,8 +4,7 @@
|
||||
*/
|
||||
|
||||
import styles from "./installed-extensions.module.scss";
|
||||
import React, { useMemo } from "react";
|
||||
import type { ExtensionDiscovery } from "../../../extensions/extension-discovery/extension-discovery";
|
||||
import React from "react";
|
||||
import { Icon } from "../icon";
|
||||
import { List } from "../list/list";
|
||||
import { MenuActions, MenuItem } from "../menu";
|
||||
@ -17,18 +16,27 @@ import extensionDiscoveryInjectable from "../../../extensions/extension-discover
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import extensionInstallationStateStoreInjectable from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
||||
import type { ExtensionInstallationStateStore } from "../../../extensions/extension-installation-state-store/extension-installation-state-store";
|
||||
import type { InstalledExtension, LensExtensionId } from "@k8slens/legacy-extensions";
|
||||
import type { InstalledExtension } from "@k8slens/legacy-extensions";
|
||||
import type { IComputedValue } from "mobx";
|
||||
import type { ConfirmUninstallExtension } from "./confirm-uninstall-extension.injectable";
|
||||
import confirmUninstallExtensionInjectable from "./confirm-uninstall-extension.injectable";
|
||||
import type { DisableExtension } from "./disable-extension.injectable";
|
||||
import disableExtensionInjectable from "./disable-extension.injectable";
|
||||
import type { EnableExtension } from "./enable-extension.injectable";
|
||||
import enableExtensionInjectable from "./enable-extension.injectable";
|
||||
import userExtensionsInjectable from "./user-extensions/user-extensions.injectable";
|
||||
import type { ExtensionDiscovery } from "../../../extensions/extension-discovery/extension-discovery";
|
||||
|
||||
export interface InstalledExtensionsProps {
|
||||
extensions: InstalledExtension[];
|
||||
enable: (id: LensExtensionId) => void;
|
||||
disable: (id: LensExtensionId) => void;
|
||||
uninstall: (extension: InstalledExtension) => void;
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
extensionDiscovery: ExtensionDiscovery;
|
||||
extensionInstallationStateStore: ExtensionInstallationStateStore;
|
||||
userExtensions: IComputedValue<InstalledExtension[]>;
|
||||
enableExtension: EnableExtension;
|
||||
disableExtension: DisableExtension;
|
||||
confirmUninstallExtension: ConfirmUninstallExtension;
|
||||
}
|
||||
|
||||
function getStatus(extension: InstalledExtension) {
|
||||
@ -39,9 +47,43 @@ function getStatus(extension: InstalledExtension) {
|
||||
return extension.isEnabled ? "Enabled" : "Disabled";
|
||||
}
|
||||
|
||||
const NonInjectedInstalledExtensions = observer(({ extensionDiscovery, extensionInstallationStateStore, extensions, uninstall, enable, disable }: Dependencies & InstalledExtensionsProps) => {
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
const NonInjectedInstalledExtensions = observer(({
|
||||
extensionDiscovery,
|
||||
extensionInstallationStateStore,
|
||||
userExtensions,
|
||||
confirmUninstallExtension,
|
||||
enableExtension,
|
||||
disableExtension,
|
||||
}: Dependencies & InstalledExtensionsProps) => {
|
||||
if (!extensionDiscovery.isLoaded) {
|
||||
return <div><Spinner center /></div>;
|
||||
}
|
||||
|
||||
const extensions = userExtensions.get();
|
||||
|
||||
if (extensions.length == 0) {
|
||||
return (
|
||||
<div className="flex column h-full items-center justify-center">
|
||||
<Icon material="extension" className={styles.noItemsIcon}/>
|
||||
<h3 className="font-medium text-3xl mt-5 mb-2">
|
||||
There are no extensions installed.
|
||||
</h3>
|
||||
<p>Please use the form above to install or drag a tarball file here.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const toggleExtensionWith = (enabled: boolean) => (
|
||||
enabled
|
||||
? disableExtension
|
||||
: enableExtension
|
||||
);
|
||||
|
||||
return (
|
||||
<section data-testid="extensions-table">
|
||||
<List
|
||||
title={<h2 className={styles.title}>Installed extensions</h2>}
|
||||
columns={[
|
||||
{
|
||||
Header: "Name",
|
||||
accessor: "extension",
|
||||
@ -69,16 +111,13 @@ const NonInjectedInstalledExtensions = observer(({ extensionDiscovery, extension
|
||||
accessor: "actions",
|
||||
disableSortBy: true,
|
||||
width: 20,
|
||||
className: "actions",
|
||||
},
|
||||
], [],
|
||||
);
|
||||
|
||||
const data = useMemo(
|
||||
() => extensions.map(extension => {
|
||||
]}
|
||||
data={extensions.map(extension => {
|
||||
const { id, isEnabled, isCompatible, manifest } = extension;
|
||||
const { name, description, version } = manifest;
|
||||
const isUninstalling = extensionInstallationStateStore.isExtensionUninstalling(id);
|
||||
const toggleExtension = toggleExtensionWith(isEnabled);
|
||||
|
||||
return {
|
||||
extension: (
|
||||
@ -101,30 +140,20 @@ const NonInjectedInstalledExtensions = observer(({ extensionDiscovery, extension
|
||||
usePortal
|
||||
toolbar={false}>
|
||||
{isCompatible && (
|
||||
<>
|
||||
{isEnabled ? (
|
||||
<MenuItem
|
||||
disabled={isUninstalling}
|
||||
onClick={() => disable(id)}
|
||||
onClick={() => toggleExtension(id)}
|
||||
>
|
||||
<Icon material="unpublished" />
|
||||
<span className="title" aria-disabled={isUninstalling}>Disable</span>
|
||||
<Icon material={isEnabled ? "unpublished" : "check_circle"} />
|
||||
<span className="title" aria-disabled={isUninstalling}>
|
||||
{isEnabled ? "Disable" : "Enabled"}
|
||||
</span>
|
||||
</MenuItem>
|
||||
) : (
|
||||
<MenuItem
|
||||
disabled={isUninstalling}
|
||||
onClick={() => enable(id)}
|
||||
>
|
||||
<Icon material="check_circle" />
|
||||
<span className="title" aria-disabled={isUninstalling}>Enable</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<MenuItem
|
||||
disabled={isUninstalling}
|
||||
onClick={() => uninstall(extension)}
|
||||
onClick={() => confirmUninstallExtension(extension)}
|
||||
>
|
||||
<Icon material="delete" />
|
||||
<span className="title" aria-disabled={isUninstalling}>Uninstall</span>
|
||||
@ -132,32 +161,8 @@ const NonInjectedInstalledExtensions = observer(({ extensionDiscovery, extension
|
||||
</MenuActions>
|
||||
),
|
||||
};
|
||||
}), [extensions, extensionInstallationStateStore.anyUninstalling],
|
||||
);
|
||||
|
||||
if (!extensionDiscovery.isLoaded) {
|
||||
return <div><Spinner center /></div>;
|
||||
}
|
||||
|
||||
if (extensions.length == 0) {
|
||||
return (
|
||||
<div className="flex column h-full items-center justify-center">
|
||||
<Icon material="extension" className={styles.noItemsIcon}/>
|
||||
<h3 className="font-medium text-3xl mt-5 mb-2">
|
||||
There are no extensions installed.
|
||||
</h3>
|
||||
<p>Please use the form above to install or drag a tarball file here.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section data-testid="extensions-table">
|
||||
<List
|
||||
title={<h2 className={styles.title}>Installed extensions</h2>}
|
||||
columns={columns}
|
||||
data={data}
|
||||
items={extensions}
|
||||
})}
|
||||
items={userExtensions.get()}
|
||||
filters={[
|
||||
(extension) => extension.manifest.name,
|
||||
(extension) => getStatus(extension),
|
||||
@ -170,8 +175,12 @@ const NonInjectedInstalledExtensions = observer(({ extensionDiscovery, extension
|
||||
|
||||
export const InstalledExtensions = withInjectables<Dependencies, InstalledExtensionsProps>(NonInjectedInstalledExtensions, {
|
||||
getProps: (di, props) => ({
|
||||
...props,
|
||||
extensionDiscovery: di.inject(extensionDiscoveryInjectable),
|
||||
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable),
|
||||
...props,
|
||||
userExtensions: di.inject(userExtensionsInjectable),
|
||||
enableExtension: di.inject(enableExtensionInjectable),
|
||||
disableExtension: di.inject(disableExtensionInjectable),
|
||||
confirmUninstallExtension: di.inject(confirmUninstallExtensionInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
@ -27,7 +27,7 @@ const uninstallExtensionInjectable = getInjectable({
|
||||
const showErrorNotification = di.inject(showErrorNotificationInjectable);
|
||||
|
||||
return async (extensionId: LensExtensionId): Promise<boolean> => {
|
||||
const ext = extensionLoader.getExtension(extensionId);
|
||||
const ext = extensionLoader.getExtensionById(extensionId);
|
||||
|
||||
if (!ext) {
|
||||
logger.debug(`[EXTENSIONS]: cannot uninstall ${extensionId}, was not installed`);
|
||||
@ -45,7 +45,7 @@ const uninstallExtensionInjectable = getInjectable({
|
||||
await extensionDiscovery.uninstallExtension(extensionId);
|
||||
|
||||
// wait for the ExtensionLoader to actually uninstall the extension
|
||||
await when(() => !extensionLoader.userExtensions.has(extensionId));
|
||||
await when(() => !extensionLoader.userExtensions.get().has(extensionId));
|
||||
|
||||
showSuccessNotification(
|
||||
<p>
|
||||
|
||||
@ -12,7 +12,7 @@ const userExtensionsInjectable = getInjectable({
|
||||
instantiate: (di) => {
|
||||
const extensionLoader = di.inject(extensionLoaderInjectable);
|
||||
|
||||
return computed(() => [...extensionLoader.userExtensions.values()]);
|
||||
return computed(() => [...extensionLoader.userExtensions.get().values()]);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -30,7 +30,7 @@ const createExtensionInstanceInjectable = getInjectable({
|
||||
};
|
||||
|
||||
return (ExtensionClass, extension) => {
|
||||
const instance = new ExtensionClass(extension) as LensRendererExtension;
|
||||
const instance = new ExtensionClass(extension as any) as LensRendererExtension;
|
||||
|
||||
(instance as Writable<LensRendererExtension>)[lensExtensionDependencies] = deps;
|
||||
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import { getInjectionToken } from "@ogre-tools/injectable";
|
||||
import type {
|
||||
LensExtensionConstructor,
|
||||
LensExtensionManifest,
|
||||
BundledLensExtensionConstructor,
|
||||
BundledLensExtensionManifest,
|
||||
} from "./lens-extension";
|
||||
|
||||
export interface BundledExtension {
|
||||
readonly manifest: LensExtensionManifest;
|
||||
main: () => LensExtensionConstructor | null;
|
||||
renderer: () => LensExtensionConstructor | null;
|
||||
readonly manifest: BundledLensExtensionManifest;
|
||||
main: () => Promise<BundledLensExtensionConstructor | null>;
|
||||
renderer: () => Promise<BundledLensExtensionConstructor | null>;
|
||||
}
|
||||
|
||||
export const bundledExtensionInjectionToken =
|
||||
|
||||
@ -1,26 +1,39 @@
|
||||
export type LensExtensionId = string;
|
||||
|
||||
export type LensExtensionConstructor = new (
|
||||
ext: InstalledExtension
|
||||
) => LegacyLensExtension;
|
||||
export type BundledLensExtensionConstructor = new (
|
||||
ext: BundledInstalledExtension
|
||||
) => LegacyLensExtension;
|
||||
|
||||
export interface InstalledExtension {
|
||||
id: LensExtensionId;
|
||||
|
||||
readonly manifest: LensExtensionManifest;
|
||||
|
||||
export interface BaseInstalledExtension {
|
||||
readonly id: LensExtensionId;
|
||||
// Absolute path to the non-symlinked source folder,
|
||||
// e.g. "/Users/user/.k8slens/extensions/helloworld"
|
||||
readonly absolutePath: string;
|
||||
|
||||
/**
|
||||
* Absolute to the symlinked package.json file
|
||||
*/
|
||||
// Absolute to the symlinked package.json file
|
||||
readonly manifestPath: string;
|
||||
readonly isBundled: boolean;
|
||||
}
|
||||
|
||||
export interface BundledInstalledExtension extends BaseInstalledExtension {
|
||||
readonly manifest: BundledLensExtensionManifest;
|
||||
readonly isBundled: true;
|
||||
readonly isCompatible: true;
|
||||
readonly isEnabled: true;
|
||||
}
|
||||
|
||||
export interface ExternalInstalledExtension extends BaseInstalledExtension {
|
||||
readonly manifest: LensExtensionManifest;
|
||||
readonly isBundled: false;
|
||||
readonly isCompatible: boolean;
|
||||
isEnabled: boolean;
|
||||
}
|
||||
|
||||
export type InstalledExtension =
|
||||
| BundledInstalledExtension
|
||||
| ExternalInstalledExtension;
|
||||
|
||||
export interface LegacyLensExtension {
|
||||
readonly id: LensExtensionId;
|
||||
readonly manifest: LensExtensionManifest;
|
||||
@ -38,22 +51,11 @@ export interface LegacyLensExtension {
|
||||
activate(): Promise<void>;
|
||||
}
|
||||
|
||||
export interface LensExtensionManifest {
|
||||
export interface BundledLensExtensionManifest {
|
||||
name: string;
|
||||
version: string;
|
||||
description?: string;
|
||||
|
||||
main?: string; // path to %ext/dist/main.js
|
||||
renderer?: string; // path to %ext/dist/renderer.js
|
||||
/**
|
||||
* Supported Lens version engine by extension could be defined in `manifest.engines.lens`
|
||||
* Only MAJOR.MINOR version is taken in consideration.
|
||||
*/
|
||||
engines: {
|
||||
lens: string; // "semver"-package format
|
||||
npm?: string;
|
||||
node?: string;
|
||||
};
|
||||
publishConfig?: Partial<Record<string, string>>;
|
||||
|
||||
/**
|
||||
* Specify extension name used for persisting data.
|
||||
@ -61,3 +63,17 @@ export interface LensExtensionManifest {
|
||||
*/
|
||||
storeName?: string;
|
||||
}
|
||||
|
||||
export interface LensExtensionManifest extends BundledLensExtensionManifest {
|
||||
main?: string; // path to %ext/dist/main.js
|
||||
renderer?: string; // path to %ext/dist/renderer.js
|
||||
|
||||
/**
|
||||
* Supported Lens version engine by extension could be defined in `manifest.engines.lens`
|
||||
* Only MAJOR.MINOR version is taken in consideration.
|
||||
*/
|
||||
engines: {
|
||||
lens: string; // "semver"-package format
|
||||
[x: string]: string | undefined;
|
||||
};
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user