mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
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:
parent
eccc502ef2
commit
df0f080380
@ -100,6 +100,8 @@ import { ExamplePage } from "./src/example-page"
|
|||||||
export default class ExampleRendererExtension extends LensRendererExtension {
|
export default class ExampleRendererExtension extends LensRendererExtension {
|
||||||
globalPages = [
|
globalPages = [
|
||||||
{
|
{
|
||||||
|
id: "example",
|
||||||
|
routePath: "/example",
|
||||||
components: {
|
components: {
|
||||||
Page: ExamplePage,
|
Page: ExamplePage,
|
||||||
}
|
}
|
||||||
@ -109,6 +111,7 @@ export default class ExampleRendererExtension extends LensRendererExtension {
|
|||||||
globalPageMenus = [
|
globalPageMenus = [
|
||||||
{
|
{
|
||||||
title: "Example page", // used in icon's tooltip
|
title: "Example page", // used in icon's tooltip
|
||||||
|
target: { pageId: "example" }
|
||||||
components: {
|
components: {
|
||||||
Icon: () => <Component.Icon material="arrow"/>,
|
Icon: () => <Component.Icon material="arrow"/>,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,11 +5,21 @@ import React from "react"
|
|||||||
export default class ExampleExtension extends LensRendererExtension {
|
export default class ExampleExtension extends LensRendererExtension {
|
||||||
clusterPages = [
|
clusterPages = [
|
||||||
{
|
{
|
||||||
path: "/extension-example",
|
id: "example",
|
||||||
|
routePath: "/extension-example",
|
||||||
title: "Example Extension",
|
title: "Example Extension",
|
||||||
components: {
|
components: {
|
||||||
Page: () => <ExamplePage extension={this}/>,
|
Page: () => <ExamplePage extension={this}/>,
|
||||||
MenuIcon: ExampleIcon,
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
clusterPageMenus = [
|
||||||
|
{
|
||||||
|
target: { pageId: "example", params: {} },
|
||||||
|
title: "Example Extension",
|
||||||
|
components: {
|
||||||
|
Icon: ExampleIcon,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import { SupportPage } from "./src/support";
|
|||||||
export default class SupportPageRendererExtension extends LensRendererExtension {
|
export default class SupportPageRendererExtension extends LensRendererExtension {
|
||||||
globalPages: Interface.PageRegistration[] = [
|
globalPages: Interface.PageRegistration[] = [
|
||||||
{
|
{
|
||||||
|
id: "support",
|
||||||
|
routePath: "/support",
|
||||||
components: {
|
components: {
|
||||||
Page: SupportPage,
|
Page: SupportPage,
|
||||||
}
|
}
|
||||||
@ -14,7 +16,7 @@ export default class SupportPageRendererExtension extends LensRendererExtension
|
|||||||
statusBarItems: Interface.StatusBarRegistration[] = [
|
statusBarItems: Interface.StatusBarRegistration[] = [
|
||||||
{
|
{
|
||||||
item: (
|
item: (
|
||||||
<div className="SupportPageIcon flex align-center" onClick={() => this.navigate()}>
|
<div className="SupportPageIcon flex align-center" onClick={() => this.navigate("/support")}>
|
||||||
<Component.Icon interactive material="help" smallest/>
|
<Component.Icon interactive material="help" smallest/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
23
src/extensions/__tests__/lens-extension.test.ts
Normal file
23
src/extensions/__tests__/lens-extension.test.ts
Normal file
@ -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")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -57,29 +57,29 @@ export class ExtensionLoader {
|
|||||||
loadOnMain() {
|
loadOnMain() {
|
||||||
logger.info('[EXTENSIONS-LOADER]: load on main')
|
logger.info('[EXTENSIONS-LOADER]: load on main')
|
||||||
this.autoInitExtensions((ext: LensMainExtension) => [
|
this.autoInitExtensions((ext: LensMainExtension) => [
|
||||||
registries.menuRegistry.add(ext.appMenus, { key: ext })
|
registries.menuRegistry.add(ext.appMenus)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
loadOnClusterManagerRenderer() {
|
loadOnClusterManagerRenderer() {
|
||||||
logger.info('[EXTENSIONS-LOADER]: load on main renderer (cluster manager)')
|
logger.info('[EXTENSIONS-LOADER]: load on main renderer (cluster manager)')
|
||||||
this.autoInitExtensions((ext: LensRendererExtension) => [
|
this.autoInitExtensions((ext: LensRendererExtension) => [
|
||||||
registries.globalPageRegistry.add(ext.globalPages, { key: ext }),
|
registries.globalPageRegistry.add(ext.globalPages, ext),
|
||||||
registries.globalPageMenuRegistry.add(ext.globalPageMenus, { key: ext }),
|
registries.globalPageMenuRegistry.add(ext.globalPageMenus, ext),
|
||||||
registries.appPreferenceRegistry.add(ext.appPreferences, { key: ext }),
|
registries.appPreferenceRegistry.add(ext.appPreferences),
|
||||||
registries.clusterFeatureRegistry.add(ext.clusterFeatures, { key: ext }),
|
registries.clusterFeatureRegistry.add(ext.clusterFeatures),
|
||||||
registries.statusBarRegistry.add(ext.statusBarItems, { key: ext }),
|
registries.statusBarRegistry.add(ext.statusBarItems),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
loadOnClusterRenderer() {
|
loadOnClusterRenderer() {
|
||||||
logger.info('[EXTENSIONS-LOADER]: load on cluster renderer (dashboard)')
|
logger.info('[EXTENSIONS-LOADER]: load on cluster renderer (dashboard)')
|
||||||
this.autoInitExtensions((ext: LensRendererExtension) => [
|
this.autoInitExtensions((ext: LensRendererExtension) => [
|
||||||
registries.clusterPageRegistry.add(ext.clusterPages, { key: ext }),
|
registries.clusterPageRegistry.add(ext.clusterPages, ext),
|
||||||
registries.clusterPageMenuRegistry.add(ext.clusterPageMenus, { key: ext }),
|
registries.clusterPageMenuRegistry.add(ext.clusterPageMenus, ext),
|
||||||
registries.kubeObjectMenuRegistry.add(ext.kubeObjectMenuItems, { key: ext }),
|
registries.kubeObjectMenuRegistry.add(ext.kubeObjectMenuItems),
|
||||||
registries.kubeObjectDetailRegistry.add(ext.kubeObjectDetailItems, { key: ext }),
|
registries.kubeObjectDetailRegistry.add(ext.kubeObjectDetailItems),
|
||||||
registries.kubeObjectStatusRegistry.add(ext.kubeObjectStatusTexts, { key: ext })
|
registries.kubeObjectStatusRegistry.add(ext.kubeObjectStatusTexts)
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import type { InstalledExtension } from "./extension-manager";
|
import type { InstalledExtension } from "./extension-manager";
|
||||||
import { action, observable, reaction } from "mobx";
|
import { action, observable, reaction } from "mobx";
|
||||||
import { compile } from "path-to-regexp"
|
|
||||||
import logger from "../main/logger";
|
import logger from "../main/logger";
|
||||||
|
|
||||||
export type LensExtensionId = string; // path to manifest (package.json)
|
export type LensExtensionId = string; // path to manifest (package.json)
|
||||||
@ -15,7 +14,6 @@ export interface LensExtensionManifest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class LensExtension {
|
export class LensExtension {
|
||||||
readonly routePrefix = "/extension/:name"
|
|
||||||
readonly manifest: LensExtensionManifest;
|
readonly manifest: LensExtensionManifest;
|
||||||
readonly manifestPath: string;
|
readonly manifestPath: string;
|
||||||
readonly isBundled: boolean;
|
readonly isBundled: boolean;
|
||||||
@ -44,14 +42,6 @@ export class LensExtension {
|
|||||||
return this.manifest.description
|
return this.manifest.description
|
||||||
}
|
}
|
||||||
|
|
||||||
getPageUrl(baseUrl = "") {
|
|
||||||
return compile(this.routePrefix)({ name: this.name }) + baseUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
getPageRoute(baseRoute = "") {
|
|
||||||
return this.routePrefix + baseRoute;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async enable() {
|
async enable() {
|
||||||
if (this.isEnabled) return;
|
if (this.isEnabled) return;
|
||||||
|
|||||||
@ -2,13 +2,14 @@ import type { MenuRegistration } from "./registries/menu-registry";
|
|||||||
import { observable } from "mobx";
|
import { observable } from "mobx";
|
||||||
import { LensExtension } from "./lens-extension"
|
import { LensExtension } from "./lens-extension"
|
||||||
import { WindowManager } from "../main/window-manager";
|
import { WindowManager } from "../main/window-manager";
|
||||||
|
import { getPageUrl } from "./registries/page-registry"
|
||||||
|
|
||||||
export class LensMainExtension extends LensExtension {
|
export class LensMainExtension extends LensExtension {
|
||||||
@observable.shallow appMenus: MenuRegistration[] = []
|
@observable.shallow appMenus: MenuRegistration[] = []
|
||||||
|
|
||||||
async navigate(location?: string, frameId?: number) {
|
async navigate(location?: string, frameId?: number) {
|
||||||
const windowManager = WindowManager.getInstance<WindowManager>();
|
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);
|
await windowManager.navigate(url, frameId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import type { AppPreferenceRegistration, ClusterFeatureRegistration, KubeObjectDetailRegistration, KubeObjectMenuRegistration, KubeObjectStatusRegistration, PageMenuRegistration, PageRegistration, StatusBarRegistration, } from "./registries"
|
import type { AppPreferenceRegistration, ClusterFeatureRegistration, KubeObjectDetailRegistration, KubeObjectMenuRegistration, KubeObjectStatusRegistration, PageMenuRegistration, PageRegistration, StatusBarRegistration, } from "./registries"
|
||||||
import { observable } from "mobx";
|
import { observable } from "mobx";
|
||||||
import { LensExtension } from "./lens-extension"
|
import { LensExtension } from "./lens-extension"
|
||||||
|
import { getPageUrl } from "./registries/page-registry"
|
||||||
|
|
||||||
export class LensRendererExtension extends LensExtension {
|
export class LensRendererExtension extends LensExtension {
|
||||||
@observable.shallow globalPages: PageRegistration[] = []
|
@observable.shallow globalPages: PageRegistration[] = []
|
||||||
@ -16,6 +17,6 @@ export class LensRendererExtension extends LensExtension {
|
|||||||
|
|
||||||
async navigate(location?: string) {
|
async navigate(location?: string) {
|
||||||
const { navigate } = await import("../renderer/navigation");
|
const { navigate } = await import("../renderer/navigation");
|
||||||
navigate(this.getPageUrl(location));
|
navigate(getPageUrl(this, location));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
31
src/extensions/registries/__tests__/page-registry.test.ts
Normal file
31
src/extensions/registries/__tests__/page-registry.test.ts
Normal file
@ -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 type React from "react"
|
||||||
import { BaseRegistry, BaseRegistryItem } from "./base-registry";
|
import { BaseRegistry } from "./base-registry";
|
||||||
|
|
||||||
export interface AppPreferenceComponents {
|
export interface AppPreferenceComponents {
|
||||||
Hint: React.ComponentType<any>;
|
Hint: React.ComponentType<any>;
|
||||||
Input: React.ComponentType<any>;
|
Input: React.ComponentType<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppPreferenceRegistration extends BaseRegistryItem {
|
export interface AppPreferenceRegistration {
|
||||||
title: string;
|
title: string;
|
||||||
components: AppPreferenceComponents;
|
components: AppPreferenceComponents;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,65 +1,24 @@
|
|||||||
// Base class for extensions-api registries
|
// Base class for extensions-api registries
|
||||||
import { action, observable } from "mobx";
|
import { action, observable } from "mobx";
|
||||||
import { LensExtension } from "../lens-extension";
|
|
||||||
import { getRandId } from "../../common/utils";
|
|
||||||
|
|
||||||
export type BaseRegistryKey = LensExtension | null;
|
export class BaseRegistry<T = any> {
|
||||||
export type BaseRegistryItemId = string | symbol;
|
private items = observable<T>([], { deep: false });
|
||||||
|
|
||||||
export interface BaseRegistryItem {
|
getItems(): T[] {
|
||||||
id?: BaseRegistryItemId; // uniq id, generated automatically when not provided
|
return this.items.toJS();
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
add(items: T | T[], { key = null, merge = true }: BaseRegistryAddMeta = {}) {
|
add(items: T | T[]) {
|
||||||
const normalizedItems = (Array.isArray(items) ? items : [items]).map((item: T) => {
|
const normalizedItems = (Array.isArray(items) ? items : [items])
|
||||||
item.id = item.id || getRandId();
|
this.items.push(...normalizedItems);
|
||||||
return item;
|
return () => this.remove(...normalizedItems);
|
||||||
});
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
remove(items: T[], key: BaseRegistryKey = null) {
|
remove(...items: T[]) {
|
||||||
const storedItems = this.items.get(key);
|
items.forEach(item => {
|
||||||
if (!storedItems) return;
|
this.items.remove(item); // works because of {deep: false};
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import type React from "react"
|
import type React from "react"
|
||||||
import { BaseRegistry, BaseRegistryItem } from "./base-registry";
|
import { BaseRegistry } from "./base-registry";
|
||||||
import { ClusterFeature } from "../cluster-feature";
|
import { ClusterFeature } from "../cluster-feature";
|
||||||
|
|
||||||
export interface ClusterFeatureComponents {
|
export interface ClusterFeatureComponents {
|
||||||
Description: React.ComponentType<any>;
|
Description: React.ComponentType<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClusterFeatureRegistration extends BaseRegistryItem {
|
export interface ClusterFeatureRegistration {
|
||||||
title: string;
|
title: string;
|
||||||
components: ClusterFeatureComponents
|
components: ClusterFeatureComponents
|
||||||
feature: ClusterFeature
|
feature: ClusterFeature
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import { BaseRegistry, BaseRegistryItem } from "./base-registry";
|
import { BaseRegistry } from "./base-registry";
|
||||||
|
|
||||||
export interface KubeObjectDetailComponents {
|
export interface KubeObjectDetailComponents {
|
||||||
Details: React.ComponentType<any>;
|
Details: React.ComponentType<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KubeObjectDetailRegistration extends BaseRegistryItem {
|
export interface KubeObjectDetailRegistration {
|
||||||
kind: string;
|
kind: string;
|
||||||
apiVersions: string[];
|
apiVersions: string[];
|
||||||
components: KubeObjectDetailComponents;
|
components: KubeObjectDetailComponents;
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import { BaseRegistry, BaseRegistryItem } from "./base-registry";
|
import { BaseRegistry } from "./base-registry";
|
||||||
|
|
||||||
export interface KubeObjectMenuComponents {
|
export interface KubeObjectMenuComponents {
|
||||||
MenuItem: React.ComponentType<any>;
|
MenuItem: React.ComponentType<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KubeObjectMenuRegistration extends BaseRegistryItem {
|
export interface KubeObjectMenuRegistration {
|
||||||
kind: string;
|
kind: string;
|
||||||
apiVersions: string[];
|
apiVersions: string[];
|
||||||
components: KubeObjectMenuComponents;
|
components: KubeObjectMenuComponents;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { KubeObject, KubeObjectStatus } from "../renderer-api/k8s-api";
|
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;
|
kind: string;
|
||||||
apiVersions: string[];
|
apiVersions: string[];
|
||||||
resolve: (object: KubeObject) => KubeObjectStatus;
|
resolve: (object: KubeObject) => KubeObjectStatus;
|
||||||
|
|||||||
@ -1,15 +1,21 @@
|
|||||||
// Extensions-api -> Register page menu items
|
// Extensions-api -> Register page menu items
|
||||||
|
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
|
import { action } from "mobx";
|
||||||
import type { IconProps } from "../../renderer/components/icon";
|
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 {
|
export interface PageMenuTarget {
|
||||||
id: BaseRegistryItemId; // required id from page-registry item to match with
|
pageId: string;
|
||||||
url?: string; // when not provided initial extension's path used, e.g. "/extension/lens-extension-name"
|
extensionId?: string;
|
||||||
|
params?: object;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PageMenuRegistration {
|
||||||
|
target?: PageMenuTarget;
|
||||||
title: React.ReactNode;
|
title: React.ReactNode;
|
||||||
components: PageMenuComponents;
|
components: PageMenuComponents;
|
||||||
subMenus?: PageSubMenuRegistration[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PageSubMenuRegistration {
|
export interface PageSubMenuRegistration {
|
||||||
@ -22,13 +28,18 @@ export interface PageMenuComponents {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class PageMenuRegistry<T extends PageMenuRegistration> extends BaseRegistry<T> {
|
export class PageMenuRegistry<T extends PageMenuRegistration> extends BaseRegistry<T> {
|
||||||
getItems() {
|
|
||||||
return super.getItems().map(item => {
|
@action
|
||||||
item.url = item.extension.getPageUrl(item.url)
|
add(items: T[], ext?: LensExtension) {
|
||||||
return item
|
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 globalPageMenuRegistry = new PageMenuRegistry<PageMenuRegistration>();
|
||||||
export const clusterPageMenuRegistry = new PageMenuRegistry();
|
export const clusterPageMenuRegistry = new PageMenuRegistry<PageMenuRegistration>();
|
||||||
|
|||||||
@ -1,13 +1,17 @@
|
|||||||
// Extensions-api -> Custom page registration
|
// Extensions-api -> Custom page registration
|
||||||
|
|
||||||
import React from "react";
|
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"
|
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
|
exact?: boolean; // route matching flag, see: https://reactrouter.com/web/api/NavLink/exact-bool
|
||||||
components: PageComponents;
|
components: PageComponents;
|
||||||
subPages?: SubPageRegistration[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SubPageRegistration {
|
export interface SubPageRegistration {
|
||||||
@ -20,14 +24,31 @@ export interface PageComponents {
|
|||||||
Page: React.ComponentType<any>;
|
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> {
|
export class PageRegistry<T extends PageRegistration> extends BaseRegistry<T> {
|
||||||
getItems() {
|
|
||||||
return super.getItems().map(item => {
|
@action
|
||||||
item.routePath = item.extension.getPageRoute(item.routePath)
|
add(items: T[], ext?: LensExtension) {
|
||||||
return item
|
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 globalPageRegistry = new PageRegistry<PageRegistration>();
|
||||||
export const clusterPageRegistry = new PageRegistry();
|
export const clusterPageRegistry = new PageRegistry<PageRegistration>();
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
// Extensions API -> Status bar customizations
|
// Extensions API -> Status bar customizations
|
||||||
|
|
||||||
import React from "react";
|
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;
|
item?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -74,27 +74,8 @@ export class App extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderExtensionRoutes() {
|
renderExtensionRoutes() {
|
||||||
return clusterPageRegistry.getItems().map(({ id: pageId, components: { Page }, exact, routePath, subPages }) => {
|
return clusterPageRegistry.getItems().map(({ components: { Page }, exact, routePath }) => {
|
||||||
const Component = () => {
|
const Component = () => {
|
||||||
if (subPages) {
|
|
||||||
const tabs: TabLayoutRoute[] = subPages.map(({ exact, routePath, components: { Page } }) => {
|
|
||||||
const menuItem = clusterPageMenuRegistry.getById(pageId);
|
|
||||||
if (!menuItem) return;
|
|
||||||
return {
|
|
||||||
routePath, exact,
|
|
||||||
component: Page,
|
|
||||||
url: menuItem.url,
|
|
||||||
title: menuItem.title,
|
|
||||||
}
|
|
||||||
}).filter(Boolean);
|
|
||||||
if (tabs.length > 0) {
|
|
||||||
return (
|
|
||||||
<Page>
|
|
||||||
<TabLayout tabs={tabs}/>
|
|
||||||
</Page>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return <Page/>
|
return <Page/>
|
||||||
};
|
};
|
||||||
return <Route key={routePath} path={routePath} exact={exact} component={Component}/>
|
return <Route key={routePath} path={routePath} exact={exact} component={Component}/>
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import { remote } from "electron"
|
|||||||
import type { Cluster } from "../../../main/cluster";
|
import type { Cluster } from "../../../main/cluster";
|
||||||
import { DragDropContext, Draggable, DraggableProvided, Droppable, DroppableProvided, DropResult } from "react-beautiful-dnd";
|
import { DragDropContext, Draggable, DraggableProvided, Droppable, DroppableProvided, DropResult } from "react-beautiful-dnd";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { matchPath } from "react-router";
|
|
||||||
import { _i18n } from "../../i18n";
|
import { _i18n } from "../../i18n";
|
||||||
import { t, Trans } from "@lingui/macro";
|
import { t, Trans } from "@lingui/macro";
|
||||||
import { userStore } from "../../../common/user-store";
|
import { userStore } from "../../../common/user-store";
|
||||||
@ -15,7 +14,7 @@ import { ClusterIcon } from "../cluster-icon";
|
|||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { autobind, cssNames, IClassName } from "../../utils";
|
import { autobind, cssNames, IClassName } from "../../utils";
|
||||||
import { Badge } from "../badge";
|
import { Badge } from "../badge";
|
||||||
import { navigate, navigation } from "../../navigation";
|
import { isActiveRoute, navigate } from "../../navigation";
|
||||||
import { addClusterURL } from "../+add-cluster";
|
import { addClusterURL } from "../+add-cluster";
|
||||||
import { clusterSettingsURL } from "../+cluster-settings";
|
import { clusterSettingsURL } from "../+cluster-settings";
|
||||||
import { landingURL } from "../+landing-page";
|
import { landingURL } from "../+landing-page";
|
||||||
@ -24,6 +23,7 @@ import { ConfirmDialog } from "../confirm-dialog";
|
|||||||
import { clusterIpc } from "../../../common/cluster-ipc";
|
import { clusterIpc } from "../../../common/cluster-ipc";
|
||||||
import { clusterViewURL } from "./cluster-view.route";
|
import { clusterViewURL } from "./cluster-view.route";
|
||||||
import { globalPageMenuRegistry, globalPageRegistry } from "../../../extensions/registries";
|
import { globalPageMenuRegistry, globalPageRegistry } from "../../../extensions/registries";
|
||||||
|
import { compile } from "path-to-regexp";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: IClassName;
|
className?: IClassName;
|
||||||
@ -149,17 +149,16 @@ export class ClustersMenu extends React.Component<Props> {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="extensions">
|
<div className="extensions">
|
||||||
{globalPageMenuRegistry.getItems().map(({ id: menuItemId, title, url, components: { Icon } }) => {
|
{globalPageMenuRegistry.getItems().map(({ title, target, components: { Icon } }) => {
|
||||||
const registeredPage = globalPageRegistry.getById(menuItemId);
|
const registeredPage = globalPageRegistry.getByPageMenuTarget(target);
|
||||||
if (!registeredPage) return;
|
if (!registeredPage) return;
|
||||||
const { routePath, exact } = registeredPage;
|
const { routePath, exact } = registeredPage;
|
||||||
const isActive = !!matchPath(navigation.location.pathname, { path: routePath, exact });
|
|
||||||
return (
|
return (
|
||||||
<Icon
|
<Icon
|
||||||
key={routePath}
|
key={routePath}
|
||||||
tooltip={title}
|
tooltip={title}
|
||||||
active={isActive}
|
active={isActiveRoute({ path: routePath, exact })}
|
||||||
onClick={() => navigate(url)}
|
onClick={() => navigate(compile(routePath)(target.params))}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import { isActiveRoute } from "../../navigation";
|
|||||||
import { isAllowedResource } from "../../../common/rbac"
|
import { isAllowedResource } from "../../../common/rbac"
|
||||||
import { Spinner } from "../spinner";
|
import { Spinner } from "../spinner";
|
||||||
import { clusterPageMenuRegistry, clusterPageRegistry } from "../../../extensions/registries";
|
import { clusterPageMenuRegistry, clusterPageRegistry } from "../../../extensions/registries";
|
||||||
|
import { compile } from "path-to-regexp";
|
||||||
|
|
||||||
const SidebarContext = React.createContext<SidebarContextValue>({ pinned: false });
|
const SidebarContext = React.createContext<SidebarContextValue>({ pinned: false });
|
||||||
type SidebarContextValue = {
|
type SidebarContextValue = {
|
||||||
@ -191,10 +192,11 @@ export class Sidebar extends React.Component<Props> {
|
|||||||
>
|
>
|
||||||
{this.renderCustomResources()}
|
{this.renderCustomResources()}
|
||||||
</SidebarNavItem>
|
</SidebarNavItem>
|
||||||
{clusterPageMenuRegistry.getItems().map(({ id: menuItemId, title, url, components: { Icon } }) => {
|
{clusterPageMenuRegistry.getItems().map(({ title, target, components: { Icon } }) => {
|
||||||
const registeredPage = clusterPageRegistry.getById(menuItemId);
|
const registeredPage = clusterPageRegistry.getByPageMenuTarget(target);
|
||||||
if (!registeredPage) return;
|
if (!registeredPage) return;
|
||||||
const { routePath, exact } = registeredPage;
|
const { routePath, exact } = registeredPage;
|
||||||
|
const url = compile(routePath)(target.params)
|
||||||
return (
|
return (
|
||||||
<SidebarNavItem
|
<SidebarNavItem
|
||||||
key={url}
|
key={url}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user