Replace CatalogEntityDetailRegistry with an injectable solution (#6605)

* Replace EntityDetailRegistry with an injectable solution

- Add some behavioural tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Update snapshots

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix import error

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Simplify loading extensions

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix lint

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Update snapshot

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove the last reminents of BaseRegistry

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix import errors

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix TypeError when loading extensions

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Update snapshots

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Cleanup LensExtensions

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove bad comment

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix type errors

Signed-off-by: Sebastian Malton <sebastian@malton.name>

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton authored and GitHub committed 2022-12-02 10:31:27 -05:00
1 parent 1acacbb7ef
commit 9ba92cb072
78 files changed
+15151 -573

No files matched your search

+3 -3
View File
@@ -7,9 +7,9 @@ export type { KubeObjectMenuRegistration, KubeObjectMenuComponents } from "../..
export type { AppPreferenceRegistration, AppPreferenceComponents } from "../../features/preferences/renderer/compliance-for-legacy-extension-api/app-preference-registration";
export type { KubeObjectDetailRegistration, KubeObjectDetailComponents } from "../../renderer/components/kube-object-details/kube-object-detail-registration";
export type { KubeObjectStatusRegistration } from "../../renderer/components/kube-object-status-icon/kube-object-status-registration";
export type { PageRegistration, RegisteredPage, PageParams, PageComponentProps, PageComponents, PageTarget } from "../registries/page-registry";
export type { ClusterPageMenuRegistration, ClusterPageMenuComponents } from "../registries/page-menu-registry";
export type { ProtocolHandlerRegistration, RouteParams as ProtocolRouteParams, RouteHandler as ProtocolRouteHandler } from "../registries/protocol-handler";
export type { PageRegistration, RegisteredPage, PageParams, PageComponentProps, PageComponents, PageTarget } from "../../renderer/routes/page-registration";
export type { ClusterPageMenuRegistration, ClusterPageMenuComponents } from "../../renderer/components/layout/cluster-page-menu";
export type { ProtocolHandlerRegistration, RouteParams as ProtocolRouteParams, RouteHandler as ProtocolRouteHandler } from "../../common/protocol-handler/registration";
export type { CustomCategoryViewProps, CustomCategoryViewComponents, CustomCategoryViewRegistration } from "../../renderer/components/+catalog/custom-views";
export type { ShellEnvModifier, ShellEnvContext } from "../../main/shell-session/shell-env-modifier/shell-env-modifier-registration";
export type { KubeObjectContextMenuItem, KubeObjectOnContextMenuOpenContext, KubeObjectOnContextMenuOpen, KubeObjectHandlers, KubeObjectHandlerRegistration } from "../../renderer/kube-object/handler";
@@ -8,12 +8,9 @@ import { isEqual } from "lodash";
import type { ObservableMap } from "mobx";
import { action, computed, makeObservable, observable, observe, reaction, when } from "mobx";
import { broadcastMessage, ipcMainOn, ipcRendererOn, ipcMainHandle } from "../../common/ipc";
import type { Disposer } from "../../common/utils";
import { isDefined, toJS } from "../../common/utils";
import type { InstalledExtension } from "../extension-discovery/extension-discovery";
import type { LensExtension, LensExtensionConstructor, LensExtensionId } from "../lens-extension";
import type { LensRendererExtension } from "../lens-renderer-extension";
import * as registries from "../registries";
import type { LensExtensionState } from "../extensions-store/extensions-store";
import { extensionLoaderFromMainChannel, extensionLoaderFromRendererChannel } from "../../common/ipc/extension-handling";
import { requestExtensionLoaderInitialState } from "../../renderer/ipc";
@@ -203,7 +200,7 @@ export class ExtensionLoader {
protected async initMain() {
this.isLoaded = true;
this.loadOnMain();
await this.autoInitExtensions();
ipcMainHandle(extensionLoaderFromMainChannel, () => {
return Array.from(this.toJSON());
@@ -251,38 +248,7 @@ export class ExtensionLoader {
});
}
loadOnMain() {
this.autoInitExtensions(() => Promise.resolve([]));
}
loadOnClusterManagerRenderer = () => {
this.dependencies.logger.debug(`${logModule}: load on main renderer (cluster manager)`);
return this.autoInitExtensions(async (ext) => {
const extension = ext as LensRendererExtension;
const removeItems = [
registries.CatalogEntityDetailRegistry.getInstance().add(extension.catalogEntityDetailItems),
];
this.onRemoveExtensionId.addListener((removedExtensionId) => {
if (removedExtensionId === extension.id) {
removeItems.forEach(remove => {
remove();
});
}
});
return removeItems;
});
};
loadOnClusterRenderer = () => {
this.dependencies.logger.debug(`${logModule}: load on cluster renderer (dashboard)`);
this.autoInitExtensions(async () => []);
};
protected async loadExtensions(installedExtensions: Map<string, InstalledExtension>, register: (ext: LensExtension) => Promise<Disposer[]>) {
protected async loadExtensions(installedExtensions: Map<string, InstalledExtension>) {
// Steps of the function:
// 1. require and call .activate for each Extension
// 2. Wait until every extension's onActivate has been resolved
@@ -346,7 +312,7 @@ export class ExtensionLoader {
// Return ExtensionLoading[]
return extensions.map(extension => {
const loaded = extension.instance.enable(register).catch((err) => {
const loaded = extension.instance.enable().catch((err) => {
this.dependencies.logger.error(`${logModule}: failed to enable`, { ext: extension, err });
});
@@ -357,12 +323,14 @@ export class ExtensionLoader {
});
}
protected autoInitExtensions(register: (ext: LensExtension) => Promise<Disposer[]>) {
autoInitExtensions() {
this.dependencies.logger.info(`${logModule}: auto initializing extensions`);
// Setup reaction to load extensions on JSON changes
reaction(() => this.toJSON(), installedExtensions => this.loadExtensions(installedExtensions, register));
reaction(() => this.toJSON(), installedExtensions => this.loadExtensions(installedExtensions));
// Load initial extensions
return this.loadExtensions(this.toJSON(), register);
return this.loadExtensions(this.toJSON());
}
protected requireExtension(extension: InstalledExtension): LensExtensionConstructor | null {
@@ -12,9 +12,11 @@ import type { CatalogEntityRegistry as RendererCatalogEntityRegistry } from "../
import type { GetExtensionPageParameters } from "../renderer/routes/get-extension-page-parameters.injectable";
import type { FileSystemProvisionerStore } from "./extension-loader/file-system-provisioner-store/file-system-provisioner-store";
import type { NavigateForExtension } from "../main/start-main-application/lens-window/navigate-for-extension.injectable";
import type { Logger } from "../common/logger";
export interface LensExtensionDependencies {
readonly fileSystemProvisionerStore: FileSystemProvisionerStore;
readonly logger: Logger;
}
export interface LensMainExtensionDependencies extends LensExtensionDependencies {
+6 -15
View File
@@ -5,12 +5,10 @@
import type { InstalledExtension } from "./extension-discovery/extension-discovery";
import { action, computed, makeObservable, observable } from "mobx";
import logger from "../main/logger";
import type { ProtocolHandlerRegistration } from "./registries";
import type { PackageJson } from "type-fest";
import type { Disposer } from "../common/utils";
import { disposer } from "../common/utils";
import type { LensExtensionDependencies } from "./lens-extension-set-dependencies";
import type { ProtocolHandlerRegistration } from "./common-api/registrations";
export type LensExtensionId = string; // path to manifest (package.json)
export type LensExtensionConstructor = new (...args: ConstructorParameters<typeof LensExtension>) => LensExtension;
@@ -88,20 +86,13 @@ export class LensExtension<Dependencies extends LensExtensionDependencies = Lens
}
@action
async enable(register: (ext: this) => Promise<Disposer[]>) {
async enable() {
if (this._isEnabled) {
return;
}
try {
this._isEnabled = true;
this[Disposers].push(...await register(this));
logger.info(`[EXTENSION]: enabled ${this.name}@${this.version}`);
} catch (error) {
logger.error(`[EXTENSION]: failed to activate ${this.name}@${this.version}: ${error}`);
}
this._isEnabled = true;
this[lensExtensionDependencies].logger.info(`[EXTENSION]: enabled ${this.name}@${this.version}`);
}
@action
@@ -115,9 +106,9 @@ export class LensExtension<Dependencies extends LensExtensionDependencies = Lens
try {
await this.onDeactivate();
this[Disposers]();
logger.info(`[EXTENSION]: disabled ${this.name}@${this.version}`);
this[lensExtensionDependencies].logger.info(`[EXTENSION]: disabled ${this.name}@${this.version}`);
} catch (error) {
logger.error(`[EXTENSION]: disabling ${this.name}@${this.version} threw an error: ${error}`);
this[lensExtensionDependencies].logger.error(`[EXTENSION]: disabling ${this.name}@${this.version} threw an error: ${error}`);
}
}
+6 -5
View File
@@ -3,7 +3,6 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type * as registries from "./registries";
import { Disposers, LensExtension, lensExtensionDependencies } from "./lens-extension";
import type { CatalogEntity } from "../common/catalog";
import type { Disposer } from "../common/utils";
@@ -30,11 +29,13 @@ import type { AppPreferenceTabRegistration } from "../features/preferences/rende
import type { KubeObjectDetailRegistration } from "../renderer/components/kube-object-details/kube-object-detail-registration";
import type { ClusterFrameChildComponent } from "../renderer/frames/cluster-frame/cluster-frame-child-component-injection-token";
import type { EntitySettingRegistration } from "../renderer/components/+entity-settings/extension-registrator.injectable";
import type { CatalogEntityDetailRegistration } from "../renderer/components/+catalog/entity-details/token";
import type { ClusterPageMenuRegistration, PageRegistration } from "./common-api/registrations";
export class LensRendererExtension extends LensExtension<LensRendererExtensionDependencies> {
globalPages: registries.PageRegistration[] = [];
clusterPages: registries.PageRegistration[] = [];
clusterPageMenus: registries.ClusterPageMenuRegistration[] = [];
globalPages: PageRegistration[] = [];
clusterPages: PageRegistration[] = [];
clusterPageMenus: ClusterPageMenuRegistration[] = [];
clusterFrameComponents: ClusterFrameChildComponent[] = [];
kubeObjectStatusTexts: KubeObjectStatusRegistration[] = [];
appPreferences: AppPreferenceRegistration[] = [];
@@ -47,7 +48,7 @@ export class LensRendererExtension extends LensExtension<LensRendererExtensionDe
commands: CommandRegistration[] = [];
welcomeMenus: WelcomeMenuRegistration[] = [];
welcomeBanners: WelcomeBannerRegistration[] = [];
catalogEntityDetailItems: registries.CatalogEntityDetailRegistration<CatalogEntity>[] = [];
catalogEntityDetailItems: CatalogEntityDetailRegistration<CatalogEntity>[] = [];
topBarItems: TopBarRegistration[] = [];
additionalCategoryColumns: AdditionalCategoryColumnRegistration[] = [];
customCategoryViews: CustomCategoryViewRegistration[] = [];
@@ -1,46 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
// Base class for extensions-api registries
import { action, observable, makeObservable } from "mobx";
import type { Disposer } from "../../common/utils";
import { Singleton } from "../../common/utils";
import type { LensExtension } from "../lens-extension";
export class BaseRegistry<T, I = T> extends Singleton {
private items = observable.map<T, I>([], { deep: false });
constructor() {
super();
makeObservable(this);
}
getItems(): I[] {
return Array.from(this.items.values());
}
@action
add(items: T | T[], extension?: LensExtension): Disposer {
const itemArray = [items].flat() as T[];
itemArray.forEach(item => {
this.items.set(item, this.getRegisteredItem(item, extension));
});
return () => this.remove(...itemArray);
}
// eslint-disable-next-line unused-imports/no-unused-vars-ts
protected getRegisteredItem(item: T, extension?: LensExtension): I {
return item as never;
}
@action
remove(...items: T[]) {
items.forEach(item => {
this.items.delete(item);
});
}
}
@@ -1,38 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type React from "react";
import type { Disposer } from "../../common/utils";
import type { CatalogEntity } from "../common-api/catalog";
import { BaseRegistry } from "./base-registry";
export interface CatalogEntityDetailsProps<T extends CatalogEntity> {
entity: T;
}
export interface CatalogEntityDetailComponents<T extends CatalogEntity> {
Details: React.ComponentType<CatalogEntityDetailsProps<T>>;
}
export interface CatalogEntityDetailRegistration<T extends CatalogEntity> {
kind: string;
apiVersions: string[];
components: CatalogEntityDetailComponents<T>;
priority?: number;
}
export class CatalogEntityDetailRegistry extends BaseRegistry<CatalogEntityDetailRegistration<CatalogEntity>> {
add<T extends CatalogEntity>(items: CatalogEntityDetailRegistration<T> | CatalogEntityDetailRegistration<T>[]): Disposer {
return super.add(items as never);
}
getItemsForKind(kind: string, apiVersion: string) {
const items = this.getItems().filter((item) => {
return item.kind === kind && item.apiVersions.includes(apiVersion);
});
return items.sort((a, b) => (b.priority ?? 50) - (a.priority ?? 50));
}
}
-11
View File
@@ -1,11 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
// All registries managed by extensions api
export * from "./page-registry";
export * from "./page-menu-registry";
export * from "./catalog-entity-detail-registry";
export * from "./protocol-handler";
@@ -1,23 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
// Extensions-api -> Register page menu items
import type { IconProps } from "../../renderer/components/icon";
import type React from "react";
import type { PageTarget } from "./page-registry";
import type { IComputedValue } from "mobx";
export interface ClusterPageMenuRegistration {
id?: string;
parentId?: string;
target?: PageTarget;
title: React.ReactNode;
components: ClusterPageMenuComponents;
visible?: IComputedValue<boolean>;
}
export interface ClusterPageMenuComponents {
Icon: React.ComponentType<IconProps>;
}
@@ -1,47 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { IComputedValue } from "mobx";
import type { PageParamInit, PageParam } from "../../renderer/navigation";
// Extensions-api -> Custom page registration
export interface PageRegistration {
/**
* Page ID, part of extension's page url, must be unique within same extension
* When not provided, first registered page without "id" would be used for page-menus without target.pageId for same extension
*/
id?: string;
params?: PageParams<string | Omit<PageParamInit<any>, "name" | "prefix">>;
components: PageComponents;
enabled?: IComputedValue<boolean>;
}
export interface PageComponents {
Page: React.ComponentType<any>;
}
export interface PageTarget {
extensionId?: string;
pageId?: string;
params?: PageParams;
}
export interface PageParams<V = any> {
[paramName: string]: V;
}
export interface PageComponentProps<P extends PageParams = {}> {
params?: {
[N in keyof P]: PageParam<P[N]>;
};
}
export interface RegisteredPage {
id: string;
extensionId: string;
url: string; // registered extension's page URL (without page params)
params: PageParams<PageParam<any>>; // normalized params
components: PageComponents; // normalized components
}
@@ -1,49 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
/**
* ProtocolHandlerRegistration is the data required for an extension to register
* a handler to a specific path or dynamic path.
*/
export interface ProtocolHandlerRegistration {
pathSchema: string;
handler: RouteHandler;
}
/**
* The collection of the dynamic parts of a URI which initiated a `lens://`
* protocol request
*/
export interface RouteParams {
/**
* the parts of the URI query string
*/
search: Record<string, string | undefined>;
/**
* the matching parts of the path. The dynamic parts of the URI path.
*/
pathname: Record<string, string | undefined>;
/**
* if the most specific path schema that is matched does not cover the whole
* of the URI's path. Then this field will be set to the remaining path
* segments.
*
* Example:
*
* If the path schema `/landing/:type` is the matched schema for the URI
* `/landing/soft/easy` then this field will be set to `"/easy"`.
*/
tail?: string;
}
/**
* RouteHandler represents the function signature of the handler function for
* `lens://` protocol routing.
*/
export interface RouteHandler {
(params: RouteParams): void;
}