Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 13 commits into from
Nov 13, 2020
23 changes: 23 additions & 0 deletions src/extensions/__tests__/lens-extension.test.ts
Original file line number Diff line number Diff line change
@@ -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")
})
})
})
22 changes: 11 additions & 11 deletions src/extensions/extension-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
])
}

Expand Down
9 changes: 0 additions & 9 deletions src/extensions/lens-extension.ts
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -44,14 +43,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;
Expand Down
31 changes: 31 additions & 0 deletions src/extensions/registries/__tests__/page-registry.test.ts
Original file line number Diff line number Diff line change
@@ -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")
})
})
4 changes: 2 additions & 2 deletions src/extensions/registries/app-preference-registry.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Expand Down
65 changes: 12 additions & 53 deletions src/extensions/registries/base-registry.ts
Original file line number Diff line number Diff line change
@@ -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> {
protected 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};
})
}
}
4 changes: 2 additions & 2 deletions src/extensions/registries/cluster-feature-registry.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/extensions/registries/kube-object-detail-registry.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
4 changes: 2 additions & 2 deletions src/extensions/registries/kube-object-menu-registry.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
4 changes: 2 additions & 2 deletions src/extensions/registries/kube-object-status-registry.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
24 changes: 16 additions & 8 deletions src/extensions/registries/page-menu-registry.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
// 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";
import { getPageUrl } from "./page-registry";

export interface PageMenuRegistration extends BaseRegistryItem {
id: BaseRegistryItemId; // required id from page-registry item to match with
export interface PageMenuRegistration {
url?: string; // when not provided initial extension's path used, e.g. "/extension/lens-extension-name"
title: React.ReactNode;
components: PageMenuComponents;
Expand All @@ -22,11 +24,17 @@ 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((i) => {
i.url = getPageUrl(ext, i.url)
return i
})
return super.add(normalizedItems);
}

getByRoutePath(routePath: string) {
return this.getItems().find((i) => i.url === routePath)
}
}

Expand Down
31 changes: 24 additions & 7 deletions src/extensions/registries/page-registry.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
// 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"

export interface PageRegistration extends BaseRegistryItem {
export interface PageRegistration {
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;
Expand All @@ -20,12 +23,26 @@ export interface PageComponents {
Page: React.ComponentType<any>;
}

const routePrefix = "/extension/:name"

export function getPageUrl(ext: LensExtension, baseUrl = "") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export function getPageUrl(ext: LensExtension, baseUrl = "") {
export function getPageRoute(ext: LensExtension, routePathOrUrl = "") {

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);
}

getByUrl(url: string) {
return this.getItems().find((i) => i.routePath === url)
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/extensions/registries/status-bar-registry.ts
Original file line number Diff line number Diff line change
@@ -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;
}

Expand Down
4 changes: 2 additions & 2 deletions src/renderer/components/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,11 @@ export class App extends React.Component {
}

renderExtensionRoutes() {
return clusterPageRegistry.getItems().map(({ id: pageId, components: { Page }, exact, routePath, subPages }) => {
return clusterPageRegistry.getItems().map(({ components: { Page }, exact, routePath, subPages }) => {
const Component = () => {
if (subPages) {
const tabs: TabLayoutRoute[] = subPages.map(({ exact, routePath, components: { Page } }) => {
const menuItem = clusterPageMenuRegistry.getById(pageId);
const menuItem = clusterPageMenuRegistry.getByRoutePath(routePath);
if (!menuItem) return;
return {
routePath, exact,
Expand Down
Loading