mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
make Pages and PageMenus observable and reactive
Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
e718b250cc
commit
1e2069466a
@ -1,6 +1,6 @@
|
||||
// Lens-extensions api developer's kit
|
||||
export * from "../lens-main-extension";
|
||||
export * from "../lens-renderer-extension";
|
||||
export { LensMainExtension } from "../lens-main-extension";
|
||||
export { LensRendererExtension } from "../lens-renderer-extension";
|
||||
|
||||
// APIs
|
||||
import * as App from "./app";
|
||||
|
||||
@ -14,7 +14,6 @@ import type { LensRendererExtension } from "./lens-renderer-extension";
|
||||
import * as registries from "./registries";
|
||||
import fs from "fs";
|
||||
|
||||
|
||||
export function extensionPackagesRoot() {
|
||||
return path.join((app || remote.app).getPath("userData"));
|
||||
}
|
||||
@ -66,10 +65,22 @@ export class ExtensionLoader {
|
||||
return extensions;
|
||||
}
|
||||
|
||||
@computed get allEnabledInstances(): LensExtension[] {
|
||||
const res: LensExtension[] = [];
|
||||
|
||||
for (const [extId, ext] of this.instances) {
|
||||
if (this.extensions.get(extId).isEnabled) {
|
||||
res.push(ext);
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
getExtensionByName(name: string): LensExtension | null {
|
||||
for (const [, val] of this.instances) {
|
||||
if (val.name === name) {
|
||||
return val;
|
||||
for (const [extId, ext] of this.instances) {
|
||||
if (ext.name === name && this.extensions.get(extId).isEnabled) {
|
||||
return ext;
|
||||
}
|
||||
}
|
||||
|
||||
@ -210,8 +221,6 @@ export class ExtensionLoader {
|
||||
logger.debug(`${logModule}: load on main renderer (cluster manager)`);
|
||||
this.autoInitExtensions(async (extension: LensRendererExtension) => {
|
||||
const removeItems = [
|
||||
registries.globalPageRegistry.add(extension.globalPages, extension),
|
||||
registries.globalPageMenuRegistry.add(extension.globalPageMenus, extension),
|
||||
registries.appPreferenceRegistry.add(extension.appPreferences),
|
||||
registries.clusterFeatureRegistry.add(extension.clusterFeatures),
|
||||
registries.statusBarRegistry.add(extension.statusBarItems),
|
||||
@ -240,8 +249,6 @@ export class ExtensionLoader {
|
||||
}
|
||||
|
||||
const removeItems = [
|
||||
registries.clusterPageRegistry.add(extension.clusterPages, extension),
|
||||
registries.clusterPageMenuRegistry.add(extension.clusterPageMenus, extension),
|
||||
registries.kubeObjectMenuRegistry.add(extension.kubeObjectMenuItems),
|
||||
registries.kubeObjectDetailRegistry.add(extension.kubeObjectDetailItems),
|
||||
registries.kubeObjectStatusRegistry.add(extension.kubeObjectStatusTexts),
|
||||
|
||||
@ -9,7 +9,7 @@ export class LensMainExtension extends LensExtension {
|
||||
async navigate<P extends object>(pageId?: string, params?: P, frameId?: number) {
|
||||
const windowManager = WindowManager.getInstance<WindowManager>();
|
||||
const pageUrl = getExtensionPageUrl({
|
||||
extensionId: this.name,
|
||||
extensionName: this.name,
|
||||
pageId,
|
||||
params: params ?? {}, // compile to url with params
|
||||
});
|
||||
|
||||
@ -1,14 +1,56 @@
|
||||
import type { AppPreferenceRegistration, ClusterFeatureRegistration, ClusterPageMenuRegistration, KubeObjectDetailRegistration, KubeObjectMenuRegistration, KubeObjectStatusRegistration, PageMenuRegistration, PageRegistration, StatusBarRegistration, } from "./registries";
|
||||
import { AppPreferenceRegistration, ClusterFeatureRegistration, ClusterPageMenuRegistration, KubeObjectDetailRegistration, KubeObjectMenuRegistration, KubeObjectStatusRegistration, PageMenuRegistration, PageRegistration, getRegisteredPage, Registrable, StatusBarRegistration, recitfyRegisterable, getRegisteredPageMenu, } from "./registries";
|
||||
import type { Cluster } from "../main/cluster";
|
||||
import { LensExtension } from "./lens-extension";
|
||||
import { getExtensionPageUrl } from "./registries/page-registry";
|
||||
import { CommandRegistration } from "./registries/command-registry";
|
||||
import { computed, observable } from "mobx";
|
||||
import { getHostedCluster } from "../common/cluster-store";
|
||||
|
||||
export const registeredClusterPages = Symbol("registeredClusterPages");
|
||||
export const registeredGlobalPages = Symbol("registeredGlobalPages");
|
||||
export const registeredGlobalPageMenus = Symbol("registeredGlobalPageMenus");
|
||||
export const registeredClusterPageMenus = Symbol("registeredClusterPageMenus");
|
||||
|
||||
export class LensRendererExtension extends LensExtension {
|
||||
globalPages: PageRegistration[] = [];
|
||||
clusterPages: PageRegistration[] = [];
|
||||
globalPageMenus: PageMenuRegistration[] = [];
|
||||
clusterPageMenus: ClusterPageMenuRegistration[] = [];
|
||||
#privateGetters = {
|
||||
[registeredGlobalPages]: computed(() => (
|
||||
recitfyRegisterable(this.globalPages)
|
||||
.map(page => getRegisteredPage(page, this.name))
|
||||
)),
|
||||
[registeredClusterPages]: computed(() => (
|
||||
recitfyRegisterable(this.clusterPages, getHostedCluster)
|
||||
.map(page => getRegisteredPage(page, this.name))
|
||||
)),
|
||||
[registeredGlobalPageMenus]: computed(() => (
|
||||
recitfyRegisterable(this.globalPageMenus)
|
||||
.map(pageMenu => getRegisteredPageMenu(pageMenu, this.name))
|
||||
)),
|
||||
[registeredClusterPageMenus]: computed(() => (
|
||||
recitfyRegisterable(this.clusterPageMenus, getHostedCluster)
|
||||
.map(pageMenu => getRegisteredPageMenu(pageMenu, this.name))
|
||||
)),
|
||||
};
|
||||
|
||||
@observable globalPages: Registrable<PageRegistration> = [];
|
||||
get [registeredGlobalPages]() {
|
||||
return this.#privateGetters[registeredGlobalPages].get();
|
||||
}
|
||||
|
||||
@observable clusterPages: Registrable<PageRegistration> = [];
|
||||
get [registeredClusterPages]() {
|
||||
return this.#privateGetters[registeredClusterPages].get();
|
||||
}
|
||||
|
||||
@observable globalPageMenus: Registrable<PageMenuRegistration> = [];
|
||||
get [registeredGlobalPageMenus]() {
|
||||
return this.#privateGetters[registeredGlobalPageMenus].get();
|
||||
}
|
||||
|
||||
@observable clusterPageMenus: Registrable<ClusterPageMenuRegistration> = [];
|
||||
get [registeredClusterPageMenus]() {
|
||||
return this.#privateGetters[registeredClusterPageMenus].get();
|
||||
}
|
||||
|
||||
kubeObjectStatusTexts: KubeObjectStatusRegistration[] = [];
|
||||
appPreferences: AppPreferenceRegistration[] = [];
|
||||
clusterFeatures: ClusterFeatureRegistration[] = [];
|
||||
@ -20,7 +62,7 @@ export class LensRendererExtension extends LensExtension {
|
||||
async navigate<P extends object>(pageId?: string, params?: P) {
|
||||
const { navigate } = await import("../renderer/navigation");
|
||||
const pageUrl = getExtensionPageUrl({
|
||||
extensionId: this.name,
|
||||
extensionName: this.name,
|
||||
pageId,
|
||||
params: params ?? {}, // compile to url with params
|
||||
});
|
||||
|
||||
@ -1,126 +1,134 @@
|
||||
import { getExtensionPageUrl, globalPageRegistry, PageParams } from "../page-registry";
|
||||
import { LensExtension } from "../../lens-extension";
|
||||
import { getExtensionPageUrl, PageParams } from "../page-registry";
|
||||
import React from "react";
|
||||
import { LensRendererExtension } from "../../core-api";
|
||||
import { findRegisteredPage, PageRegistration } from "..";
|
||||
import { extensionLoader } from "../../extension-loader";
|
||||
|
||||
let ext: LensExtension = null;
|
||||
jest.mock("../../extension-loader");
|
||||
|
||||
describe("getPageUrl", () => {
|
||||
const extensionName = "foo-bar";
|
||||
|
||||
beforeEach(async () => {
|
||||
ext = new LensExtension({
|
||||
manifest: {
|
||||
name: "foo-bar",
|
||||
version: "0.1.1"
|
||||
},
|
||||
id: "/this/is/fake/package.json",
|
||||
absolutePath: "/absolute/fake/",
|
||||
manifestPath: "/this/is/fake/package.json",
|
||||
isBundled: false,
|
||||
isEnabled: true
|
||||
});
|
||||
globalPageRegistry.add({
|
||||
id: "page-with-params",
|
||||
components: {
|
||||
Page: () => React.createElement("Page with params")
|
||||
},
|
||||
params: {
|
||||
test1: "test1-default",
|
||||
test2: "" // no default value, just declaration
|
||||
},
|
||||
}, ext);
|
||||
jest.spyOn(extensionLoader, "getExtensionByName")
|
||||
.mockImplementation(name => {
|
||||
if (name !== extensionName) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const ext = new LensRendererExtension({
|
||||
manifest: {
|
||||
name: extensionName,
|
||||
version: "0.1.1"
|
||||
},
|
||||
id: "/this/is/fake/package.json",
|
||||
absolutePath: "/absolute/fake/",
|
||||
manifestPath: "/this/is/fake/package.json",
|
||||
isBundled: false,
|
||||
isEnabled: true
|
||||
});
|
||||
|
||||
(ext.globalPages as PageRegistration[]).push({
|
||||
id: "page-with-params",
|
||||
components: {
|
||||
Page: () => React.createElement("Page with params")
|
||||
},
|
||||
params: {
|
||||
test1: "test1-default",
|
||||
test2: "" // no default value, just declaration
|
||||
},
|
||||
});
|
||||
|
||||
return ext;
|
||||
});
|
||||
});
|
||||
|
||||
it("returns a page url for extension", () => {
|
||||
expect(getExtensionPageUrl({ extensionId: ext.name })).toBe("/extension/foo-bar");
|
||||
expect(getExtensionPageUrl({ extensionName })).toBe("/extension/foo-bar");
|
||||
});
|
||||
|
||||
it("allows to pass base url as parameter", () => {
|
||||
expect(getExtensionPageUrl({ extensionId: ext.name, pageId: "/test" })).toBe("/extension/foo-bar/test");
|
||||
expect(getExtensionPageUrl({ extensionName, pageId: "/test" })).toBe("/extension/foo-bar/test");
|
||||
});
|
||||
|
||||
it("removes @ and replace `/` to `--`", () => {
|
||||
expect(getExtensionPageUrl({ extensionId: "@foo/bar" })).toBe("/extension/foo--bar");
|
||||
expect(getExtensionPageUrl({ extensionName: "@foo/bar" })).toBe("/extension/foo--bar");
|
||||
});
|
||||
|
||||
it("adds / prefix", () => {
|
||||
expect(getExtensionPageUrl({ extensionId: ext.name, pageId: "test" })).toBe("/extension/foo-bar/test");
|
||||
expect(getExtensionPageUrl({ extensionName, pageId: "test" })).toBe("/extension/foo-bar/test");
|
||||
});
|
||||
|
||||
it("normalize possible multi-slashes in page.id", () => {
|
||||
expect(getExtensionPageUrl({ extensionId: ext.name, pageId: "//test/" })).toBe("/extension/foo-bar/test");
|
||||
expect(getExtensionPageUrl({ extensionName, pageId: "//test/" })).toBe("/extension/foo-bar/test");
|
||||
});
|
||||
|
||||
it("gets page url with custom params", () => {
|
||||
const params: PageParams<string> = { test1: "one", test2: "2" };
|
||||
const searchParams = new URLSearchParams(params);
|
||||
const pageUrl = getExtensionPageUrl({ extensionId: ext.name, pageId: "page-with-params", params });
|
||||
const pageUrl = getExtensionPageUrl({ extensionName, pageId: "page-with-params", params });
|
||||
|
||||
expect(pageUrl).toBe(`/extension/foo-bar/page-with-params?${searchParams}`);
|
||||
});
|
||||
|
||||
it("gets page url with default custom params", () => {
|
||||
const defaultPageUrl = getExtensionPageUrl({ extensionId: ext.name, pageId: "page-with-params", });
|
||||
const defaultPageUrl = getExtensionPageUrl({ extensionName, pageId: "page-with-params", });
|
||||
|
||||
expect(defaultPageUrl).toBe(`/extension/foo-bar/page-with-params?test1=test1-default`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("globalPageRegistry", () => {
|
||||
beforeEach(async () => {
|
||||
ext = new LensExtension({
|
||||
manifest: {
|
||||
name: "@acme/foo-bar",
|
||||
version: "0.1.1"
|
||||
},
|
||||
id: "/this/is/fake/package.json",
|
||||
absolutePath: "/absolute/fake/",
|
||||
manifestPath: "/this/is/fake/package.json",
|
||||
isBundled: false,
|
||||
isEnabled: true
|
||||
});
|
||||
globalPageRegistry.add([
|
||||
{
|
||||
id: "test-page",
|
||||
components: {
|
||||
Page: () => React.createElement("Text")
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "another-page",
|
||||
components: {
|
||||
Page: () => React.createElement("Text")
|
||||
},
|
||||
},
|
||||
{
|
||||
components: {
|
||||
Page: () => React.createElement("Default")
|
||||
}
|
||||
},
|
||||
], ext);
|
||||
const extensionName = "@acme/foo-bar";
|
||||
const ext = new LensRendererExtension({
|
||||
manifest: {
|
||||
name: extensionName,
|
||||
version: "0.1.1"
|
||||
},
|
||||
id: "/this/is/fake/package.json",
|
||||
absolutePath: "/absolute/fake/",
|
||||
manifestPath: "/this/is/fake/package.json",
|
||||
isBundled: false,
|
||||
isEnabled: true
|
||||
});
|
||||
|
||||
describe("getByPageTarget", () => {
|
||||
(ext.globalPages as PageRegistration[]).push(
|
||||
{
|
||||
id: "test-page",
|
||||
components: {
|
||||
Page: () => React.createElement("Text")
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "another-page",
|
||||
components: {
|
||||
Page: () => React.createElement("Text")
|
||||
},
|
||||
},
|
||||
{
|
||||
components: {
|
||||
Page: () => React.createElement("Default")
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
describe("findRegisteredPage", () => {
|
||||
it("matching to first registered page without id", () => {
|
||||
const page = globalPageRegistry.getByPageTarget({ extensionId: ext.name });
|
||||
const page = findRegisteredPage(ext);
|
||||
|
||||
expect(page.id).toEqual(undefined);
|
||||
expect(page.extensionId).toEqual(ext.name);
|
||||
expect(page.url).toEqual(getExtensionPageUrl({ extensionId: ext.name }));
|
||||
expect(page.extensionName).toEqual(ext.name);
|
||||
expect(page.url).toEqual(getExtensionPageUrl({ extensionName }));
|
||||
});
|
||||
|
||||
it("returns matching page", () => {
|
||||
const page = globalPageRegistry.getByPageTarget({
|
||||
pageId: "test-page",
|
||||
extensionId: ext.name
|
||||
});
|
||||
const page = findRegisteredPage(ext, "test-page");
|
||||
|
||||
expect(page.id).toEqual("test-page");
|
||||
});
|
||||
|
||||
it("returns null if target not found", () => {
|
||||
const page = globalPageRegistry.getByPageTarget({
|
||||
pageId: "wrong-page",
|
||||
extensionId: ext.name
|
||||
});
|
||||
const page = findRegisteredPage(ext, "wrong-page");
|
||||
|
||||
expect(page).toBeNull();
|
||||
});
|
||||
|
||||
@ -20,8 +20,9 @@ export class BaseRegistry<T, I = T> {
|
||||
return () => this.remove(...itemArray);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars-ts
|
||||
protected getRegisteredItem(item: T, extension?: LensExtension): I {
|
||||
void extension;
|
||||
|
||||
return item as any;
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
// All registries managed by extensions api
|
||||
|
||||
import { Cluster } from "../../main/cluster";
|
||||
|
||||
export * from "./page-registry";
|
||||
export * from "./page-menu-registry";
|
||||
export * from "./menu-registry";
|
||||
@ -10,3 +12,14 @@ export * from "./kube-object-menu-registry";
|
||||
export * from "./cluster-feature-registry";
|
||||
export * from "./kube-object-status-registry";
|
||||
export * from "./command-registry";
|
||||
export * from "./sources";
|
||||
|
||||
export type Registrable<T> = (T[]) | ((cluster?: Cluster | null) => T[]);
|
||||
|
||||
export function recitfyRegisterable<T>(src: Registrable<T>, getCluster?: () => Cluster | null | undefined): T[] {
|
||||
if (typeof src === "function") {
|
||||
return src(getCluster());
|
||||
}
|
||||
|
||||
return src;
|
||||
}
|
||||
|
||||
@ -2,9 +2,14 @@
|
||||
import type { IconProps } from "../../renderer/components/icon";
|
||||
import type React from "react";
|
||||
import type { PageTarget, RegisteredPage } from "./page-registry";
|
||||
import { action } from "mobx";
|
||||
import { BaseRegistry } from "./base-registry";
|
||||
import { LensExtension } from "../lens-extension";
|
||||
import { RegisteredPageTarget } from ".";
|
||||
import { LensRendererExtension } from "../core-api";
|
||||
import { extensionLoader } from "../extension-loader";
|
||||
import { registeredClusterPageMenus, registeredGlobalPageMenus } from "../lens-renderer-extension";
|
||||
|
||||
export interface PageMenuComponents {
|
||||
Icon: React.ComponentType<IconProps>;
|
||||
}
|
||||
|
||||
export interface PageMenuRegistration {
|
||||
target?: PageTarget;
|
||||
@ -12,50 +17,57 @@ export interface PageMenuRegistration {
|
||||
components: PageMenuComponents;
|
||||
}
|
||||
|
||||
export interface RegisteredPageMenuTarget {
|
||||
target: RegisteredPageTarget;
|
||||
}
|
||||
|
||||
export type RegisteredPageMenu = PageMenuRegistration & RegisteredPageMenuTarget;
|
||||
|
||||
export interface ClusterPageMenuRegistration extends PageMenuRegistration {
|
||||
id?: string;
|
||||
parentId?: string;
|
||||
}
|
||||
|
||||
export interface PageMenuComponents {
|
||||
Icon: React.ComponentType<IconProps>;
|
||||
export type RegisteredClusterPageMenu = ClusterPageMenuRegistration & RegisteredPageMenuTarget;
|
||||
|
||||
export function getRegisteredPageMenu<T extends PageMenuRegistration>({ target: { pageId, params } = {}, ...rest }: T, extensionName: string): T & RegisteredPageMenuTarget {
|
||||
const target: RegisteredPageTarget = {
|
||||
params,
|
||||
pageId,
|
||||
extensionName,
|
||||
};
|
||||
|
||||
return { ...rest, target } as T & RegisteredPageMenuTarget;
|
||||
}
|
||||
|
||||
export class PageMenuRegistry<T extends PageMenuRegistration> extends BaseRegistry<T> {
|
||||
@action
|
||||
add(items: T[], ext: LensExtension) {
|
||||
const normalizedItems = items.map(menuItem => {
|
||||
menuItem.target = {
|
||||
extensionId: ext.name,
|
||||
...(menuItem.target || {}),
|
||||
};
|
||||
export function getGlobalPageMenus(): RegisteredPageMenu[] {
|
||||
const extensions = extensionLoader.allEnabledInstances as LensRendererExtension[];
|
||||
|
||||
return menuItem;
|
||||
});
|
||||
|
||||
return super.add(normalizedItems);
|
||||
}
|
||||
return extensions.flatMap(ext => ext[registeredGlobalPageMenus]);
|
||||
}
|
||||
|
||||
export class ClusterPageMenuRegistry extends PageMenuRegistry<ClusterPageMenuRegistration> {
|
||||
getRootItems() {
|
||||
return this.getItems().filter((item) => !item.parentId);
|
||||
}
|
||||
function getClusterPageMenus(): RegisteredClusterPageMenu[] {
|
||||
const extensions = extensionLoader.allEnabledInstances as LensRendererExtension[];
|
||||
|
||||
getSubItems(parent: ClusterPageMenuRegistration) {
|
||||
return this.getItems().filter((item) => (
|
||||
item.parentId === parent.id &&
|
||||
item.target.extensionId === parent.target.extensionId
|
||||
return extensions.flatMap(ext => ext[registeredClusterPageMenus]);
|
||||
}
|
||||
|
||||
export function getRootClusterPageMenus(): RegisteredClusterPageMenu[] {
|
||||
return getClusterPageMenus().filter(pageMenu => !pageMenu.parentId);
|
||||
}
|
||||
|
||||
export function getChildClusterPageMenus(parentMenu: RegisteredClusterPageMenu): RegisteredClusterPageMenu[] {
|
||||
return getClusterPageMenus()
|
||||
.filter(pageMenu => (
|
||||
pageMenu.parentId === parentMenu.id
|
||||
&& pageMenu.target.extensionName === parentMenu.target.extensionName
|
||||
));
|
||||
}
|
||||
|
||||
getByPage({ id: pageId, extensionId }: RegisteredPage) {
|
||||
return this.getItems().find((item) => (
|
||||
item.target.pageId == pageId &&
|
||||
item.target.extensionId === extensionId
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
export const globalPageMenuRegistry = new PageMenuRegistry();
|
||||
export const clusterPageMenuRegistry = new ClusterPageMenuRegistry();
|
||||
export function getClusterPageMenuByPage({ id: pageId, extensionName }: RegisteredPage): RegisteredClusterPageMenu {
|
||||
return getClusterPageMenus()
|
||||
.find(pageMenu => (
|
||||
pageMenu.target.pageId == pageId
|
||||
&& pageMenu.target.extensionName === extensionName
|
||||
));
|
||||
}
|
||||
|
||||
@ -2,10 +2,15 @@
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { BaseRegistry } from "./base-registry";
|
||||
import { LensExtension, sanitizeExtensionName } from "../lens-extension";
|
||||
import { sanitizeExtensionName } from "../lens-extension";
|
||||
import { PageParam, PageParamInit } from "../../renderer/navigation/page-param";
|
||||
import { createPageParam } from "../../renderer/navigation/helpers";
|
||||
import { extensionLoader } from "../extension-loader";
|
||||
import { LensRendererExtension } from "../core-api";
|
||||
import { registeredClusterPages, registeredGlobalPages } from "../lens-renderer-extension";
|
||||
import { TabLayoutRoute } from "../renderer-api/components";
|
||||
import { RegistrationScope } from "./sources";
|
||||
import { getChildClusterPageMenus, RegisteredClusterPageMenu } from "./page-menu-registry";
|
||||
|
||||
export interface PageRegistration {
|
||||
/**
|
||||
@ -24,12 +29,16 @@ export interface PageComponents {
|
||||
Page: React.ComponentType<any>;
|
||||
}
|
||||
|
||||
export interface PageTarget<P = PageParams> {
|
||||
extensionId?: string;
|
||||
export interface PageTarget<P extends PageParams = PageParams> {
|
||||
extensionName?: string;
|
||||
pageId?: string;
|
||||
params?: P;
|
||||
}
|
||||
|
||||
export interface RegisteredPageTarget<P extends PageParams = PageParams> extends PageTarget<P> {
|
||||
extensionName: string
|
||||
}
|
||||
|
||||
export interface PageParams<V = any> {
|
||||
[paramName: string]: V;
|
||||
}
|
||||
@ -42,81 +51,153 @@ export interface PageComponentProps<P extends PageParams = {}> {
|
||||
|
||||
export interface RegisteredPage {
|
||||
id: string;
|
||||
extensionId: string;
|
||||
extensionName: string;
|
||||
url: string; // registered extension's page URL (without page params)
|
||||
params: PageParams<PageParam>; // normalized params
|
||||
components: PageComponents; // normalized components
|
||||
}
|
||||
|
||||
export function getExtensionPageUrl(target: PageTarget): string {
|
||||
const { extensionId, pageId = "", params: targetParams = {} } = target;
|
||||
/**
|
||||
* Finds the first registered page on `extension` matching `pageId` in all of `sources`' scopes
|
||||
* @param extension The extension to query for a matching `RegisteredPage`
|
||||
* @param pageId The `PageId` to search for
|
||||
* @param sources Whether to search for global pages or cluster pages or both
|
||||
*/
|
||||
export function findRegisteredPage(extension: LensRendererExtension | undefined, pageId?: string, sources = new Set([RegistrationScope.GLOBAL, RegistrationScope.CLUSTER])): RegisteredPage | null {
|
||||
if (sources.has(RegistrationScope.GLOBAL)) {
|
||||
const page = extension?.[registeredGlobalPages].find(page => page.id === pageId);
|
||||
|
||||
const pagePath = ["/extension", sanitizeExtensionName(extensionId), pageId]
|
||||
if (page) {
|
||||
return page;
|
||||
}
|
||||
}
|
||||
|
||||
if (sources.has(RegistrationScope.CLUSTER)) {
|
||||
const page = extension?.[registeredClusterPages].find(page => page.id === pageId);
|
||||
|
||||
if (page) {
|
||||
return page;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getExtensionPageUrl(target: PageTarget): string {
|
||||
const { extensionName, pageId = "", params: targetParams = {} } = target;
|
||||
|
||||
const pagePath = ["/extension", sanitizeExtensionName(extensionName), pageId]
|
||||
.filter(Boolean)
|
||||
.join("/").replace(/\/+/g, "/").replace(/\/$/, ""); // normalize multi-slashes (e.g. coming from page.id)
|
||||
|
||||
const pageUrl = new URL(pagePath, `http://localhost`);
|
||||
|
||||
// stringify params to matched target page
|
||||
const registeredPage = globalPageRegistry.getByPageTarget(target) || clusterPageRegistry.getByPageTarget(target);
|
||||
const extension = extensionLoader.getExtensionByName(extensionName);
|
||||
|
||||
if (registeredPage?.params) {
|
||||
Object.entries(registeredPage.params).forEach(([name, param]) => {
|
||||
const paramValue = param.stringify(targetParams[name]);
|
||||
if (extension instanceof LensRendererExtension) {
|
||||
const registeredPage = findRegisteredPage(extension, target.pageId);
|
||||
|
||||
if (param.init.skipEmpty && param.isEmpty(paramValue)) {
|
||||
pageUrl.searchParams.delete(name);
|
||||
} else {
|
||||
pageUrl.searchParams.set(name, paramValue);
|
||||
}
|
||||
});
|
||||
if (registeredPage?.params) {
|
||||
Object.entries(registeredPage.params).forEach(([name, param]) => {
|
||||
const paramValue = param.stringify(targetParams[name]);
|
||||
|
||||
if (param.init.skipEmpty && param.isEmpty(paramValue)) {
|
||||
pageUrl.searchParams.delete(name);
|
||||
} else {
|
||||
pageUrl.searchParams.set(name, paramValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return pageUrl.href.replace(pageUrl.origin, "");
|
||||
}
|
||||
|
||||
export class PageRegistry extends BaseRegistry<PageRegistration, RegisteredPage> {
|
||||
protected getRegisteredItem(page: PageRegistration, ext: LensExtension): RegisteredPage {
|
||||
const { id: pageId } = page;
|
||||
const extensionId = ext.name;
|
||||
const params = this.normalizeParams(page.params);
|
||||
const components = this.normalizeComponents(page.components, params);
|
||||
const url = getExtensionPageUrl({ extensionId, pageId });
|
||||
function normalizeComponents(components: PageComponents, params?: PageParams<PageParam>): PageComponents {
|
||||
if (params) {
|
||||
const { Page } = components;
|
||||
|
||||
return {
|
||||
id: pageId, extensionId, params, components, url,
|
||||
};
|
||||
components.Page = observer((props: object) => React.createElement(Page, { params, ...props }));
|
||||
}
|
||||
|
||||
protected normalizeComponents(components: PageComponents, params?: PageParams<PageParam>): PageComponents {
|
||||
if (params) {
|
||||
const { Page } = components;
|
||||
return components;
|
||||
}
|
||||
|
||||
components.Page = observer((props: object) => React.createElement(Page, { params, ...props }));
|
||||
}
|
||||
|
||||
return components;
|
||||
function normalizeParams(params?: PageParams<string | ExtensionPageParamInit>): PageParams<PageParam> {
|
||||
if (!params) {
|
||||
return;
|
||||
}
|
||||
|
||||
protected normalizeParams(params?: PageParams<string | ExtensionPageParamInit>): PageParams<PageParam> {
|
||||
if (!params) {
|
||||
return;
|
||||
}
|
||||
Object.entries(params).forEach(([name, value]) => {
|
||||
const paramInit: PageParamInit = typeof value === "object"
|
||||
? { name, ...value }
|
||||
: { name, defaultValue: value };
|
||||
Object.entries(params).forEach(([name, value]) => {
|
||||
const paramInit: PageParamInit = typeof value === "object"
|
||||
? { name, ...value }
|
||||
: { name, defaultValue: value };
|
||||
|
||||
params[paramInit.name] = createPageParam(paramInit);
|
||||
});
|
||||
params[paramInit.name] = createPageParam(paramInit);
|
||||
});
|
||||
|
||||
return params as PageParams<PageParam>;
|
||||
}
|
||||
return params as PageParams<PageParam>;
|
||||
}
|
||||
|
||||
getByPageTarget(target: PageTarget): RegisteredPage | null {
|
||||
return this.getItems().find(page => page.extensionId === target.extensionId && page.id === target.pageId) || null;
|
||||
export function getRegisteredPage({ id, ...page}: PageRegistration, extensionName: string): RegisteredPage {
|
||||
const params = normalizeParams(page.params);
|
||||
const components = normalizeComponents(page.components);
|
||||
|
||||
const pagePath = ["/extension", sanitizeExtensionName(extensionName), id]
|
||||
.filter(Boolean)
|
||||
.join("/").replace(/\/+/g, "/").replace(/\/$/, ""); // normalize multi-slashes (e.g. coming from page.id)
|
||||
|
||||
const pageUrl = new URL(pagePath, `http://localhost`);
|
||||
const url = pageUrl.href.replace(pageUrl.origin, "");
|
||||
|
||||
return { id, params, components, extensionName, url };
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the `RegisteredPage` of an extension looking through all `sources`
|
||||
* @param target The `extensionName` and `pageId` for the desired page
|
||||
* @param sources Whether to search for global pages or cluster pages or both
|
||||
*/
|
||||
export function getByPageTarget(target?: PageTarget, sources = new Set([RegistrationScope.GLOBAL, RegistrationScope.CLUSTER])): RegisteredPage | null {
|
||||
return findRegisteredPage(
|
||||
extensionLoader.getExtensionByName(target?.extensionName) as LensRendererExtension,
|
||||
target?.pageId,
|
||||
sources,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all the registered pages from all extensions
|
||||
* @param source Whether to get all the global or cluster pages
|
||||
*/
|
||||
export function getAllRegisteredPages(source: RegistrationScope): RegisteredPage[] {
|
||||
const extensions = extensionLoader.allEnabledInstances as LensRendererExtension[];
|
||||
|
||||
switch (source) {
|
||||
case RegistrationScope.GLOBAL:
|
||||
return extensions.flatMap(ext => ext[registeredGlobalPages]);
|
||||
case RegistrationScope.CLUSTER:
|
||||
return extensions.flatMap(ext => ext[registeredClusterPages]);
|
||||
}
|
||||
}
|
||||
|
||||
export const globalPageRegistry = new PageRegistry();
|
||||
export const clusterPageRegistry = new PageRegistry();
|
||||
export function getTabLayoutRoutes(parentMenu: RegisteredClusterPageMenu): TabLayoutRoute[] {
|
||||
if (!parentMenu.id) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return getChildClusterPageMenus(parentMenu)
|
||||
.map(subMenu => [
|
||||
getByPageTarget(subMenu.target),
|
||||
subMenu,
|
||||
] as const)
|
||||
.filter(([subPage]) => subPage)
|
||||
.map(([{ components, extensionName, id: pageId, url }, { title, target: { params } }]) => ({
|
||||
routePath: url,
|
||||
url: getExtensionPageUrl({ extensionName, pageId, params }),
|
||||
title,
|
||||
component: components.Page,
|
||||
}));
|
||||
}
|
||||
|
||||
7
src/extensions/registries/sources.ts
Normal file
7
src/extensions/registries/sources.ts
Normal file
@ -0,0 +1,7 @@
|
||||
/**
|
||||
* This represents which sources the RegisteredPages should be searched from
|
||||
*/
|
||||
export enum RegistrationScope {
|
||||
GLOBAL = "global",
|
||||
CLUSTER = "cluster",
|
||||
}
|
||||
@ -33,14 +33,13 @@ import { Terminal } from "./dock/terminal";
|
||||
import { getHostedCluster, getHostedClusterId } from "../../common/cluster-store";
|
||||
import logger from "../../main/logger";
|
||||
import { webFrame } from "electron";
|
||||
import { clusterPageRegistry, getExtensionPageUrl } from "../../extensions/registries/page-registry";
|
||||
import { extensionLoader } from "../../extensions/extension-loader";
|
||||
import { appEventBus } from "../../common/event-bus";
|
||||
import { broadcastMessage, requestMain } from "../../common/ipc";
|
||||
import whatInput from "what-input";
|
||||
import { clusterSetFrameIdHandler } from "../../common/cluster-ipc";
|
||||
import { ClusterPageMenuRegistration, clusterPageMenuRegistry } from "../../extensions/registries";
|
||||
import { TabLayout, TabLayoutRoute } from "./layout/tab-layout";
|
||||
import { getAllRegisteredPages, getByPageTarget, getChildClusterPageMenus, getClusterPageMenuByPage, getExtensionPageUrl, getRootClusterPageMenus, getTabLayoutRoutes, RegisteredClusterPageMenu, RegistrationScope } from "../../extensions/registries";
|
||||
import { TabLayout } from "./layout/tab-layout";
|
||||
import { StatefulSetScaleDialog } from "./+workloads-statefulsets/statefulset-scale-dialog";
|
||||
import { eventStore } from "./+events/event.store";
|
||||
import { nodesStore } from "./+nodes/nodes.store";
|
||||
@ -100,38 +99,32 @@ export class App extends React.Component {
|
||||
return nodesStore.getWarningsCount() + eventStore.getWarningsCount();
|
||||
}
|
||||
|
||||
getTabLayoutRoutes(menuItem: ClusterPageMenuRegistration) {
|
||||
const routes: TabLayoutRoute[] = [];
|
||||
|
||||
getTabLayoutRoutes(menuItem: RegisteredClusterPageMenu) {
|
||||
if (!menuItem.id) {
|
||||
return routes;
|
||||
return [];
|
||||
}
|
||||
clusterPageMenuRegistry.getSubItems(menuItem).forEach((subMenu) => {
|
||||
const page = clusterPageRegistry.getByPageTarget(subMenu.target);
|
||||
|
||||
if (page) {
|
||||
routes.push({
|
||||
routePath: page.url,
|
||||
url: getExtensionPageUrl(subMenu.target),
|
||||
title: subMenu.title,
|
||||
component: page.components.Page,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return routes;
|
||||
return getChildClusterPageMenus(menuItem)
|
||||
.map(subMenu => [getByPageTarget(subMenu.target), subMenu] as const)
|
||||
.filter(([page]) => page)
|
||||
.map(([page, subMenu]) => ({
|
||||
routePath: page.url,
|
||||
url: getExtensionPageUrl(subMenu.target),
|
||||
title: subMenu.title,
|
||||
component: page.components.Page,
|
||||
}));
|
||||
}
|
||||
|
||||
renderExtensionTabLayoutRoutes() {
|
||||
return clusterPageMenuRegistry.getRootItems().map((menu, index) => {
|
||||
const tabRoutes = this.getTabLayoutRoutes(menu);
|
||||
return getRootClusterPageMenus().map((menu, index) => {
|
||||
const tabRoutes = getTabLayoutRoutes(menu);
|
||||
|
||||
if (tabRoutes.length > 0) {
|
||||
const pageComponent = () => <TabLayout tabs={tabRoutes}/>;
|
||||
|
||||
return <Route key={`extension-tab-layout-route-${index}`} component={pageComponent} path={tabRoutes.map((tab) => tab.routePath)}/>;
|
||||
} else {
|
||||
const page = clusterPageRegistry.getByPageTarget(menu.target);
|
||||
const page = getByPageTarget(menu.target, new Set([RegistrationScope.CLUSTER]));
|
||||
|
||||
if (page) {
|
||||
return <Route key={`extension-tab-layout-route-${index}`} path={page.url} component={page.components.Page}/>;
|
||||
@ -141,8 +134,8 @@ export class App extends React.Component {
|
||||
}
|
||||
|
||||
renderExtensionRoutes() {
|
||||
return clusterPageRegistry.getItems().map((page, index) => {
|
||||
const menu = clusterPageMenuRegistry.getByPage(page);
|
||||
return getAllRegisteredPages(RegistrationScope.CLUSTER).map((page, index) => {
|
||||
const menu = getClusterPageMenuByPage(page);
|
||||
|
||||
if (!menu) {
|
||||
return <Route key={`extension-route-${index}`} path={page.url} component={page.components.Page}/>;
|
||||
|
||||
@ -14,9 +14,9 @@ import { ClusterSettings, clusterSettingsRoute } from "../+cluster-settings";
|
||||
import { clusterViewRoute, clusterViewURL } from "./cluster-view.route";
|
||||
import { clusterStore } from "../../../common/cluster-store";
|
||||
import { hasLoadedView, initView, lensViews, refreshViews } from "./lens-views";
|
||||
import { globalPageRegistry } from "../../../extensions/registries/page-registry";
|
||||
import { Extensions, extensionsRoute } from "../+extensions";
|
||||
import { getMatchedClusterId } from "../../navigation";
|
||||
import { getAllRegisteredPages, RegistrationScope } from "../../../extensions/registries";
|
||||
|
||||
@observer
|
||||
export class ClusterManager extends React.Component {
|
||||
@ -69,7 +69,7 @@ export class ClusterManager extends React.Component {
|
||||
<Route component={AddCluster} {...addClusterRoute} />
|
||||
<Route component={ClusterView} {...clusterViewRoute} />
|
||||
<Route component={ClusterSettings} {...clusterSettingsRoute} />
|
||||
{globalPageRegistry.getItems().map(({ url, components: { Page } }) => {
|
||||
{getAllRegisteredPages(RegistrationScope.GLOBAL).map(({ url, components: { Page } }) => {
|
||||
return <Route key={url} path={url} component={Page}/>;
|
||||
})}
|
||||
<Redirect exact to={this.startUrl}/>
|
||||
|
||||
@ -15,7 +15,7 @@ import { addClusterURL } from "../+add-cluster";
|
||||
import { landingURL } from "../+landing-page";
|
||||
import { clusterViewURL } from "./cluster-view.route";
|
||||
import { ClusterActions } from "./cluster-actions";
|
||||
import { getExtensionPageUrl, globalPageMenuRegistry, globalPageRegistry } from "../../../extensions/registries";
|
||||
import { getByPageTarget, getExtensionPageUrl, getGlobalPageMenus, RegistrationScope } from "../../../extensions/registries";
|
||||
import { commandRegistry } from "../../../extensions/registries/command-registry";
|
||||
import { CommandOverlay } from "../command-palette/command-container";
|
||||
import { computed, observable } from "mobx";
|
||||
@ -135,8 +135,8 @@ export class ClustersMenu extends React.Component<Props> {
|
||||
</Menu>
|
||||
</div>
|
||||
<div className="extensions">
|
||||
{globalPageMenuRegistry.getItems().map(({ title, target, components: { Icon } }) => {
|
||||
const registeredPage = globalPageRegistry.getByPageTarget(target);
|
||||
{getGlobalPageMenus().map(({ title, target, components: { Icon } }) => {
|
||||
const registeredPage = getByPageTarget(target, new Set([RegistrationScope.GLOBAL]));
|
||||
|
||||
if (!registeredPage){
|
||||
return;
|
||||
|
||||
@ -27,8 +27,8 @@ import { CustomResources } from "../+custom-resources/custom-resources";
|
||||
import { isActiveRoute } from "../../navigation";
|
||||
import { isAllowedResource } from "../../../common/rbac";
|
||||
import { Spinner } from "../spinner";
|
||||
import { ClusterPageMenuRegistration, clusterPageMenuRegistry, clusterPageRegistry, getExtensionPageUrl } from "../../../extensions/registries";
|
||||
import { SidebarItem } from "./sidebar-item";
|
||||
import { getByPageTarget, getExtensionPageUrl, getRootClusterPageMenus, getTabLayoutRoutes, RegistrationScope } from "../../../extensions/registries";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
@ -50,14 +50,12 @@ export class Sidebar extends React.Component<Props> {
|
||||
}
|
||||
|
||||
return Object.entries(crdStore.groups).map(([group, crds]) => {
|
||||
const submenus: TabLayoutRoute[] = crds.map((crd) => {
|
||||
return {
|
||||
title: crd.getResourceKind(),
|
||||
component: CrdList,
|
||||
url: crd.getResourceUrl(),
|
||||
routePath: String(crdResourcesRoute.path),
|
||||
};
|
||||
});
|
||||
const submenus: TabLayoutRoute[] = crds.map(crd => ({
|
||||
title: crd.getResourceKind(),
|
||||
component: CrdList,
|
||||
url: crd.getResourceUrl(),
|
||||
routePath: String(crdResourcesRoute.path),
|
||||
}));
|
||||
|
||||
return (
|
||||
<SidebarItem
|
||||
@ -72,43 +70,18 @@ export class Sidebar extends React.Component<Props> {
|
||||
});
|
||||
}
|
||||
|
||||
getTabLayoutRoutes(menu: ClusterPageMenuRegistration): TabLayoutRoute[] {
|
||||
const routes: TabLayoutRoute[] = [];
|
||||
|
||||
if (!menu.id) {
|
||||
return routes;
|
||||
}
|
||||
|
||||
clusterPageMenuRegistry.getSubItems(menu).forEach((subMenu) => {
|
||||
const subPage = clusterPageRegistry.getByPageTarget(subMenu.target);
|
||||
|
||||
if (subPage) {
|
||||
const { extensionId, id: pageId } = subPage;
|
||||
|
||||
routes.push({
|
||||
routePath: subPage.url,
|
||||
url: getExtensionPageUrl({ extensionId, pageId, params: subMenu.target.params }),
|
||||
title: subMenu.title,
|
||||
component: subPage.components.Page,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return routes;
|
||||
}
|
||||
|
||||
renderRegisteredMenus() {
|
||||
return clusterPageMenuRegistry.getRootItems().map((menuItem, index) => {
|
||||
const registeredPage = clusterPageRegistry.getByPageTarget(menuItem.target);
|
||||
const tabRoutes = this.getTabLayoutRoutes(menuItem);
|
||||
return getRootClusterPageMenus().map((menuItem, index) => {
|
||||
const registeredPage = getByPageTarget(menuItem.target, new Set([RegistrationScope.CLUSTER]));
|
||||
const tabRoutes = getTabLayoutRoutes(menuItem);
|
||||
const id = `registered-item-${index}`;
|
||||
let pageUrl: string;
|
||||
let isActive = false;
|
||||
|
||||
if (registeredPage) {
|
||||
const { extensionId, id: pageId } = registeredPage;
|
||||
const { extensionName, id: pageId } = registeredPage;
|
||||
|
||||
pageUrl = getExtensionPageUrl({ extensionId, pageId, params: menuItem.target.params });
|
||||
pageUrl = getExtensionPageUrl({ extensionName, pageId, params: menuItem.target.params });
|
||||
isActive = isActiveRoute(registeredPage.url);
|
||||
} else if (tabRoutes.length > 0) {
|
||||
pageUrl = tabRoutes[0].url;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user