1
0
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:
Sebastian Malton 2021-02-26 16:14:34 -05:00
parent e718b250cc
commit 1e2069466a
14 changed files with 385 additions and 248 deletions

View File

@ -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";

View File

@ -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),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
/**
* This represents which sources the RegisteredPages should be searched from
*/
export enum RegistrationScope {
GLOBAL = "global",
CLUSTER = "cluster",
}

View File

@ -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}/>;

View File

@ -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}/>

View File

@ -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;

View File

@ -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;