Simplify pages/menus/registry extension api internal implementation (#1364)

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
Co-authored-by: Roman <ixrock@gmail.com>
This commit is contained in:
Jari KolehmainenandRoman authored and GitHub committed 2020-11-13 17:04:39 +02:00
1 parent eccc502ef2
commit df0f080380
21 files changed
+176 -142

No files matched your search

@@ -0,0 +1,23 @@
import { LensExtension } from "../lens-extension"
let ext: LensExtension = null
describe("lens extension", () => {
beforeEach(async () => {
ext = new LensExtension({
manifest: {
name: "foo-bar",
version: "0.1.1"
},
manifestPath: "/this/is/fake/package.json",
isBundled: false,
isEnabled: true
})
})
describe("name", () => {
it("returns name", () => {
expect(ext.name).toBe("foo-bar")
})
})
})
+11 -11
View File
@@ -57,29 +57,29 @@ export class ExtensionLoader {
loadOnMain() {
logger.info('[EXTENSIONS-LOADER]: load on main')
this.autoInitExtensions((ext: LensMainExtension) => [
registries.menuRegistry.add(ext.appMenus, { key: ext })
registries.menuRegistry.add(ext.appMenus)
]);
}
loadOnClusterManagerRenderer() {
logger.info('[EXTENSIONS-LOADER]: load on main renderer (cluster manager)')
this.autoInitExtensions((ext: LensRendererExtension) => [
registries.globalPageRegistry.add(ext.globalPages, { key: ext }),
registries.globalPageMenuRegistry.add(ext.globalPageMenus, { key: ext }),
registries.appPreferenceRegistry.add(ext.appPreferences, { key: ext }),
registries.clusterFeatureRegistry.add(ext.clusterFeatures, { key: ext }),
registries.statusBarRegistry.add(ext.statusBarItems, { key: ext }),
registries.globalPageRegistry.add(ext.globalPages, ext),
registries.globalPageMenuRegistry.add(ext.globalPageMenus, ext),
registries.appPreferenceRegistry.add(ext.appPreferences),
registries.clusterFeatureRegistry.add(ext.clusterFeatures),
registries.statusBarRegistry.add(ext.statusBarItems),
]);
}
loadOnClusterRenderer() {
logger.info('[EXTENSIONS-LOADER]: load on cluster renderer (dashboard)')
this.autoInitExtensions((ext: LensRendererExtension) => [
registries.clusterPageRegistry.add(ext.clusterPages, { key: ext }),
registries.clusterPageMenuRegistry.add(ext.clusterPageMenus, { key: ext }),
registries.kubeObjectMenuRegistry.add(ext.kubeObjectMenuItems, { key: ext }),
registries.kubeObjectDetailRegistry.add(ext.kubeObjectDetailItems, { key: ext }),
registries.kubeObjectStatusRegistry.add(ext.kubeObjectStatusTexts, { key: ext })
registries.clusterPageRegistry.add(ext.clusterPages, ext),
registries.clusterPageMenuRegistry.add(ext.clusterPageMenus, ext),
registries.kubeObjectMenuRegistry.add(ext.kubeObjectMenuItems),
registries.kubeObjectDetailRegistry.add(ext.kubeObjectDetailItems),
registries.kubeObjectStatusRegistry.add(ext.kubeObjectStatusTexts)
])
}
-10
View File
@@ -1,6 +1,5 @@
import type { InstalledExtension } from "./extension-manager";
import { action, observable, reaction } from "mobx";
import { compile } from "path-to-regexp"
import logger from "../main/logger";
export type LensExtensionId = string; // path to manifest (package.json)
@@ -15,7 +14,6 @@ export interface LensExtensionManifest {
}
export class LensExtension {
readonly routePrefix = "/extension/:name"
readonly manifest: LensExtensionManifest;
readonly manifestPath: string;
readonly isBundled: boolean;
@@ -44,14 +42,6 @@ export class LensExtension {
return this.manifest.description
}
getPageUrl(baseUrl = "") {
return compile(this.routePrefix)({ name: this.name }) + baseUrl;
}
getPageRoute(baseRoute = "") {
return this.routePrefix + baseRoute;
}
@action
async enable() {
if (this.isEnabled) return;
+2 -1
View File
@@ -2,13 +2,14 @@ import type { MenuRegistration } from "./registries/menu-registry";
import { observable } from "mobx";
import { LensExtension } from "./lens-extension"
import { WindowManager } from "../main/window-manager";
import { getPageUrl } from "./registries/page-registry"
export class LensMainExtension extends LensExtension {
@observable.shallow appMenus: MenuRegistration[] = []
async navigate(location?: string, frameId?: number) {
const windowManager = WindowManager.getInstance<WindowManager>();
const url = this.getPageUrl(location); // get full path to extension's page
const url = getPageUrl(this, location); // get full path to extension's page
await windowManager.navigate(url, frameId);
}
}
+2 -1
View File
@@ -1,6 +1,7 @@
import type { AppPreferenceRegistration, ClusterFeatureRegistration, KubeObjectDetailRegistration, KubeObjectMenuRegistration, KubeObjectStatusRegistration, PageMenuRegistration, PageRegistration, StatusBarRegistration, } from "./registries"
import { observable } from "mobx";
import { LensExtension } from "./lens-extension"
import { getPageUrl } from "./registries/page-registry"
export class LensRendererExtension extends LensExtension {
@observable.shallow globalPages: PageRegistration[] = []
@@ -16,6 +17,6 @@ export class LensRendererExtension extends LensExtension {
async navigate(location?: string) {
const { navigate } = await import("../renderer/navigation");
navigate(this.getPageUrl(location));
navigate(getPageUrl(this, location));
}
}
@@ -0,0 +1,31 @@
import { getPageUrl } from "../page-registry"
import { LensExtension } from "../../lens-extension"
let ext: LensExtension = null
describe("getPageUrl", () => {
beforeEach(async () => {
ext = new LensExtension({
manifest: {
name: "foo-bar",
version: "0.1.1"
},
manifestPath: "/this/is/fake/package.json",
isBundled: false,
isEnabled: true
})
})
it("returns a page url for extension", () => {
expect(getPageUrl(ext)).toBe("/extension/foo-bar")
})
it("allows to pass base url as parameter", () => {
expect(getPageUrl(ext, "/test")).toBe("/extension/foo-bar/test")
})
it("removes @", () => {
ext.manifest.name = "@foo/bar"
expect(getPageUrl(ext)).toBe("/extension/foo-bar")
})
})
@@ -1,12 +1,12 @@
import type React from "react"
import { BaseRegistry, BaseRegistryItem } from "./base-registry";
import { BaseRegistry } from "./base-registry";
export interface AppPreferenceComponents {
Hint: React.ComponentType<any>;
Input: React.ComponentType<any>;
}
export interface AppPreferenceRegistration extends BaseRegistryItem {
export interface AppPreferenceRegistration {
title: string;
components: AppPreferenceComponents;
}
+12 -53
View File
@@ -1,65 +1,24 @@
// Base class for extensions-api registries
import { action, observable } from "mobx";
import { LensExtension } from "../lens-extension";
import { getRandId } from "../../common/utils";
export type BaseRegistryKey = LensExtension | null;
export type BaseRegistryItemId = string | symbol;
export class BaseRegistry<T = any> {
private items = observable<T>([], { deep: false });
export interface BaseRegistryItem {
id?: BaseRegistryItemId; // uniq id, generated automatically when not provided
}
export interface BaseRegistryAddMeta {
key?: BaseRegistryKey;
merge?: boolean
}
export class BaseRegistry<T extends BaseRegistryItem = any> {
private items = observable.map<BaseRegistryKey, T[]>([], { deep: false });
getItems(): (T & { extension?: LensExtension | null })[] {
return Array.from(this.items).map(([ext, items]) => {
return items.map(item => ({
...item,
extension: ext,
}))
}).flat()
}
getById(itemId: BaseRegistryItemId, key?: BaseRegistryKey): T {
const byId = (item: BaseRegistryItem) => item.id === itemId;
if (key) {
return this.items.get(key)?.find(byId)
}
return this.getItems().find(byId);
getItems(): T[] {
return this.items.toJS();
}
@action
add(items: T | T[], { key = null, merge = true }: BaseRegistryAddMeta = {}) {
const normalizedItems = (Array.isArray(items) ? items : [items]).map((item: T) => {
item.id = item.id || getRandId();
return item;
});
if (merge && this.items.has(key)) {
const newItems = new Set(this.items.get(key));
normalizedItems.forEach(item => newItems.add(item))
this.items.set(key, [...newItems]);
} else {
this.items.set(key, normalizedItems);
}
return () => this.remove(normalizedItems, key)
add(items: T | T[]) {
const normalizedItems = (Array.isArray(items) ? items : [items])
this.items.push(...normalizedItems);
return () => this.remove(...normalizedItems);
}
@action
remove(items: T[], key: BaseRegistryKey = null) {
const storedItems = this.items.get(key);
if (!storedItems) return;
const newItems = storedItems.filter(item => !items.includes(item)); // works because of {deep: false};
if (newItems.length > 0) {
this.items.set(key, newItems)
} else {
this.items.delete(key);
}
remove(...items: T[]) {
items.forEach(item => {
this.items.remove(item); // works because of {deep: false};
})
}
}
@@ -1,12 +1,12 @@
import type React from "react"
import { BaseRegistry, BaseRegistryItem } from "./base-registry";
import { BaseRegistry } from "./base-registry";
import { ClusterFeature } from "../cluster-feature";
export interface ClusterFeatureComponents {
Description: React.ComponentType<any>;
}
export interface ClusterFeatureRegistration extends BaseRegistryItem {
export interface ClusterFeatureRegistration {
title: string;
components: ClusterFeatureComponents
feature: ClusterFeature
@@ -1,11 +1,11 @@
import React from "react"
import { BaseRegistry, BaseRegistryItem } from "./base-registry";
import { BaseRegistry } from "./base-registry";
export interface KubeObjectDetailComponents {
Details: React.ComponentType<any>;
}
export interface KubeObjectDetailRegistration extends BaseRegistryItem {
export interface KubeObjectDetailRegistration {
kind: string;
apiVersions: string[];
components: KubeObjectDetailComponents;
@@ -1,11 +1,11 @@
import React from "react"
import { BaseRegistry, BaseRegistryItem } from "./base-registry";
import { BaseRegistry } from "./base-registry";
export interface KubeObjectMenuComponents {
MenuItem: React.ComponentType<any>;
}
export interface KubeObjectMenuRegistration extends BaseRegistryItem {
export interface KubeObjectMenuRegistration {
kind: string;
apiVersions: string[];
components: KubeObjectMenuComponents;
@@ -1,7 +1,7 @@
import { KubeObject, KubeObjectStatus } from "../renderer-api/k8s-api";
import { BaseRegistry, BaseRegistryItem } from "./base-registry";
import { BaseRegistry } from "./base-registry";
export interface KubeObjectStatusRegistration extends BaseRegistryItem {
export interface KubeObjectStatusRegistration {
kind: string;
apiVersions: string[];
resolve: (object: KubeObject) => KubeObjectStatus;
+23 -12
View File
@@ -1,15 +1,21 @@
// Extensions-api -> Register page menu items
import type React from "react";
import { action } from "mobx";
import type { IconProps } from "../../renderer/components/icon";
import { BaseRegistry, BaseRegistryItem, BaseRegistryItemId } from "./base-registry";
import { BaseRegistry } from "./base-registry";
import { LensExtension } from "../lens-extension";
export interface PageMenuRegistration extends BaseRegistryItem {
id: BaseRegistryItemId; // required id from page-registry item to match with
url?: string; // when not provided initial extension's path used, e.g. "/extension/lens-extension-name"
export interface PageMenuTarget {
pageId: string;
extensionId?: string;
params?: object;
}
export interface PageMenuRegistration {
target?: PageMenuTarget;
title: React.ReactNode;
components: PageMenuComponents;
subMenus?: PageSubMenuRegistration[];
}
export interface PageSubMenuRegistration {
@@ -22,13 +28,18 @@ export interface PageMenuComponents {
}
export class PageMenuRegistry<T extends PageMenuRegistration> extends BaseRegistry<T> {
getItems() {
return super.getItems().map(item => {
item.url = item.extension.getPageUrl(item.url)
return item
});
@action
add(items: T[], ext?: LensExtension) {
const normalizedItems = items.map((menu) => {
if (menu.target && !menu.target.extensionId) {
menu.target.extensionId = ext.name
}
return menu
})
return super.add(normalizedItems);
}
}
export const globalPageMenuRegistry = new PageMenuRegistry<Omit<PageMenuRegistration, "subMenus">>();
export const clusterPageMenuRegistry = new PageMenuRegistry();
export const globalPageMenuRegistry = new PageMenuRegistry<PageMenuRegistration>();
export const clusterPageMenuRegistry = new PageMenuRegistry<PageMenuRegistration>();
+31 -10
View File
@@ -1,13 +1,17 @@
// Extensions-api -> Custom page registration
import React from "react";
import { BaseRegistry, BaseRegistryItem } from "./base-registry";
import { action } from "mobx";
import { compile } from "path-to-regexp";
import { BaseRegistry } from "./base-registry";
import { LensExtension } from "../lens-extension"
import type { PageMenuTarget } from "./page-menu-registry";
export interface PageRegistration extends BaseRegistryItem {
export interface PageRegistration {
id: string; // will be automatically prefixed with extension name
routePath?: string; // additional (suffix) route path to base extension's route: "/extension/:name"
exact?: boolean; // route matching flag, see: https://reactrouter.com/web/api/NavLink/exact-bool
components: PageComponents;
subPages?: SubPageRegistration[];
}
export interface SubPageRegistration {
@@ -20,14 +24,31 @@ export interface PageComponents {
Page: React.ComponentType<any>;
}
const routePrefix = "/extension/:name"
export function getPageUrl(ext: LensExtension, baseUrl = "") {
const validUrlName = ext.name.replace("@", "").replace("/", "-");
return compile(routePrefix)({ name: validUrlName }) + baseUrl;
}
export class PageRegistry<T extends PageRegistration> extends BaseRegistry<T> {
getItems() {
return super.getItems().map(item => {
item.routePath = item.extension.getPageRoute(item.routePath)
return item
});
@action
add(items: T[], ext?: LensExtension) {
const normalizedItems = items.map((i) => {
i.routePath = getPageUrl(ext, i.routePath)
return i
})
return super.add(normalizedItems);
}
getByPageMenuTarget(target: PageMenuTarget) {
if (!target) {
return null
}
return this.getItems().find((page) => page.routePath.startsWith(`/extension/${target.extensionId}/`) && page.id === target.pageId)
}
}
export const globalPageRegistry = new PageRegistry<Omit<PageRegistration, "subPages">>();
export const clusterPageRegistry = new PageRegistry();
export const globalPageRegistry = new PageRegistry<PageRegistration>();
export const clusterPageRegistry = new PageRegistry<PageRegistration>();
@@ -1,9 +1,9 @@
// Extensions API -> Status bar customizations
import React from "react";
import { BaseRegistry, BaseRegistryItem } from "./base-registry";
import { BaseRegistry } from "./base-registry";
export interface StatusBarRegistration extends BaseRegistryItem {
export interface StatusBarRegistration {
item?: React.ReactNode;
}