1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Navigation refactoring, handling extension page params (#1651)

* decentralizing page url-params management -- PoC / tsc 4.1 random fixes

Signed-off-by: Roman <ixrock@gmail.com>

* fixes, tweak example-extension for demo

Signed-off-by: Roman <ixrock@gmail.com>

* lint fixes, revert tests

Signed-off-by: Roman <ixrock@gmail.com>

* removed occasional changes related to typescript 4.1

Signed-off-by: Roman <ixrock@gmail.com>

* updated example with 2 menu-items targeting same page with different params

Signed-off-by: Roman <ixrock@gmail.com>

* fix: merge page url chunks with native URL()-api, simplified default page-params registration

Signed-off-by: Roman <ixrock@gmail.com>

* fix: make lint happy

Signed-off-by: Roman <ixrock@gmail.com>

* fix: unit-tests

Signed-off-by: Roman <ixrock@gmail.com>

* renaming by jim's request: UrlParam => PageParam (type), createUrlParam => createPageParam (helper)

Signed-off-by: Roman <ixrock@gmail.com>

* fix: reverting NamespaceStore public-api breaking changes

Signed-off-by: Roman <ixrock@gmail.com>

* lint fix

Signed-off-by: Roman <ixrock@gmail.com>

* fine-tuning

Signed-off-by: Roman <ixrock@gmail.com>

* yes, lint always unhappy

Signed-off-by: Roman <ixrock@gmail.com>

* fix build

Signed-off-by: Roman <ixrock@gmail.com>

* small fixes

Signed-off-by: Roman <ixrock@gmail.com>

* fix merge-conflicts

Signed-off-by: Roman <ixrock@gmail.com>

* removed `isSystem` page-param's init field exposed to extensions-api

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman 2020-12-22 15:29:25 +02:00 committed by GitHub
parent 11c611dabe
commit be4e1aa15c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
64 changed files with 751 additions and 518 deletions

View File

@ -1,30 +1,64 @@
import { LensRendererExtension, Component } from "@k8slens/extensions";
import { CoffeeDoodle } from "react-open-doodles";
import path from "path";
import React from "react"; import React from "react";
import { observer } from "mobx-react";
import { CoffeeDoodle } from "react-open-doodles";
import { Component, Interface, K8sApi, LensRendererExtension } from "@k8slens/extensions";
export function ExampleIcon(props: Component.IconProps) { export interface ExamplePageProps extends Interface.PageComponentProps<ExamplePageParams> {
return <Component.Icon {...props} material="pages" tooltip={path.basename(__filename)}/>; extension: LensRendererExtension; // provided in "./renderer.tsx"
} }
export class ExamplePage extends React.Component<{ extension: LensRendererExtension }> { export interface ExamplePageParams {
exampleId: string;
selectedNamespaces: K8sApi.Namespace[];
}
export const namespaceStore = K8sApi.apiManager.getStore<K8sApi.NamespaceStore>(K8sApi.namespacesApi);
@observer
export class ExamplePage extends React.Component<ExamplePageProps> {
async componentDidMount() {
await namespaceStore.loadAll();
}
deactivate = () => { deactivate = () => {
const { extension } = this.props; const { extension } = this.props;
extension.disable(); extension.disable();
}; };
render() { renderSelectedNamespaces() {
const doodleStyle = { const { selectedNamespaces } = this.props.params;
width: "200px"
};
return ( return (
<div className="flex column gaps align-flex-start"> <div className="flex gaps inline">
<div style={doodleStyle}><CoffeeDoodle accent="#3d90ce" /></div> {selectedNamespaces.get().map(ns => {
<p>Hello from Example extension!</p> const name = ns.getName();
<p>File: <i>{__filename}</i></p>
return <Component.Badge key={name} label={name} tooltip={`Created: ${ns.getAge()}`}/>;
})}
</div>
);
}
render() {
const { exampleId } = this.props.params;
return (
<div className="flex column gaps align-flex-start" style={{ padding: 24 }}>
<div style={{ width: 200 }}>
<CoffeeDoodle accent="#3d90ce"/>
</div>
<div>Hello from Example extension!</div>
<div>Location: <i>{location.href}</i></div>
<div>Namespaces: {this.renderSelectedNamespaces()}</div>
<p className="url-params-demo flex column gaps">
<a onClick={() => exampleId.set("secret")}>Show secret button</a>
{exampleId.get() === "secret" && (
<Component.Button accent label="Deactivate" onClick={this.deactivate}/> <Component.Button accent label="Deactivate" onClick={this.deactivate}/>
)}
</p>
</div> </div>
); );
} }

View File

@ -1,25 +1,45 @@
import { LensRendererExtension } from "@k8slens/extensions"; import { Component, Interface, K8sApi, LensRendererExtension } from "@k8slens/extensions";
import { ExampleIcon, ExamplePage } from "./page"; import { ExamplePage, ExamplePageParams, namespaceStore } from "./page";
import React from "react"; import React from "react";
import path from "path";
export default class ExampleExtension extends LensRendererExtension { export default class ExampleExtension extends LensRendererExtension {
clusterPages = [ clusterPages: Interface.PageRegistration[] = [
{ {
id: "example",
title: "Example Extension",
components: { components: {
Page: () => <ExamplePage extension={this}/>, Page: (props: Interface.PageComponentProps<ExamplePageParams>) => {
return <ExamplePage {...props} extension={this}/>;
},
},
params: {
// setup basic param "exampleId" with default value "demo"
exampleId: "demo",
// setup advanced multi-values param "selectedNamespaces" with custom parsing/stringification
selectedNamespaces: {
defaultValueStringified: ["default", "kube-system"],
multiValues: true,
parse(values: string[]) { // from URL
return values.map(name => namespaceStore.getByName(name)).filter(Boolean);
},
stringify(values: K8sApi.Namespace[]) { // to URL
return values.map(namespace => namespace.getName());
},
}
} }
} }
]; ];
clusterPageMenus = [ clusterPageMenus: Interface.ClusterPageMenuRegistration[] = [
{ {
target: { pageId: "example", params: {} }, title: "Example extension",
title: "Example Extension",
components: { components: {
Icon: ExampleIcon, Icon: ExampleIcon,
} },
} },
]; ];
} }
export function ExampleIcon(props: Component.IconProps) {
return <Component.Icon {...props} material="pages" tooltip={path.basename(__filename)}/>;
}

View File

@ -198,20 +198,6 @@
"@hapi/call": "^8.0.0", "@hapi/call": "^8.0.0",
"@hapi/subtext": "^7.0.3", "@hapi/subtext": "^7.0.3",
"@kubernetes/client-node": "^0.12.0", "@kubernetes/client-node": "^0.12.0",
"@types/crypto-js": "^3.1.47",
"@types/electron-window-state": "^2.0.34",
"@types/fs-extra": "^9.0.1",
"@types/http-proxy": "^1.17.4",
"@types/js-yaml": "^3.12.4",
"@types/jsdom": "^16.2.4",
"@types/jsonpath": "^0.2.0",
"@types/lodash": "^4.14.155",
"@types/marked": "^0.7.4",
"@types/mock-fs": "^4.10.0",
"@types/node": "^12.12.45",
"@types/proper-lockfile": "^4.1.1",
"@types/react-beautiful-dnd": "^13.0.0",
"@types/tar": "^4.0.4",
"array-move": "^3.0.0", "array-move": "^3.0.0",
"await-lock": "^2.1.0", "await-lock": "^2.1.0",
"chalk": "^4.1.0", "chalk": "^4.1.0",
@ -235,12 +221,16 @@
"md5-file": "^5.0.0", "md5-file": "^5.0.0",
"mobx": "^5.15.7", "mobx": "^5.15.7",
"mobx-observable-history": "^1.0.3", "mobx-observable-history": "^1.0.3",
"mobx-react": "^6.2.2",
"mock-fs": "^4.12.0", "mock-fs": "^4.12.0",
"node-pty": "^0.9.0", "node-pty": "^0.9.0",
"npm": "^6.14.8", "npm": "^6.14.8",
"openid-client": "^3.15.2", "openid-client": "^3.15.2",
"path-to-regexp": "^6.1.0", "path-to-regexp": "^6.1.0",
"proper-lockfile": "^4.1.1", "proper-lockfile": "^4.1.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-router": "^5.2.0",
"request": "^2.88.2", "request": "^2.88.2",
"request-promise-native": "^1.0.8", "request-promise-native": "^1.0.8",
"semver": "^7.3.2", "semver": "^7.3.2",
@ -287,6 +277,7 @@
"@types/http-proxy": "^1.17.4", "@types/http-proxy": "^1.17.4",
"@types/jest": "^25.2.3", "@types/jest": "^25.2.3",
"@types/js-yaml": "^3.12.4", "@types/js-yaml": "^3.12.4",
"@types/jsdom": "^16.2.4",
"@types/jsonpath": "^0.2.0", "@types/jsonpath": "^0.2.0",
"@types/lodash": "^4.14.155", "@types/lodash": "^4.14.155",
"@types/marked": "^0.7.4", "@types/marked": "^0.7.4",
@ -298,9 +289,10 @@
"@types/npm": "^2.0.31", "@types/npm": "^2.0.31",
"@types/progress-bar-webpack-plugin": "^2.1.0", "@types/progress-bar-webpack-plugin": "^2.1.0",
"@types/proper-lockfile": "^4.1.1", "@types/proper-lockfile": "^4.1.1",
"@types/react": "^16.9.35", "@types/react": "^17.0.0",
"@types/react-beautiful-dnd": "^13.0.0", "@types/react-beautiful-dnd": "^13.0.0",
"@types/react-router-dom": "^5.1.5", "@types/react-dom": "^17.0.0",
"@types/react-router-dom": "^5.1.6",
"@types/react-select": "^3.0.13", "@types/react-select": "^3.0.13",
"@types/react-window": "^1.8.2", "@types/react-window": "^1.8.2",
"@types/request": "^2.48.5", "@types/request": "^2.48.5",
@ -309,6 +301,7 @@
"@types/sharp": "^0.26.0", "@types/sharp": "^0.26.0",
"@types/shelljs": "^0.8.8", "@types/shelljs": "^0.8.8",
"@types/spdy": "^3.4.4", "@types/spdy": "^3.4.4",
"@types/tar": "^4.0.4",
"@types/tcp-port-used": "^1.0.0", "@types/tcp-port-used": "^1.0.0",
"@types/tempy": "^0.3.0", "@types/tempy": "^0.3.0",
"@types/terser-webpack-plugin": "^3.0.0", "@types/terser-webpack-plugin": "^3.0.0",
@ -352,7 +345,6 @@
"jest-mock-extended": "^1.0.10", "jest-mock-extended": "^1.0.10",
"make-plural": "^6.2.1", "make-plural": "^6.2.1",
"mini-css-extract-plugin": "^0.9.0", "mini-css-extract-plugin": "^0.9.0",
"mobx-react": "^6.2.2",
"moment": "^2.26.0", "moment": "^2.26.0",
"node-loader": "^0.6.0", "node-loader": "^0.6.0",
"node-sass": "^4.14.1", "node-sass": "^4.14.1",
@ -362,11 +354,8 @@
"prettier": "^2.2.0", "prettier": "^2.2.0",
"progress-bar-webpack-plugin": "^2.1.0", "progress-bar-webpack-plugin": "^2.1.0",
"raw-loader": "^4.0.1", "raw-loader": "^4.0.1",
"react": "^16.14.0",
"react-beautiful-dnd": "^13.0.0", "react-beautiful-dnd": "^13.0.0",
"react-dom": "^16.13.1",
"react-refresh": "^0.9.0", "react-refresh": "^0.9.0",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-select": "^3.1.0", "react-select": "^3.1.0",
"react-window": "^1.8.5", "react-window": "^1.8.5",
@ -382,7 +371,7 @@
"typedoc": "0.17.0-3", "typedoc": "0.17.0-3",
"typedoc-plugin-markdown": "^2.4.0", "typedoc-plugin-markdown": "^2.4.0",
"typeface-roboto": "^0.0.75", "typeface-roboto": "^0.0.75",
"typescript": "^4.0.2", "typescript": "4.0.2",
"url-loader": "^4.1.0", "url-loader": "^4.1.0",
"webpack": "^4.44.2", "webpack": "^4.44.2",
"webpack-cli": "^3.3.11", "webpack-cli": "^3.3.11",

View File

@ -14,7 +14,6 @@ export * from "./splitArray";
export * from "./saveToAppFiles"; export * from "./saveToAppFiles";
export * from "./singleton"; export * from "./singleton";
export * from "./openExternal"; export * from "./openExternal";
export * from "./rectify-array";
export * from "./downloadFile"; export * from "./downloadFile";
export * from "./escapeRegExp"; export * from "./escapeRegExp";
export * from "./tar"; export * from "./tar";

View File

@ -1,8 +0,0 @@
/**
* rectify condences the single item or array of T type, to an array.
* @param items either one item or an array of items
* @returns a list of items
*/
export function rectify<T>(items: T | T[]): T[] {
return Array.isArray(items) ? items : [items];
}

View File

@ -3,6 +3,6 @@ export type { ClusterFeatureRegistration, ClusterFeatureComponents } from "../re
export type { KubeObjectDetailRegistration, KubeObjectDetailComponents } from "../registries/kube-object-detail-registry"; export type { KubeObjectDetailRegistration, KubeObjectDetailComponents } from "../registries/kube-object-detail-registry";
export type { KubeObjectMenuRegistration, KubeObjectMenuComponents } from "../registries/kube-object-menu-registry"; export type { KubeObjectMenuRegistration, KubeObjectMenuComponents } from "../registries/kube-object-menu-registry";
export type { KubeObjectStatusRegistration } from "../registries/kube-object-status-registry"; export type { KubeObjectStatusRegistration } from "../registries/kube-object-status-registry";
export type { PageRegistration, PageComponents } from "../registries/page-registry"; export type { PageRegistration, RegisteredPage, PageParams, PageComponentProps, PageComponents, PageTarget } from "../registries/page-registry";
export type { PageMenuRegistration, PageMenuComponents } from "../registries/page-menu-registry"; export type { PageMenuRegistration, ClusterPageMenuRegistration, PageMenuComponents } from "../registries/page-menu-registry";
export type { StatusBarRegistration } from "../registries/status-bar-registry"; export type { StatusBarRegistration } from "../registries/status-bar-registry";

View File

@ -1,14 +1,13 @@
import type { AppPreferenceRegistration, ClusterFeatureRegistration, KubeObjectDetailRegistration, KubeObjectMenuRegistration, KubeObjectStatusRegistration, PageMenuRegistration, PageRegistration, StatusBarRegistration, } from "./registries"; import type { AppPreferenceRegistration, ClusterFeatureRegistration, ClusterPageMenuRegistration, KubeObjectDetailRegistration, KubeObjectMenuRegistration, KubeObjectStatusRegistration, PageMenuRegistration, PageRegistration, StatusBarRegistration, } from "./registries";
import type { Cluster } from "../main/cluster"; import type { Cluster } from "../main/cluster";
import { LensExtension } from "./lens-extension"; import { LensExtension } from "./lens-extension";
import { getExtensionPageUrl } from "./registries/page-registry"; import { getExtensionPageUrl } from "./registries/page-registry";
export class LensRendererExtension extends LensExtension { export class LensRendererExtension extends LensExtension {
globalPages: PageRegistration[] = []; globalPages: PageRegistration[] = [];
clusterPages: PageRegistration[] = []; clusterPages: PageRegistration[] = [];
globalPageMenus: PageMenuRegistration[] = []; globalPageMenus: PageMenuRegistration[] = [];
clusterPageMenus: PageMenuRegistration[] = []; clusterPageMenus: ClusterPageMenuRegistration[] = [];
kubeObjectStatusTexts: KubeObjectStatusRegistration[] = []; kubeObjectStatusTexts: KubeObjectStatusRegistration[] = [];
appPreferences: AppPreferenceRegistration[] = []; appPreferences: AppPreferenceRegistration[] = [];
clusterFeatures: ClusterFeatureRegistration[] = []; clusterFeatures: ClusterFeatureRegistration[] = [];

View File

@ -1,4 +1,4 @@
import { getExtensionPageUrl, globalPageRegistry } from "../page-registry"; import { getExtensionPageUrl, globalPageRegistry, PageParams } from "../page-registry";
import { LensExtension } from "../../lens-extension"; import { LensExtension } from "../../lens-extension";
import React from "react"; import React from "react";
@ -17,6 +17,16 @@ describe("getPageUrl", () => {
isBundled: false, isBundled: false,
isEnabled: true 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);
}); });
it("returns a page url for extension", () => { it("returns a page url for extension", () => {
@ -34,6 +44,24 @@ describe("getPageUrl", () => {
it("adds / prefix", () => { it("adds / prefix", () => {
expect(getExtensionPageUrl({ extensionId: ext.name, pageId: "test" })).toBe("/extension/foo-bar/test"); expect(getExtensionPageUrl({ extensionId: ext.name, 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");
});
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 });
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", });
expect(defaultPageUrl).toBe(`/extension/foo-bar/page-with-params?test1=test1-default`);
});
}); });
describe("globalPageRegistry", () => { describe("globalPageRegistry", () => {
@ -70,17 +98,17 @@ describe("globalPageRegistry", () => {
], ext); ], ext);
}); });
describe("getByPageMenuTarget", () => { describe("getByPageTarget", () => {
it("matching to first registered page without id", () => { it("matching to first registered page without id", () => {
const page = globalPageRegistry.getByPageMenuTarget({ extensionId: ext.name }); const page = globalPageRegistry.getByPageTarget({ extensionId: ext.name });
expect(page.id).toEqual(undefined); expect(page.id).toEqual(undefined);
expect(page.extensionId).toEqual(ext.name); expect(page.extensionId).toEqual(ext.name);
expect(page.routePath).toEqual(getExtensionPageUrl({ extensionId: ext.name })); expect(page.url).toEqual(getExtensionPageUrl({ extensionId: ext.name }));
}); });
it("returns matching page", () => { it("returns matching page", () => {
const page = globalPageRegistry.getByPageMenuTarget({ const page = globalPageRegistry.getByPageTarget({
pageId: "test-page", pageId: "test-page",
extensionId: ext.name extensionId: ext.name
}); });
@ -89,7 +117,7 @@ describe("globalPageRegistry", () => {
}); });
it("returns null if target not found", () => { it("returns null if target not found", () => {
const page = globalPageRegistry.getByPageMenuTarget({ const page = globalPageRegistry.getByPageTarget({
pageId: "wrong-page", pageId: "wrong-page",
extensionId: ext.name extensionId: ext.name
}); });

View File

@ -1,29 +1,34 @@
// 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 { LensExtension } from "../lens-extension";
import { rectify } from "../../common/utils";
export class BaseRegistry<T> { export class BaseRegistry<T, I = T> {
private items = observable<T>([], { deep: false }); private items = observable.map<T, I>();
getItems(): T[] { getItems(): I[] {
return this.items.toJS(); return Array.from(this.items.values());
} }
add(items: T | T[], ext?: LensExtension): () => void; // allow method overloading with required "ext"
@action @action
add(items: T | T[]) { add(items: T | T[], extension?: LensExtension) {
const itemArray = rectify(items); const itemArray = [items].flat() as T[];
this.items.push(...itemArray); itemArray.forEach(item => {
this.items.set(item, this.getRegisteredItem(item, extension));
});
return () => this.remove(...itemArray); return () => this.remove(...itemArray);
} }
// eslint-disable-next-line unused-imports/no-unused-vars-ts
protected getRegisteredItem(item: T, extension?: LensExtension): I {
return item as any;
}
@action @action
remove(...items: T[]) { remove(...items: T[]) {
items.forEach(item => { items.forEach(item => {
this.items.remove(item); // works because of {deep: false}; this.items.delete(item);
}); });
} }
} }

View File

@ -1,19 +1,13 @@
// Extensions-api -> Register page menu items // Extensions-api -> Register page menu items
import type { IconProps } from "../../renderer/components/icon"; import type { IconProps } from "../../renderer/components/icon";
import type React from "react"; import type React from "react";
import type { PageTarget, RegisteredPage } from "./page-registry";
import { action } from "mobx"; import { action } from "mobx";
import { BaseRegistry } from "./base-registry"; import { BaseRegistry } from "./base-registry";
import { LensExtension } from "../lens-extension"; import { LensExtension } from "../lens-extension";
import { RegisteredPage } from "./page-registry";
export interface PageMenuTarget<P extends object = any> {
extensionId?: string;
pageId?: string;
params?: P;
}
export interface PageMenuRegistration { export interface PageMenuRegistration {
target?: PageMenuTarget; target?: PageTarget;
title: React.ReactNode; title: React.ReactNode;
components: PageMenuComponents; components: PageMenuComponents;
} }
@ -27,9 +21,9 @@ export interface PageMenuComponents {
Icon: React.ComponentType<IconProps>; Icon: React.ComponentType<IconProps>;
} }
export class GlobalPageMenuRegistry extends BaseRegistry<PageMenuRegistration> { export class PageMenuRegistry<T extends PageMenuRegistration> extends BaseRegistry<T> {
@action @action
add(items: PageMenuRegistration[], ext: LensExtension) { add(items: T[], ext: LensExtension) {
const normalizedItems = items.map(menuItem => { const normalizedItems = items.map(menuItem => {
menuItem.target = { menuItem.target = {
extensionId: ext.name, extensionId: ext.name,
@ -43,33 +37,25 @@ export class GlobalPageMenuRegistry extends BaseRegistry<PageMenuRegistration> {
} }
} }
export class ClusterPageMenuRegistry extends BaseRegistry<ClusterPageMenuRegistration> { export class ClusterPageMenuRegistry extends PageMenuRegistry<ClusterPageMenuRegistration> {
@action
add(items: PageMenuRegistration[], ext: LensExtension) {
const normalizedItems = items.map(menuItem => {
menuItem.target = {
extensionId: ext.name,
...(menuItem.target || {}),
};
return menuItem;
});
return super.add(normalizedItems);
}
getRootItems() { getRootItems() {
return this.getItems().filter((item) => !item.parentId); return this.getItems().filter((item) => !item.parentId);
} }
getSubItems(parent: ClusterPageMenuRegistration) { getSubItems(parent: ClusterPageMenuRegistration) {
return this.getItems().filter((item) => item.parentId === parent.id && item.target.extensionId === parent.target.extensionId); return this.getItems().filter((item) => (
item.parentId === parent.id &&
item.target.extensionId === parent.target.extensionId
));
} }
getByPage(page: RegisteredPage) { getByPage({ id: pageId, extensionId }: RegisteredPage) {
return this.getItems().find((item) => item.target?.pageId == page.id && item.target?.extensionId === page.extensionId); return this.getItems().find((item) => (
item.target.pageId == pageId &&
item.target.extensionId === extensionId
));
} }
} }
export const globalPageMenuRegistry = new GlobalPageMenuRegistry(); export const globalPageMenuRegistry = new PageMenuRegistry();
export const clusterPageMenuRegistry = new ClusterPageMenuRegistry(); export const clusterPageMenuRegistry = new ClusterPageMenuRegistry();

View File

@ -1,93 +1,120 @@
// Extensions-api -> Custom page registration // Extensions-api -> Custom page registration
import type { PageMenuTarget } from "./page-menu-registry";
import type React from "react"; import React from "react";
import path from "path"; import { observer } from "mobx-react";
import { action } from "mobx";
import { compile } from "path-to-regexp";
import { BaseRegistry } from "./base-registry"; import { BaseRegistry } from "./base-registry";
import { LensExtension, sanitizeExtensionName } from "../lens-extension"; import { LensExtension, sanitizeExtensionName } from "../lens-extension";
import logger from "../../main/logger"; import { PageParam, PageParamInit } from "../../renderer/navigation/page-param";
import { rectify } from "../../common/utils"; import { createPageParam } from "../../renderer/navigation/helpers";
export interface PageRegistration { export interface PageRegistration {
/** /**
* Page ID or additional route path to indicate uniqueness within current extension registered pages * Page ID, part of extension's page url, must be unique within same extension
* Might contain special url placeholders, e.g. "/users/:userId?" (? - marks as optional param)
* When not provided, first registered page without "id" would be used for page-menus without target.pageId for same extension * When not provided, first registered page without "id" would be used for page-menus without target.pageId for same extension
*/ */
id?: string; id?: string;
/** params?: PageParams<string | ExtensionPageParamInit>;
* Strict route matching to provided page-id, read also: https://reactrouter.com/web/api/NavLink/exact-bool
* In case when more than one page registered at same extension "pageId" is required to identify different pages,
* It might be useful to provide `exact: true` in some cases to avoid overlapping routes.
* Without {exact:true} second page never matches since first page-id/route already includes partial route.
* @example const pages = [
* {id: "/users", exact: true},
* {id: "/users/:userId?"}
* ]
* Pro-tip: registering pages in opposite order will make same effect without "exact".
*/
exact?: boolean;
components: PageComponents; components: PageComponents;
} }
export interface RegisteredPage extends PageRegistration { // exclude "name" field since provided as key in page.params
extensionId: string; // required for compiling registered page to url with page-menu-target to compare export type ExtensionPageParamInit = Omit<PageParamInit, "name" | "isSystem">;
routePath: string; // full route-path to registered extension page
}
export interface PageComponents { export interface PageComponents {
Page: React.ComponentType<any>; Page: React.ComponentType<any>;
} }
export function getExtensionPageUrl<P extends object>({ extensionId, pageId = "", params }: PageMenuTarget<P>): string { export interface PageTarget<P = PageParams> {
const extensionBaseUrl = compile(`/extension/:name`)({ extensionId?: string;
name: sanitizeExtensionName(extensionId), // compile only with extension-id first and define base path pageId?: string;
}); params?: P;
const extPageRoutePath = path.posix.join(extensionBaseUrl, pageId);
if (params) {
return compile(extPageRoutePath)(params); // might throw error when required params not passed
}
return extPageRoutePath;
} }
export class PageRegistry extends BaseRegistry<RegisteredPage> { export interface PageParams<V = any> {
@action [paramName: string]: V;
add(items: PageRegistration | PageRegistration[], ext: LensExtension) { }
const itemArray = rectify(items);
let registeredPages: RegisteredPage[] = [];
try { export interface PageComponentProps<P extends PageParams = {}> {
registeredPages = itemArray.map(page => ({ params?: {
...page, [N in keyof P]: PageParam<P[N]>;
extensionId: ext.name, }
routePath: getExtensionPageUrl({ extensionId: ext.name, pageId: page.id }), }
}));
} catch (err) { export interface RegisteredPage {
logger.error(`[EXTENSION]: page-registration failed`, { id: string;
items, extensionId: string;
extension: ext, url: string; // registered extension's page URL (without page params)
error: String(err), params: PageParams<PageParam>; // normalized params
components: PageComponents; // normalized components
}
export function getExtensionPageUrl(target: PageTarget): string {
const { extensionId, pageId = "", params: targetParams = {} } = target;
const pagePath = ["/extension", sanitizeExtensionName(extensionId), 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);
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 super.add(registeredPages); 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 });
return {
id: pageId, extensionId, params, components, url,
};
} }
getUrl<P extends object>({ extensionId, id: pageId }: RegisteredPage, params?: P) { protected normalizeComponents(components: PageComponents, params?: PageParams<PageParam>): PageComponents {
return getExtensionPageUrl({ extensionId, pageId, params }); if (params) {
const { Page } = components;
components.Page = observer((props: object) => React.createElement(Page, { params, ...props }));
} }
getByPageMenuTarget(target: PageMenuTarget = {}): RegisteredPage | null { return components;
const targetUrl = getExtensionPageUrl(target); }
return this.getItems().find(({ id: pageId, extensionId }) => { protected normalizeParams(params?: PageParams<string | ExtensionPageParamInit>): PageParams<PageParam> {
const pageUrl = getExtensionPageUrl({ extensionId, pageId, params: target.params }); // compiled with provided params if (!params) {
return;
}
Object.entries(params).forEach(([name, value]) => {
const paramInit: PageParamInit = typeof value === "object"
? { name, ...value }
: { name, defaultValue: value };
return targetUrl === pageUrl; params[paramInit.name] = createPageParam(paramInit);
}) || null; });
return params as PageParams<PageParam>;
}
getByPageTarget(target: PageTarget): RegisteredPage | null {
return this.getItems().find(page => page.extensionId === target.extensionId && page.id === target.pageId) || null;
} }
} }

View File

@ -1,3 +1,12 @@
export { navigate } from "../../renderer/navigation"; import { PageParam, PageParamInit } from "../../renderer/navigation/page-param";
export { hideDetails, showDetails, getDetailsUrl } from "../../renderer/navigation"; import { navigation } from "../../renderer/navigation";
export type { PageParamInit, PageParam } from "../../renderer/navigation/page-param";
export { navigate, isActiveRoute } from "../../renderer/navigation/helpers";
export { hideDetails, showDetails, getDetailsUrl } from "../../renderer/components/kube-object/kube-object-details";
export { IURLParams } from "../../common/utils/buildUrl"; export { IURLParams } from "../../common/utils/buildUrl";
// exporting to extensions-api version of helper without `isSystem` flag
export function createPageParam<V = string>(init: PageParamInit<V>) {
return new PageParam<V>(init, navigation);
}

View File

@ -50,8 +50,8 @@ export class ApiManager {
}); });
} }
getStore(api: string | KubeApi): KubeObjectStore { getStore<S extends KubeObjectStore>(api: string | KubeApi): S {
return this.stores.get(this.resolveApi(api)); return this.stores.get(this.resolveApi(api)) as S;
} }
} }

View File

@ -20,13 +20,13 @@ import { Button } from "../button";
import { releaseStore } from "./release.store"; import { releaseStore } from "./release.store";
import { Notifications } from "../notifications"; import { Notifications } from "../notifications";
import { createUpgradeChartTab } from "../dock/upgrade-chart.store"; import { createUpgradeChartTab } from "../dock/upgrade-chart.store";
import { getDetailsUrl } from "../../navigation";
import { _i18n } from "../../i18n"; import { _i18n } from "../../i18n";
import { themeStore } from "../../theme.store"; import { themeStore } from "../../theme.store";
import { apiManager } from "../../api/api-manager"; import { apiManager } from "../../api/api-manager";
import { SubTitle } from "../layout/sub-title"; import { SubTitle } from "../layout/sub-title";
import { secretsStore } from "../+config-secrets/secrets.store"; import { secretsStore } from "../+config-secrets/secrets.store";
import { Secret } from "../../api/endpoints"; import { Secret } from "../../api/endpoints";
import { getDetailsUrl } from "../kube-object";
interface Props { interface Props {
release: HelmRelease; release: HelmRelease;
@ -161,10 +161,7 @@ export class ReleaseDetails extends Component<Props> {
const name = item.getName(); const name = item.getName();
const namespace = item.getNs(); const namespace = item.getNs();
const api = apiManager.getApi(item.metadata.selfLink); const api = apiManager.getApi(item.metadata.selfLink);
const detailsUrl = api ? getDetailsUrl(api.getUrl({ const detailsUrl = api ? getDetailsUrl(api.getUrl({ name, namespace })) : "";
name,
namespace,
})) : "";
return ( return (
<TableRow key={item.getId()}> <TableRow key={item.getId()}>

View File

@ -4,12 +4,12 @@ import { Trans } from "@lingui/macro";
import { TabLayout, TabLayoutRoute } from "../layout/tab-layout"; import { TabLayout, TabLayoutRoute } from "../layout/tab-layout";
import { HelmCharts, helmChartsRoute, helmChartsURL } from "../+apps-helm-charts"; import { HelmCharts, helmChartsRoute, helmChartsURL } from "../+apps-helm-charts";
import { HelmReleases, releaseRoute, releaseURL } from "../+apps-releases"; import { HelmReleases, releaseRoute, releaseURL } from "../+apps-releases";
import { namespaceStore } from "../+namespaces/namespace.store"; import { namespaceUrlParam } from "../+namespaces/namespace.store";
@observer @observer
export class Apps extends React.Component { export class Apps extends React.Component {
static get tabRoutes(): TabLayoutRoute[] { static get tabRoutes(): TabLayoutRoute[] {
const query = namespaceStore.getContextParams(); const query = namespaceUrlParam.toObjectParam();
return [ return [
{ {

View File

@ -10,11 +10,11 @@ import { Table, TableCell, TableHead, TableRow } from "../table";
import { nodesStore } from "../+nodes/nodes.store"; import { nodesStore } from "../+nodes/nodes.store";
import { eventStore } from "../+events/event.store"; import { eventStore } from "../+events/event.store";
import { autobind, cssNames, prevDefault } from "../../utils"; import { autobind, cssNames, prevDefault } from "../../utils";
import { getSelectedDetails, showDetails } from "../../navigation";
import { ItemObject } from "../../item.store"; import { ItemObject } from "../../item.store";
import { Spinner } from "../spinner"; import { Spinner } from "../spinner";
import { themeStore } from "../../theme.store"; import { themeStore } from "../../theme.store";
import { lookupApiLink } from "../../api/kube-api"; import { lookupApiLink } from "../../api/kube-api";
import { kubeSelectedUrlParam, showDetails } from "../kube-object";
interface Props { interface Props {
className?: string; className?: string;
@ -85,7 +85,7 @@ export class ClusterIssues extends React.Component<Props> {
<TableRow <TableRow
key={getId()} key={getId()}
sortItem={warning} sortItem={warning}
selected={selfLink === getSelectedDetails()} selected={selfLink === kubeSelectedUrlParam.get()}
onClick={prevDefault(() => showDetails(selfLink))} onClick={prevDefault(() => showDetails(selfLink))}
> >
<TableCell className="message"> <TableCell className="message">

View File

@ -5,13 +5,12 @@ import { observer } from "mobx-react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { DrawerItem, DrawerTitle } from "../drawer"; import { DrawerItem, DrawerTitle } from "../drawer";
import { Badge } from "../badge"; import { Badge } from "../badge";
import { KubeObjectDetailsProps } from "../kube-object"; import { KubeObjectDetailsProps, getDetailsUrl } from "../kube-object";
import { cssNames } from "../../utils"; import { cssNames } from "../../utils";
import { HorizontalPodAutoscaler, HpaMetricType, IHpaMetric } from "../../api/endpoints/hpa.api"; import { HorizontalPodAutoscaler, HpaMetricType, IHpaMetric } from "../../api/endpoints/hpa.api";
import { KubeEventDetails } from "../+events/kube-event-details"; import { KubeEventDetails } from "../+events/kube-event-details";
import { Trans } from "@lingui/macro"; import { Trans } from "@lingui/macro";
import { Table, TableCell, TableHead, TableRow } from "../table"; import { Table, TableCell, TableHead, TableRow } from "../table";
import { getDetailsUrl } from "../../navigation";
import { lookupApiLink } from "../../api/kube-api"; import { lookupApiLink } from "../../api/kube-api";
import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";

View File

@ -17,8 +17,8 @@ import { Icon } from "../icon";
import { IKubeObjectMetadata } from "../../api/kube-object"; import { IKubeObjectMetadata } from "../../api/kube-object";
import { base64 } from "../../utils"; import { base64 } from "../../utils";
import { Notifications } from "../notifications"; import { Notifications } from "../notifications";
import { showDetails } from "../../navigation";
import upperFirst from "lodash/upperFirst"; import upperFirst from "lodash/upperFirst";
import { showDetails } from "../kube-object";
interface Props extends Partial<DialogProps> { interface Props extends Partial<DialogProps> {
} }

View File

@ -4,7 +4,7 @@ import { Trans } from "@lingui/macro";
import { TabLayout, TabLayoutRoute } from "../layout/tab-layout"; import { TabLayout, TabLayoutRoute } from "../layout/tab-layout";
import { ConfigMaps, configMapsRoute, configMapsURL } from "../+config-maps"; import { ConfigMaps, configMapsRoute, configMapsURL } from "../+config-maps";
import { Secrets, secretsRoute, secretsURL } from "../+config-secrets"; import { Secrets, secretsRoute, secretsURL } from "../+config-secrets";
import { namespaceStore } from "../+namespaces/namespace.store"; import { namespaceUrlParam } from "../+namespaces/namespace.store";
import { resourceQuotaRoute, ResourceQuotas, resourceQuotaURL } from "../+config-resource-quotas"; import { resourceQuotaRoute, ResourceQuotas, resourceQuotaURL } from "../+config-resource-quotas";
import { pdbRoute, pdbURL, PodDisruptionBudgets } from "../+config-pod-disruption-budgets"; import { pdbRoute, pdbURL, PodDisruptionBudgets } from "../+config-pod-disruption-budgets";
import { HorizontalPodAutoscalers, hpaRoute, hpaURL } from "../+config-autoscalers"; import { HorizontalPodAutoscalers, hpaRoute, hpaURL } from "../+config-autoscalers";
@ -13,7 +13,7 @@ import { isAllowedResource } from "../../../common/rbac";
@observer @observer
export class Config extends React.Component { export class Config extends React.Component {
static get tabRoutes(): TabLayoutRoute[] { static get tabRoutes(): TabLayoutRoute[] {
const query = namespaceStore.getContextParams(); const query = namespaceUrlParam.toObjectParam();
const routes: TabLayoutRoute[] = []; const routes: TabLayoutRoute[] = [];
if (isAllowedResource("configmaps")) { if (isAllowedResource("configmaps")) {

View File

@ -10,9 +10,16 @@ import { KubeObjectListLayout } from "../kube-object";
import { crdStore } from "./crd.store"; import { crdStore } from "./crd.store";
import { CustomResourceDefinition } from "../../api/endpoints/crd.api"; import { CustomResourceDefinition } from "../../api/endpoints/crd.api";
import { Select, SelectOption } from "../select"; import { Select, SelectOption } from "../select";
import { navigation, setQueryParams } from "../../navigation"; import { createPageParam } from "../../navigation";
import { Icon } from "../icon"; import { Icon } from "../icon";
export const crdGroupsUrlParam = createPageParam<string[]>({
name: "groups",
multiValues: true,
isSystem: true,
defaultValue: [],
});
enum sortBy { enum sortBy {
kind = "kind", kind = "kind",
group = "group", group = "group",
@ -23,17 +30,19 @@ enum sortBy {
@observer @observer
export class CrdList extends React.Component { export class CrdList extends React.Component {
@computed get groups() { @computed get groups(): string[] {
return navigation.searchParams.getAsArray("groups"); return crdGroupsUrlParam.get();
} }
onGroupChange(group: string) { onSelectGroup(group: string) {
const groups = [...this.groups]; const groups = new Set(this.groups);
const index = groups.findIndex(item => item == group);
if (index !== -1) groups.splice(index, 1); if (groups.has(group)) {
else groups.push(group); groups.delete(group); // toggle selection
setQueryParams({ groups }); } else {
groups.add(group);
}
crdGroupsUrlParam.set(Array.from(groups));
} }
render() { render() {
@ -71,7 +80,7 @@ export class CrdList extends React.Component {
className="group-select" className="group-select"
placeholder={placeholder} placeholder={placeholder}
options={Object.keys(crdStore.groups)} options={Object.keys(crdStore.groups)}
onChange={({ value: group }: SelectOption) => this.onGroupChange(group)} onChange={({ value: group }: SelectOption) => this.onSelectGroup(group)}
controlShouldRenderValue={false} controlShouldRenderValue={false}
formatOptionLabel={({ value: group }: SelectOption) => { formatOptionLabel={({ value: group }: SelectOption) => {
const isSelected = selectedGroups.includes(group); const isSelected = selectedGroups.includes(group);

View File

@ -6,10 +6,9 @@ import { Trans } from "@lingui/macro";
import { DrawerItem, DrawerTitle } from "../drawer"; import { DrawerItem, DrawerTitle } from "../drawer";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { KubeObjectDetailsProps } from "../kube-object"; import { KubeObjectDetailsProps, getDetailsUrl } from "../kube-object";
import { KubeEvent } from "../../api/endpoints/events.api"; import { KubeEvent } from "../../api/endpoints/events.api";
import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { getDetailsUrl } from "../../navigation";
import { Table, TableCell, TableHead, TableRow } from "../table"; import { Table, TableCell, TableHead, TableRow } from "../table";
import { lookupApiLink } from "../../api/kube-api"; import { lookupApiLink } from "../../api/kube-api";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";

View File

@ -4,14 +4,13 @@ import React, { Fragment } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { TabLayout } from "../layout/tab-layout"; import { TabLayout } from "../layout/tab-layout";
import { eventStore } from "./event.store"; import { eventStore } from "./event.store";
import { KubeObjectListLayout, KubeObjectListLayoutProps } from "../kube-object"; import { KubeObjectListLayout, KubeObjectListLayoutProps, getDetailsUrl } from "../kube-object";
import { Trans } from "@lingui/macro"; import { Trans } from "@lingui/macro";
import { KubeEvent } from "../../api/endpoints/events.api"; import { KubeEvent } from "../../api/endpoints/events.api";
import { Tooltip } from "../tooltip"; import { Tooltip } from "../tooltip";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { cssNames, IClassName, stopPropagation } from "../../utils"; import { cssNames, IClassName, stopPropagation } from "../../utils";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { getDetailsUrl } from "../../navigation";
import { lookupApiLink } from "../../api/kube-api"; import { lookupApiLink } from "../../api/kube-api";
enum sortBy { enum sortBy {

View File

@ -7,9 +7,8 @@ import { Trans } from "@lingui/macro";
import { DrawerItem } from "../drawer"; import { DrawerItem } from "../drawer";
import { cssNames } from "../../utils"; import { cssNames } from "../../utils";
import { Namespace } from "../../api/endpoints"; import { Namespace } from "../../api/endpoints";
import { KubeObjectDetailsProps } from "../kube-object"; import { getDetailsUrl, KubeObjectDetailsProps } from "../kube-object";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { getDetailsUrl } from "../../navigation";
import { Spinner } from "../spinner"; import { Spinner } from "../spinner";
import { resourceQuotaStore } from "../+config-resource-quotas/resource-quotas.store"; import { resourceQuotaStore } from "../+config-resource-quotas/resource-quotas.store";
import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { KubeObjectMeta } from "../kube-object/kube-object-meta";

View File

@ -1,44 +1,52 @@
import { action, observable, reaction } from "mobx"; import { action, comparer, observable, reaction } from "mobx";
import { autobind, createStorage } from "../../utils"; import { autobind, createStorage } from "../../utils";
import { KubeObjectStore } from "../../kube-object.store"; import { KubeObjectStore } from "../../kube-object.store";
import { Namespace, namespacesApi } from "../../api/endpoints"; import { Namespace, namespacesApi } from "../../api/endpoints";
import { IQueryParams, navigation, setQueryParams } from "../../navigation"; import { createPageParam } from "../../navigation";
import { apiManager } from "../../api/api-manager"; import { apiManager } from "../../api/api-manager";
import { isAllowedResource } from "../../../common/rbac"; import { isAllowedResource } from "../../../common/rbac";
import { getHostedCluster } from "../../../common/cluster-store"; import { getHostedCluster } from "../../../common/cluster-store";
const storage = createStorage<string[]>("context_namespaces", []);
export const namespaceUrlParam = createPageParam<string[]>({
name: "namespaces",
isSystem: true,
multiValues: true,
get defaultValue() {
return storage.get(); // initial namespaces coming from URL or local-storage (default)
}
});
@autobind() @autobind()
export class NamespaceStore extends KubeObjectStore<Namespace> { export class NamespaceStore extends KubeObjectStore<Namespace> {
api = namespacesApi; api = namespacesApi;
contextNs = observable.array<string>(); contextNs = observable.array<string>();
protected storage = createStorage<string[]>("context_ns", this.contextNs);
get initNamespaces() {
const fromUrl = navigation.searchParams.getAsArray("namespaces");
return fromUrl.length ? fromUrl : this.storage.get();
}
constructor() { constructor() {
super(); super();
this.init();
}
// restore context namespaces private init() {
const { initNamespaces: namespaces } = this; this.setContext(this.initNamespaces);
this.setContext(namespaces); return reaction(() => this.contextNs.toJS(), namespaces => {
this.updateUrl(namespaces); storage.set(namespaces); // save to local-storage
namespaceUrlParam.set(namespaces, { replaceHistory: true }); // update url
// sync with local-storage & url-search-params }, {
reaction(() => this.contextNs.toJS(), namespaces => { fireImmediately: true,
this.storage.set(namespaces); equals: comparer.identity,
this.updateUrl(namespaces);
}); });
} }
getContextParams(): Partial<IQueryParams> { get initNamespaces() {
return namespaceUrlParam.get();
}
getContextParams() {
return { return {
namespaces: this.contextNs namespaces: this.contextNs.toJS(),
}; };
} }
@ -47,16 +55,12 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
// if user has given static list of namespaces let's not start watches because watch adds stuff that's not wanted // if user has given static list of namespaces let's not start watches because watch adds stuff that's not wanted
if (accessibleNamespaces.length > 0) { if (accessibleNamespaces.length > 0) {
return () => { return; }; return Function; // no-op
} }
return super.subscribe(apis); return super.subscribe(apis);
} }
protected updateUrl(namespaces: string[]) {
setQueryParams({ namespaces }, { replace: true });
}
protected async loadItems(namespaces?: string[]) { protected async loadItems(namespaces?: string[]) {
if (!isAllowedResource("namespaces")) { if (!isAllowedResource("namespaces")) {
if (namespaces) return namespaces.map(this.getDummyNamespace); if (namespaces) return namespaces.map(this.getDummyNamespace);
@ -84,6 +88,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
}); });
} }
@action
setContext(namespaces: string[]) { setContext(namespaces: string[]) {
this.contextNs.replace(namespaces); this.contextNs.replace(namespaces);
} }
@ -94,6 +99,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
return context.every(namespace => this.contextNs.includes(namespace)); return context.every(namespace => this.contextNs.includes(namespace));
} }
@action
toggleContext(namespace: string) { toggleContext(namespace: string) {
if (this.hasContext(namespace)) this.contextNs.remove(namespace); if (this.hasContext(namespace)) this.contextNs.remove(namespace);
else this.contextNs.push(namespace); else this.contextNs.push(namespace);
@ -105,6 +111,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
this.contextNs.clear(); this.contextNs.clear();
} }
@action
async remove(item: Namespace) { async remove(item: Namespace) {
await super.remove(item); await super.remove(item);
this.contextNs.remove(item.getName()); this.contextNs.remove(item.getName());

View File

@ -7,8 +7,8 @@ import { Trans } from "@lingui/macro";
import { Table, TableCell, TableHead, TableRow } from "../table"; import { Table, TableCell, TableHead, TableRow } from "../table";
import { autobind } from "../../utils"; import { autobind } from "../../utils";
import { lookupApiLink } from "../../api/kube-api"; import { lookupApiLink } from "../../api/kube-api";
import { getDetailsUrl } from "../../navigation";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { getDetailsUrl } from "../kube-object";
interface Props { interface Props {
subset: EndpointSubset; subset: EndpointSubset;

View File

@ -3,10 +3,10 @@ import { observer } from "mobx-react";
import React from "react"; import React from "react";
import { Table, TableHead, TableCell, TableRow } from "../table"; import { Table, TableHead, TableCell, TableRow } from "../table";
import { prevDefault } from "../../utils"; import { prevDefault } from "../../utils";
import { showDetails } from "../../navigation";
import { Trans } from "@lingui/macro"; import { Trans } from "@lingui/macro";
import { endpointStore } from "../+network-endpoints/endpoints.store"; import { endpointStore } from "../+network-endpoints/endpoints.store";
import { Spinner } from "../spinner"; import { Spinner } from "../spinner";
import { showDetails } from "../kube-object";
interface Props { interface Props {
endpoint: KubeObject; endpoint: KubeObject;

View File

@ -8,13 +8,13 @@ import { Services, servicesRoute, servicesURL } from "../+network-services";
import { endpointRoute, Endpoints, endpointURL } from "../+network-endpoints"; import { endpointRoute, Endpoints, endpointURL } from "../+network-endpoints";
import { Ingresses, ingressRoute, ingressURL } from "../+network-ingresses"; import { Ingresses, ingressRoute, ingressURL } from "../+network-ingresses";
import { NetworkPolicies, networkPoliciesRoute, networkPoliciesURL } from "../+network-policies"; import { NetworkPolicies, networkPoliciesRoute, networkPoliciesURL } from "../+network-policies";
import { namespaceStore } from "../+namespaces/namespace.store"; import { namespaceUrlParam } from "../+namespaces/namespace.store";
import { isAllowedResource } from "../../../common/rbac"; import { isAllowedResource } from "../../../common/rbac";
@observer @observer
export class Network extends React.Component { export class Network extends React.Component {
static get tabRoutes(): TabLayoutRoute[] { static get tabRoutes(): TabLayoutRoute[] {
const query = namespaceStore.getContextParams(); const query = namespaceUrlParam.toObjectParam();
const routes: TabLayoutRoute[] = []; const routes: TabLayoutRoute[] = [];
if (isAllowedResource("services")) { if (isAllowedResource("services")) {

View File

@ -10,13 +10,11 @@ import { podsStore } from "../+workloads-pods/pods.store";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { KubeEventDetails } from "../+events/kube-event-details"; import { KubeEventDetails } from "../+events/kube-event-details";
import { volumeClaimStore } from "./volume-claim.store"; import { volumeClaimStore } from "./volume-claim.store";
import { getDetailsUrl } from "../../navigation";
import { ResourceMetrics } from "../resource-metrics"; import { ResourceMetrics } from "../resource-metrics";
import { VolumeClaimDiskChart } from "./volume-claim-disk-chart"; import { VolumeClaimDiskChart } from "./volume-claim-disk-chart";
import { KubeObjectDetailsProps } from "../kube-object"; import { getDetailsUrl, KubeObjectDetailsProps, KubeObjectMeta } from "../kube-object";
import { PersistentVolumeClaim } from "../../api/endpoints"; import { PersistentVolumeClaim } from "../../api/endpoints";
import { _i18n } from "../../i18n"; import { _i18n } from "../../i18n";
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
interface Props extends KubeObjectDetailsProps<PersistentVolumeClaim> { interface Props extends KubeObjectDetailsProps<PersistentVolumeClaim> {

View File

@ -7,11 +7,10 @@ import { Trans } from "@lingui/macro";
import { volumeClaimStore } from "./volume-claim.store"; import { volumeClaimStore } from "./volume-claim.store";
import { PersistentVolumeClaim } from "../../api/endpoints/persistent-volume-claims.api"; import { PersistentVolumeClaim } from "../../api/endpoints/persistent-volume-claims.api";
import { podsStore } from "../+workloads-pods/pods.store"; import { podsStore } from "../+workloads-pods/pods.store";
import { KubeObjectListLayout } from "../kube-object"; import { getDetailsUrl, KubeObjectListLayout } from "../kube-object";
import { IVolumeClaimsRouteParams } from "./volume-claims.route"; import { IVolumeClaimsRouteParams } from "./volume-claims.route";
import { unitsToBytes } from "../../utils/convertMemory"; import { unitsToBytes } from "../../utils/convertMemory";
import { stopPropagation } from "../../utils"; import { stopPropagation } from "../../utils";
import { getDetailsUrl } from "../../navigation";
import { storageClassApi } from "../../api/endpoints"; import { storageClassApi } from "../../api/endpoints";
import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { KubeObjectStatusIcon } from "../kube-object-status-icon";

View File

@ -8,9 +8,8 @@ import { observer } from "mobx-react";
import { DrawerItem, DrawerTitle } from "../drawer"; import { DrawerItem, DrawerTitle } from "../drawer";
import { Badge } from "../badge"; import { Badge } from "../badge";
import { KubeEventDetails } from "../+events/kube-event-details"; import { KubeEventDetails } from "../+events/kube-event-details";
import { getDetailsUrl } from "../../navigation";
import { PersistentVolume, pvcApi } from "../../api/endpoints"; import { PersistentVolume, pvcApi } from "../../api/endpoints";
import { KubeObjectDetailsProps } from "../kube-object"; import { getDetailsUrl, KubeObjectDetailsProps } from "../kube-object";
import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";

View File

@ -5,10 +5,9 @@ import { observer } from "mobx-react";
import { Trans } from "@lingui/macro"; import { Trans } from "@lingui/macro";
import { Link, RouteComponentProps } from "react-router-dom"; import { Link, RouteComponentProps } from "react-router-dom";
import { PersistentVolume } from "../../api/endpoints/persistent-volume.api"; import { PersistentVolume } from "../../api/endpoints/persistent-volume.api";
import { KubeObjectListLayout } from "../kube-object"; import { getDetailsUrl, KubeObjectListLayout } from "../kube-object";
import { IVolumesRouteParams } from "./volumes.route"; import { IVolumesRouteParams } from "./volumes.route";
import { stopPropagation } from "../../utils"; import { stopPropagation } from "../../utils";
import { getDetailsUrl } from "../../navigation";
import { volumesStore } from "./volumes.store"; import { volumesStore } from "./volumes.store";
import { pvcApi, storageClassApi } from "../../api/endpoints"; import { pvcApi, storageClassApi } from "../../api/endpoints";
import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { KubeObjectStatusIcon } from "../kube-object-status-icon";

View File

@ -7,14 +7,14 @@ import { TabLayout, TabLayoutRoute } from "../layout/tab-layout";
import { PersistentVolumes, volumesRoute, volumesURL } from "../+storage-volumes"; import { PersistentVolumes, volumesRoute, volumesURL } from "../+storage-volumes";
import { StorageClasses, storageClassesRoute, storageClassesURL } from "../+storage-classes"; import { StorageClasses, storageClassesRoute, storageClassesURL } from "../+storage-classes";
import { PersistentVolumeClaims, volumeClaimsRoute, volumeClaimsURL } from "../+storage-volume-claims"; import { PersistentVolumeClaims, volumeClaimsRoute, volumeClaimsURL } from "../+storage-volume-claims";
import { namespaceStore } from "../+namespaces/namespace.store"; import { namespaceUrlParam } from "../+namespaces/namespace.store";
import { isAllowedResource } from "../../../common/rbac"; import { isAllowedResource } from "../../../common/rbac";
@observer @observer
export class Storage extends React.Component { export class Storage extends React.Component {
static get tabRoutes() { static get tabRoutes() {
const tabRoutes: TabLayoutRoute[] = []; const tabRoutes: TabLayoutRoute[] = [];
const query = namespaceStore.getContextParams(); const query = namespaceUrlParam.toObjectParam();
tabRoutes.push({ tabRoutes.push({
title: <Trans>Persistent Volume Claims</Trans>, title: <Trans>Persistent Volume Claims</Trans>,

View File

@ -16,11 +16,11 @@ import { NamespaceSelect } from "../+namespaces/namespace-select";
import { Checkbox } from "../checkbox"; import { Checkbox } from "../checkbox";
import { KubeObject } from "../../api/kube-object"; import { KubeObject } from "../../api/kube-object";
import { Notifications } from "../notifications"; import { Notifications } from "../notifications";
import { showDetails } from "../../navigation";
import { rolesStore } from "../+user-management-roles/roles.store"; import { rolesStore } from "../+user-management-roles/roles.store";
import { namespaceStore } from "../+namespaces/namespace.store"; import { namespaceStore } from "../+namespaces/namespace.store";
import { serviceAccountsStore } from "../+user-management-service-accounts/service-accounts.store"; import { serviceAccountsStore } from "../+user-management-service-accounts/service-accounts.store";
import { roleBindingsStore } from "./role-bindings.store"; import { roleBindingsStore } from "./role-bindings.store";
import { showDetails } from "../kube-object";
interface BindingSelectOption extends SelectOption { interface BindingSelectOption extends SelectOption {
value: string; // binding name value: string; // binding name

View File

@ -10,7 +10,7 @@ import { Wizard, WizardStep } from "../wizard";
import { Notifications } from "../notifications"; import { Notifications } from "../notifications";
import { rolesStore } from "./roles.store"; import { rolesStore } from "./roles.store";
import { Input } from "../input"; import { Input } from "../input";
import { showDetails } from "../../navigation"; import { showDetails } from "../kube-object";
interface Props extends Partial<DialogProps> { interface Props extends Partial<DialogProps> {
} }

View File

@ -13,7 +13,7 @@ import { Input } from "../input";
import { systemName } from "../input/input_validators"; import { systemName } from "../input/input_validators";
import { NamespaceSelect } from "../+namespaces/namespace-select"; import { NamespaceSelect } from "../+namespaces/namespace-select";
import { Notifications } from "../notifications"; import { Notifications } from "../notifications";
import { showDetails } from "../../navigation"; import { showDetails } from "../kube-object";
interface Props extends Partial<DialogProps> { interface Props extends Partial<DialogProps> {
} }

View File

@ -11,8 +11,7 @@ import { secretsStore } from "../+config-secrets/secrets.store";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Secret, ServiceAccount } from "../../api/endpoints"; import { Secret, ServiceAccount } from "../../api/endpoints";
import { KubeEventDetails } from "../+events/kube-event-details"; import { KubeEventDetails } from "../+events/kube-event-details";
import { getDetailsUrl } from "../../navigation"; import { getDetailsUrl, KubeObjectDetailsProps } from "../kube-object";
import { KubeObjectDetailsProps } from "../kube-object";
import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";

View File

@ -7,7 +7,7 @@ import { Roles } from "../+user-management-roles";
import { RoleBindings } from "../+user-management-roles-bindings"; import { RoleBindings } from "../+user-management-roles-bindings";
import { ServiceAccounts } from "../+user-management-service-accounts"; import { ServiceAccounts } from "../+user-management-service-accounts";
import { roleBindingsRoute, roleBindingsURL, rolesRoute, rolesURL, serviceAccountsRoute, serviceAccountsURL } from "./user-management.route"; import { roleBindingsRoute, roleBindingsURL, rolesRoute, rolesURL, serviceAccountsRoute, serviceAccountsURL } from "./user-management.route";
import { namespaceStore } from "../+namespaces/namespace.store"; import { namespaceUrlParam } from "../+namespaces/namespace.store";
import { PodSecurityPolicies, podSecurityPoliciesRoute, podSecurityPoliciesURL } from "../+pod-security-policies"; import { PodSecurityPolicies, podSecurityPoliciesRoute, podSecurityPoliciesURL } from "../+pod-security-policies";
import { isAllowedResource } from "../../../common/rbac"; import { isAllowedResource } from "../../../common/rbac";
@ -15,7 +15,7 @@ import { isAllowedResource } from "../../../common/rbac";
export class UserManagement extends React.Component { export class UserManagement extends React.Component {
static get tabRoutes() { static get tabRoutes() {
const tabRoutes: TabLayoutRoute[] = []; const tabRoutes: TabLayoutRoute[] = [];
const query = namespaceStore.getContextParams(); const query = namespaceUrlParam.toObjectParam();
tabRoutes.push( tabRoutes.push(
{ {

View File

@ -10,8 +10,7 @@ import { jobStore } from "../+workloads-jobs/job.store";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { KubeEventDetails } from "../+events/kube-event-details"; import { KubeEventDetails } from "../+events/kube-event-details";
import { cronJobStore } from "./cronjob.store"; import { cronJobStore } from "./cronjob.store";
import { getDetailsUrl } from "../../navigation"; import { getDetailsUrl, KubeObjectDetailsProps } from "../kube-object";
import { KubeObjectDetailsProps } from "../kube-object";
import { CronJob, Job } from "../../api/endpoints"; import { CronJob, Job } from "../../api/endpoints";
import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";

View File

@ -13,8 +13,7 @@ import { PodDetailsAffinities } from "../+workloads-pods/pod-details-affinities"
import { KubeEventDetails } from "../+events/kube-event-details"; import { KubeEventDetails } from "../+events/kube-event-details";
import { podsStore } from "../+workloads-pods/pods.store"; import { podsStore } from "../+workloads-pods/pods.store";
import { jobStore } from "./job.store"; import { jobStore } from "./job.store";
import { getDetailsUrl } from "../../navigation"; import { getDetailsUrl, KubeObjectDetailsProps } from "../kube-object";
import { KubeObjectDetailsProps } from "../kube-object";
import { Job } from "../../api/endpoints"; import { Job } from "../../api/endpoints";
import { PodDetailsList } from "../+workloads-pods/pod-details-list"; import { PodDetailsList } from "../+workloads-pods/pod-details-list";
import { lookupApiLink } from "../../api/kube-api"; import { lookupApiLink } from "../../api/kube-api";

View File

@ -2,6 +2,7 @@ import "./pod-details-list.scss";
import React from "react"; import React from "react";
import kebabCase from "lodash/kebabCase"; import kebabCase from "lodash/kebabCase";
import { reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react"; import { disposeOnUnmount, observer } from "mobx-react";
import { Trans } from "@lingui/macro"; import { Trans } from "@lingui/macro";
import { podsStore } from "./pods.store"; import { podsStore } from "./pods.store";
@ -10,11 +11,10 @@ import { autobind, bytesToUnits, cssNames, interval, prevDefault } from "../../u
import { LineProgress } from "../line-progress"; import { LineProgress } from "../line-progress";
import { KubeObject } from "../../api/kube-object"; import { KubeObject } from "../../api/kube-object";
import { Table, TableCell, TableHead, TableRow } from "../table"; import { Table, TableCell, TableHead, TableRow } from "../table";
import { showDetails } from "../../navigation";
import { reaction } from "mobx";
import { Spinner } from "../spinner"; import { Spinner } from "../spinner";
import { DrawerTitle } from "../drawer"; import { DrawerTitle } from "../drawer";
import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import { showDetails } from "../kube-object";
enum sortBy { enum sortBy {
name = "name", name = "name",

View File

@ -5,7 +5,7 @@ import { Link } from "react-router-dom";
import { autorun, observable } from "mobx"; import { autorun, observable } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react"; import { disposeOnUnmount, observer } from "mobx-react";
import { Pod, Secret, secretsApi } from "../../api/endpoints"; import { Pod, Secret, secretsApi } from "../../api/endpoints";
import { getDetailsUrl } from "../../navigation"; import { getDetailsUrl } from "../kube-object";
interface Props { interface Props {
pod: Pod; pod: Pod;

View File

@ -18,8 +18,7 @@ import { KubeEventDetails } from "../+events/kube-event-details";
import { PodDetailsSecrets } from "./pod-details-secrets"; import { PodDetailsSecrets } from "./pod-details-secrets";
import { ResourceMetrics } from "../resource-metrics"; import { ResourceMetrics } from "../resource-metrics";
import { podsStore } from "./pods.store"; import { podsStore } from "./pods.store";
import { getDetailsUrl } from "../../navigation"; import { getDetailsUrl, KubeObjectDetailsProps } from "../kube-object";
import { KubeObjectDetailsProps } from "../kube-object";
import { getItemMetrics } from "../../api/endpoints/metrics.api"; import { getItemMetrics } from "../../api/endpoints/metrics.api";
import { PodCharts, podMetricTabs } from "./pod-charts"; import { PodCharts, podMetricTabs } from "./pod-charts";
import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { KubeObjectMeta } from "../kube-object/kube-object-meta";

View File

@ -9,11 +9,10 @@ import { RouteComponentProps } from "react-router";
import { volumeClaimStore } from "../+storage-volume-claims/volume-claim.store"; import { volumeClaimStore } from "../+storage-volume-claims/volume-claim.store";
import { IPodsRouteParams } from "../+workloads"; import { IPodsRouteParams } from "../+workloads";
import { eventStore } from "../+events/event.store"; import { eventStore } from "../+events/event.store";
import { KubeObjectListLayout } from "../kube-object"; import { getDetailsUrl, KubeObjectListLayout } from "../kube-object";
import { nodesApi, Pod } from "../../api/endpoints"; import { nodesApi, Pod } from "../../api/endpoints";
import { StatusBrick } from "../status-brick"; import { StatusBrick } from "../status-brick";
import { cssNames, stopPropagation } from "../../utils"; import { cssNames, stopPropagation } from "../../utils";
import { getDetailsUrl } from "../../navigation";
import toPairs from "lodash/toPairs"; import toPairs from "lodash/toPairs";
import startCase from "lodash/startCase"; import startCase from "lodash/startCase";
import kebabCase from "lodash/kebabCase"; import kebabCase from "lodash/kebabCase";

View File

@ -6,7 +6,7 @@ import { Trans } from "@lingui/macro";
import { TabLayout, TabLayoutRoute } from "../layout/tab-layout"; import { TabLayout, TabLayoutRoute } from "../layout/tab-layout";
import { WorkloadsOverview } from "../+workloads-overview/overview"; import { WorkloadsOverview } from "../+workloads-overview/overview";
import { cronJobsRoute, cronJobsURL, daemonSetsRoute, daemonSetsURL, deploymentsRoute, deploymentsURL, jobsRoute, jobsURL, overviewRoute, overviewURL, podsRoute, podsURL, replicaSetsRoute, replicaSetsURL, statefulSetsRoute, statefulSetsURL } from "./workloads.route"; import { cronJobsRoute, cronJobsURL, daemonSetsRoute, daemonSetsURL, deploymentsRoute, deploymentsURL, jobsRoute, jobsURL, overviewRoute, overviewURL, podsRoute, podsURL, replicaSetsRoute, replicaSetsURL, statefulSetsRoute, statefulSetsURL } from "./workloads.route";
import { namespaceStore } from "../+namespaces/namespace.store"; import { namespaceUrlParam } from "../+namespaces/namespace.store";
import { Pods } from "../+workloads-pods"; import { Pods } from "../+workloads-pods";
import { Deployments } from "../+workloads-deployments"; import { Deployments } from "../+workloads-deployments";
import { DaemonSets } from "../+workloads-daemonsets"; import { DaemonSets } from "../+workloads-daemonsets";
@ -19,7 +19,7 @@ import { ReplicaSets } from "../+workloads-replicasets";
@observer @observer
export class Workloads extends React.Component { export class Workloads extends React.Component {
static get tabRoutes(): TabLayoutRoute[] { static get tabRoutes(): TabLayoutRoute[] {
const query = namespaceStore.getContextParams(); const query = namespaceUrlParam.toObjectParam();
const routes: TabLayoutRoute[] = [ const routes: TabLayoutRoute[] = [
{ {
title: <Trans>Overview</Trans>, title: <Trans>Overview</Trans>,

View File

@ -41,10 +41,10 @@ import { broadcastMessage, requestMain } from "../../common/ipc";
import whatInput from "what-input"; import whatInput from "what-input";
import { clusterSetFrameIdHandler } from "../../common/cluster-ipc"; import { clusterSetFrameIdHandler } from "../../common/cluster-ipc";
import { ClusterPageMenuRegistration, clusterPageMenuRegistry } from "../../extensions/registries"; import { ClusterPageMenuRegistration, clusterPageMenuRegistry } from "../../extensions/registries";
import { TabLayoutRoute, TabLayout } from "./layout/tab-layout"; import { TabLayout, TabLayoutRoute } from "./layout/tab-layout";
import { StatefulSetScaleDialog } from "./+workloads-statefulsets/statefulset-scale-dialog"; import { StatefulSetScaleDialog } from "./+workloads-statefulsets/statefulset-scale-dialog";
import { eventStore } from "./+events/event.store"; import { eventStore } from "./+events/event.store";
import { reaction, computed } from "mobx"; import { computed, reaction } from "mobx";
import { nodesStore } from "./+nodes/nodes.store"; import { nodesStore } from "./+nodes/nodes.store";
import { podsStore } from "./+workloads-pods/pods.store"; import { podsStore } from "./+workloads-pods/pods.store";
import { sum } from "lodash"; import { sum } from "lodash";
@ -129,16 +129,15 @@ export class App extends React.Component {
if (!menuItem.id) { if (!menuItem.id) {
return routes; return routes;
} }
clusterPageMenuRegistry.getSubItems(menuItem).forEach((item) => { clusterPageMenuRegistry.getSubItems(menuItem).forEach((subMenu) => {
const page = clusterPageRegistry.getByPageMenuTarget(item.target); const page = clusterPageRegistry.getByPageTarget(subMenu.target);
if (page) { if (page) {
routes.push({ routes.push({
routePath: page.routePath, routePath: page.url,
url: getExtensionPageUrl({ extensionId: page.extensionId, pageId: page.id, params: item.target.params }), url: getExtensionPageUrl(subMenu.target),
title: item.title, title: subMenu.title,
component: page.components.Page, component: page.components.Page,
exact: page.exact
}); });
} }
}); });
@ -151,14 +150,14 @@ export class App extends React.Component {
const tabRoutes = this.getTabLayoutRoutes(menu); const tabRoutes = this.getTabLayoutRoutes(menu);
if (tabRoutes.length > 0) { if (tabRoutes.length > 0) {
const pageComponent = () => <TabLayout tabs={tabRoutes} />; const pageComponent = () => <TabLayout tabs={tabRoutes}/>;
return <Route key={`extension-tab-layout-route-${index}`} component={pageComponent} path={tabRoutes.map((tab) => tab.routePath)} />; return <Route key={`extension-tab-layout-route-${index}`} component={pageComponent} path={tabRoutes.map((tab) => tab.routePath)}/>;
} else { } else {
const page = clusterPageRegistry.getByPageMenuTarget(menu.target); const page = clusterPageRegistry.getByPageTarget(menu.target);
if (page) { if (page) {
return <Route key={`extension-tab-layout-route-${index}`} path={page.routePath} exact={page.exact} component={page.components.Page}/>; return <Route key={`extension-tab-layout-route-${index}`} path={page.url} component={page.components.Page}/>;
} }
} }
}); });
@ -169,7 +168,7 @@ export class App extends React.Component {
const menu = clusterPageMenuRegistry.getByPage(page); const menu = clusterPageMenuRegistry.getByPage(page);
if (!menu) { if (!menu) {
return <Route key={`extension-route-${index}`} path={page.routePath} exact={page.exact} component={page.components.Page}/>; return <Route key={`extension-route-${index}`} path={page.url} component={page.components.Page}/>;
} }
}); });
} }

View File

@ -71,8 +71,8 @@ export class ClusterManager extends React.Component {
<Route component={AddCluster} {...addClusterRoute} /> <Route component={AddCluster} {...addClusterRoute} />
<Route component={ClusterView} {...clusterViewRoute} /> <Route component={ClusterView} {...clusterViewRoute} />
<Route component={ClusterSettings} {...clusterSettingsRoute} /> <Route component={ClusterSettings} {...clusterSettingsRoute} />
{globalPageRegistry.getItems().map(({ routePath, exact, components: { Page } }) => { {globalPageRegistry.getItems().map(({ url, components: { Page } }) => {
return <Route key={routePath} path={routePath} component={Page} exact={exact}/>; return <Route key={url} path={url} component={Page}/>;
})} })}
<Redirect exact to={this.startUrl}/> <Redirect exact to={this.startUrl}/>
</Switch> </Switch>

View File

@ -15,7 +15,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";
@ -158,12 +158,13 @@ export class ClustersMenu extends React.Component<Props> {
</div> </div>
<div className="extensions"> <div className="extensions">
{globalPageMenuRegistry.getItems().map(({ title, target, components: { Icon } }) => { {globalPageMenuRegistry.getItems().map(({ title, target, components: { Icon } }) => {
const registeredPage = globalPageRegistry.getByPageMenuTarget(target); const registeredPage = globalPageRegistry.getByPageTarget(target);
if (!registeredPage) return; if (!registeredPage){
const { extensionId, id: pageId } = registeredPage; return;
const pageUrl = getExtensionPageUrl({ extensionId, pageId, params: target.params }); }
const isActive = pageUrl === navigation.location.pathname; const pageUrl = getExtensionPageUrl(target);
const isActive = isActiveRoute(registeredPage.url);
return ( return (
<Icon <Icon

View File

@ -114,7 +114,13 @@ export class Icon extends React.PureComponent<IconProps> {
// render icon type // render icon type
if (link) { if (link) {
return <NavLink {...iconProps} to={link}/>; const { className, children } = iconProps;
return (
<NavLink className={className} to={link}>
{children}
</NavLink>
);
} }
if (href) { if (href) {

View File

@ -2,9 +2,15 @@ import React from "react";
import debounce from "lodash/debounce"; import debounce from "lodash/debounce";
import { autorun, observable } from "mobx"; import { autorun, observable } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react"; import { disposeOnUnmount, observer } from "mobx-react";
import { getSearch, setSearch } from "../../navigation";
import { InputProps } from "./input"; import { InputProps } from "./input";
import { SearchInput } from "./search-input"; import { SearchInput } from "./search-input";
import { createPageParam } from "../../navigation";
export const searchUrlParam = createPageParam({
name: "search",
isSystem: true,
defaultValue: "",
});
interface Props extends InputProps { interface Props extends InputProps {
compact?: boolean; // show only search-icon when not focused compact?: boolean; // show only search-icon when not focused
@ -12,11 +18,11 @@ interface Props extends InputProps {
@observer @observer
export class SearchInputUrl extends React.Component<Props> { export class SearchInputUrl extends React.Component<Props> {
@observable inputVal = ""; // fix: use empty string to avoid react warnings @observable inputVal = ""; // fix: use empty string on init to avoid react warnings
@disposeOnUnmount @disposeOnUnmount
updateInput = autorun(() => this.inputVal = getSearch()); updateInput = autorun(() => this.inputVal = searchUrlParam.get());
updateUrl = debounce((val: string) => setSearch(val), 250); updateUrl = debounce((val: string) => searchUrlParam.set(val), 250);
setValue = (value: string) => { setValue = (value: string) => {
this.inputVal = value; this.inputVal = value;

View File

@ -1,7 +1,7 @@
import { computed, observable, reaction } from "mobx"; import { computed, observable, reaction } from "mobx";
import { autobind } from "../../utils"; import { autobind } from "../../utils";
import { getSearch, setSearch } from "../../navigation";
import { namespaceStore } from "../+namespaces/namespace.store"; import { namespaceStore } from "../+namespaces/namespace.store";
import { searchUrlParam } from "../input/search-input-url";
export enum FilterType { export enum FilterType {
SEARCH = "search", SEARCH = "search",
@ -54,8 +54,8 @@ export class PageFiltersStore {
protected syncWithGlobalSearch() { protected syncWithGlobalSearch() {
const disposers = [ const disposers = [
reaction(() => this.getValues(FilterType.SEARCH)[0], setSearch), reaction(() => this.getValues(FilterType.SEARCH)[0], search => searchUrlParam.set(search)),
reaction(() => getSearch(), search => { reaction(() => searchUrlParam.get(), search => {
const filter = this.getByType(FilterType.SEARCH); const filter = this.getByType(FilterType.SEARCH);
if (filter) { if (filter) {

View File

@ -4,7 +4,7 @@ import React from "react";
import { disposeOnUnmount, observer } from "mobx-react"; import { disposeOnUnmount, observer } from "mobx-react";
import { computed, observable, reaction } from "mobx"; import { computed, observable, reaction } from "mobx";
import { Trans } from "@lingui/macro"; import { Trans } from "@lingui/macro";
import { getDetails, hideDetails } from "../../navigation"; import { createPageParam, navigation } from "../../navigation";
import { Drawer } from "../drawer"; import { Drawer } from "../drawer";
import { KubeObject } from "../../api/kube-object"; import { KubeObject } from "../../api/kube-object";
import { Spinner } from "../spinner"; import { Spinner } from "../spinner";
@ -14,6 +14,43 @@ import { CrdResourceDetails } from "../+custom-resources";
import { KubeObjectMenu } from "./kube-object-menu"; import { KubeObjectMenu } from "./kube-object-menu";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
export const kubeDetailsUrlParam = createPageParam({
name: "kube-details",
isSystem: true,
});
export const kubeSelectedUrlParam = createPageParam({
name: "kube-selected",
isSystem: true,
get defaultValue() {
return kubeDetailsUrlParam.get();
}
});
export function showDetails(details = "", resetSelected = true) {
const detailsUrl = getDetailsUrl(details, resetSelected);
navigation.merge({ search: detailsUrl });
}
export function hideDetails() {
showDetails();
}
export function getDetailsUrl(details: string, resetSelected = false) {
const detailsUrl = kubeDetailsUrlParam.toSearchString({ value: details });
if (resetSelected) {
const params = new URLSearchParams(detailsUrl);
params.delete(kubeSelectedUrlParam.name);
return `?${params.toString()}`;
}
return detailsUrl;
}
export interface KubeObjectDetailsProps<T = KubeObject> { export interface KubeObjectDetailsProps<T = KubeObject> {
className?: string; className?: string;
object: T; object: T;
@ -25,7 +62,7 @@ export class KubeObjectDetails extends React.Component {
@observable.ref loadingError: React.ReactNode; @observable.ref loadingError: React.ReactNode;
@computed get path() { @computed get path() {
return getDetails(); return kubeDetailsUrlParam.get();
} }
@computed get object() { @computed get object() {
@ -70,7 +107,7 @@ export class KubeObjectDetails extends React.Component {
const { object, isLoading, loadingError, isCrdInstance } = this; const { object, isLoading, loadingError, isCrdInstance } = this;
const isOpen = !!(object || isLoading || loadingError); const isOpen = !!(object || isLoading || loadingError);
let title = ""; let title = "";
let details: JSX.Element[]; let details: React.ReactNode[];
if (object) { if (object) {
const { kind, getName } = object; const { kind, getName } = object;
@ -81,7 +118,7 @@ export class KubeObjectDetails extends React.Component {
}); });
if (isCrdInstance && details.length === 0) { if (isCrdInstance && details.length === 0) {
details.push(<CrdResourceDetails object={object} />); details.push(<CrdResourceDetails object={object}/>);
} }
} }
@ -90,7 +127,7 @@ export class KubeObjectDetails extends React.Component {
className="KubeObjectDetails flex column" className="KubeObjectDetails flex column"
open={isOpen} open={isOpen}
title={title} title={title}
toolbar={<KubeObjectMenu object={object} toolbar={true} />} toolbar={<KubeObjectMenu object={object} toolbar={true}/>}
onClose={hideDetails} onClose={hideDetails}
> >
{isLoading && <Spinner center/>} {isLoading && <Spinner center/>}

View File

@ -3,10 +3,10 @@ import { computed } from "mobx";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { cssNames } from "../../utils"; import { cssNames } from "../../utils";
import { KubeObject } from "../../api/kube-object"; import { KubeObject } from "../../api/kube-object";
import { getSelectedDetails, showDetails } from "../../navigation";
import { ItemListLayout, ItemListLayoutProps } from "../item-object-list/item-list-layout"; import { ItemListLayout, ItemListLayoutProps } from "../item-object-list/item-list-layout";
import { KubeObjectStore } from "../../kube-object.store"; import { KubeObjectStore } from "../../kube-object.store";
import { KubeObjectMenu } from "./kube-object-menu"; import { KubeObjectMenu } from "./kube-object-menu";
import { kubeSelectedUrlParam, showDetails } from "./kube-object-details";
export interface KubeObjectListLayoutProps extends ItemListLayoutProps { export interface KubeObjectListLayoutProps extends ItemListLayoutProps {
store: KubeObjectStore; store: KubeObjectStore;
@ -15,14 +15,13 @@ export interface KubeObjectListLayoutProps extends ItemListLayoutProps {
@observer @observer
export class KubeObjectListLayout extends React.Component<KubeObjectListLayoutProps> { export class KubeObjectListLayout extends React.Component<KubeObjectListLayoutProps> {
@computed get selectedItem() { @computed get selectedItem() {
return this.props.store.getByPath(getSelectedDetails()); return this.props.store.getByPath(kubeSelectedUrlParam.get());
} }
onDetails = (item: KubeObject) => { onDetails = (item: KubeObject) => {
if (this.props.onDetails) { if (this.props.onDetails) {
this.props.onDetails(item); this.props.onDetails(item);
} } else {
else {
showDetails(item.selfLink); showDetails(item.selfLink);
} }
}; };

View File

@ -4,7 +4,7 @@ import { autobind, cssNames } from "../../utils";
import { KubeObject } from "../../api/kube-object"; import { KubeObject } from "../../api/kube-object";
import { editResourceTab } from "../dock/edit-resource.store"; import { editResourceTab } from "../dock/edit-resource.store";
import { MenuActions, MenuActionsProps } from "../menu/menu-actions"; import { MenuActions, MenuActionsProps } from "../menu/menu-actions";
import { hideDetails } from "../../navigation"; import { hideDetails } from "./kube-object-details";
import { apiManager } from "../../api/api-manager"; import { apiManager } from "../../api/api-manager";
import { kubeObjectMenuRegistry } from "../../../extensions/registries/kube-object-menu-registry"; import { kubeObjectMenuRegistry } from "../../../extensions/registries/kube-object-menu-registry";

View File

@ -2,10 +2,10 @@ import React from "react";
import { Trans } from "@lingui/macro"; import { Trans } from "@lingui/macro";
import { IKubeMetaField, KubeObject } from "../../api/kube-object"; import { IKubeMetaField, KubeObject } from "../../api/kube-object";
import { DrawerItem, DrawerItemLabels } from "../drawer"; import { DrawerItem, DrawerItemLabels } from "../drawer";
import { getDetailsUrl } from "../../navigation";
import { lookupApiLink } from "../../api/kube-api"; import { lookupApiLink } from "../../api/kube-api";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import { getDetailsUrl } from "./kube-object-details";
export interface KubeObjectMetaProps { export interface KubeObjectMetaProps {
object: KubeObject; object: KubeObject;

View File

@ -17,7 +17,7 @@ import { clusterRoute, clusterURL } from "../+cluster";
import { Config, configRoute, configURL } from "../+config"; import { Config, configRoute, configURL } from "../+config";
import { eventRoute, eventsURL } from "../+events"; import { eventRoute, eventsURL } from "../+events";
import { Apps, appsRoute, appsURL } from "../+apps"; import { Apps, appsRoute, appsURL } from "../+apps";
import { namespaceStore } from "../+namespaces/namespace.store"; import { namespaceUrlParam } from "../+namespaces/namespace.store";
import { Workloads } from "../+workloads"; import { Workloads } from "../+workloads";
import { UserManagement } from "../+user-management"; import { UserManagement } from "../+user-management";
import { Storage } from "../+storage"; import { Storage } from "../+storage";
@ -75,21 +75,23 @@ export class Sidebar extends React.Component<Props> {
} }
getTabLayoutRoutes(menu: ClusterPageMenuRegistration): TabLayoutRoute[] { getTabLayoutRoutes(menu: ClusterPageMenuRegistration): TabLayoutRoute[] {
if (!menu.id) {
return [];
}
const routes: TabLayoutRoute[] = []; const routes: TabLayoutRoute[] = [];
clusterPageMenuRegistry.getSubItems(menu).forEach((subItem) => { if (!menu.id) {
const subPage = clusterPageRegistry.getByPageMenuTarget(subItem.target); return routes;
}
clusterPageMenuRegistry.getSubItems(menu).forEach((subMenu) => {
const subPage = clusterPageRegistry.getByPageTarget(subMenu.target);
if (subPage) { if (subPage) {
const { extensionId, id: pageId } = subPage;
routes.push({ routes.push({
routePath: subPage.routePath, routePath: subPage.url,
url: getExtensionPageUrl({ extensionId: subPage.extensionId, pageId: subPage.id, params: subItem.target.params }), url: getExtensionPageUrl({ extensionId, pageId, params: subMenu.target.params }),
title: subItem.title, title: subMenu.title,
component: subPage.components.Page, component: subPage.components.Page,
exact: subPage.exact
}); });
} }
}); });
@ -99,7 +101,7 @@ export class Sidebar extends React.Component<Props> {
renderRegisteredMenus() { renderRegisteredMenus() {
return clusterPageMenuRegistry.getRootItems().map((menuItem, index) => { return clusterPageMenuRegistry.getRootItems().map((menuItem, index) => {
const registeredPage = clusterPageRegistry.getByPageMenuTarget(menuItem.target); const registeredPage = clusterPageRegistry.getByPageTarget(menuItem.target);
const tabRoutes = this.getTabLayoutRoutes(menuItem); const tabRoutes = this.getTabLayoutRoutes(menuItem);
const id = `registered-item-${index}`; const id = `registered-item-${index}`;
let pageUrl: string; let pageUrl: string;
@ -109,7 +111,7 @@ export class Sidebar extends React.Component<Props> {
const { extensionId, id: pageId } = registeredPage; const { extensionId, id: pageId } = registeredPage;
pageUrl = getExtensionPageUrl({ extensionId, pageId, params: menuItem.target.params }); pageUrl = getExtensionPageUrl({ extensionId, pageId, params: menuItem.target.params });
isActive = isActiveRoute(registeredPage.routePath); isActive = isActiveRoute(registeredPage.url);
} else if (tabRoutes.length > 0) { } else if (tabRoutes.length > 0) {
pageUrl = tabRoutes[0].url; pageUrl = tabRoutes[0].url;
isActive = isActiveRoute(tabRoutes.map((tab) => tab.routePath)); isActive = isActiveRoute(tabRoutes.map((tab) => tab.routePath));
@ -133,7 +135,7 @@ export class Sidebar extends React.Component<Props> {
render() { render() {
const { toggle, isPinned, className } = this.props; const { toggle, isPinned, className } = this.props;
const query = namespaceStore.getContextParams(); const query = namespaceUrlParam.toObjectParam();
return ( return (
<SidebarContext.Provider value={{ pinned: isPinned }}> <SidebarContext.Provider value={{ pinned: isPinned }}>

View File

@ -1,19 +1,17 @@
import "./table.scss"; import "./table.scss";
import React from "react"; import React from "react";
import { orderBy } from "lodash";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { computed, observable } from "mobx"; import { observable } from "mobx";
import { autobind, cssNames, noop } from "../../utils"; import { autobind, cssNames, noop } from "../../utils";
import { TableRow, TableRowElem, TableRowProps } from "./table-row"; import { TableRow, TableRowElem, TableRowProps } from "./table-row";
import { TableHead, TableHeadElem, TableHeadProps } from "./table-head"; import { TableHead, TableHeadElem, TableHeadProps } from "./table-head";
import { TableCellElem } from "./table-cell"; import { TableCellElem } from "./table-cell";
import { VirtualList } from "../virtual-list"; import { VirtualList } from "../virtual-list";
import { navigation, setQueryParams } from "../../navigation"; import { createPageParam } from "../../navigation";
import orderBy from "lodash/orderBy";
import { ItemObject } from "../../item.store"; import { ItemObject } from "../../item.store";
// todo: refactor + decouple search from location
export type TableSortBy = string; export type TableSortBy = string;
export type TableOrderBy = "asc" | "desc" | string; export type TableOrderBy = "asc" | "desc" | string;
export type TableSortParams = { sortBy: TableSortBy; orderBy: TableOrderBy }; export type TableSortParams = { sortBy: TableSortBy; orderBy: TableOrderBy };
@ -43,6 +41,16 @@ export interface TableProps extends React.DOMAttributes<HTMLDivElement> {
getTableRow?: (uid: string) => React.ReactElement<TableRowProps>; getTableRow?: (uid: string) => React.ReactElement<TableRowProps>;
} }
export const sortByUrlParam = createPageParam({
name: "sort",
isSystem: true,
});
export const orderByUrlParam = createPageParam({
name: "order",
isSystem: true,
});
@observer @observer
export class Table extends React.Component<TableProps> { export class Table extends React.Component<TableProps> {
static defaultProps: TableProps = { static defaultProps: TableProps = {
@ -53,18 +61,13 @@ export class Table extends React.Component<TableProps> {
sortSyncWithUrl: true, sortSyncWithUrl: true,
}; };
@observable sortParamsLocal = this.props.sortByDefault; @observable sortParams: Partial<TableSortParams> = Object.assign(
this.props.sortSyncWithUrl ? {
@computed get sortParams(): Partial<TableSortParams> { sortBy: sortByUrlParam.get(),
if (this.props.sortSyncWithUrl) { orderBy: orderByUrlParam.get(),
const sortBy = navigation.searchParams.get("sortBy"); } : {},
const orderBy = navigation.searchParams.get("orderBy"); this.props.sortByDefault,
);
return { sortBy, orderBy };
}
return this.sortParamsLocal || {};
}
renderHead() { renderHead() {
const { sortable, children } = this.props; const { sortable, children } = this.props;
@ -101,29 +104,24 @@ export class Table extends React.Component<TableProps> {
} }
getSorted(items: any[]) { getSorted(items: any[]) {
const { sortParams } = this; const { sortBy, orderBy: order } = this.sortParams;
const sortingCallback = this.props.sortable[sortParams.sortBy] || noop; const sortingCallback = this.props.sortable[sortBy] || noop;
return orderBy( return orderBy(items, sortingCallback, order as any);
items,
sortingCallback,
sortParams.orderBy as any
);
} }
@autobind() @autobind()
protected onSort(params: TableSortParams) { protected onSort({ sortBy, orderBy }: TableSortParams) {
this.sortParams = { sortBy, orderBy };
const { sortSyncWithUrl, onSort } = this.props; const { sortSyncWithUrl, onSort } = this.props;
if (sortSyncWithUrl) { if (sortSyncWithUrl) {
setQueryParams(params); sortByUrlParam.set(sortBy);
} orderByUrlParam.set(orderBy);
else {
this.sortParamsLocal = params;
} }
if (onSort) { if (onSort) {
onSort(params); onSort({ sortBy, orderBy });
} }
} }

View File

@ -1,136 +0,0 @@
// Navigation helpers
import { matchPath, RouteProps } from "react-router";
import { reaction } from "mobx";
import { createObservableHistory } from "mobx-observable-history";
import { createBrowserHistory, LocationDescriptor } from "history";
import logger from "../main/logger";
import { clusterViewRoute, IClusterViewRouteParams } from "./components/cluster-manager/cluster-view.route";
import { broadcastMessage, subscribeToBroadcast } from "../common/ipc";
export const history = createBrowserHistory();
export const navigation = createObservableHistory(history);
/**
* Navigate to a location. Works only in renderer.
*/
export function navigate(location: LocationDescriptor) {
const currentLocation = navigation.getPath();
navigation.push(location);
if (currentLocation === navigation.getPath()) {
navigation.goBack(); // prevent sequences of same url in history
}
}
export function matchParams<P>(route: string | string[] | RouteProps) {
return matchPath<P>(navigation.location.pathname, route);
}
export function isActiveRoute(route: string | string[] | RouteProps): boolean {
return !!matchParams(route);
}
// common params for all pages
export interface IQueryParams {
namespaces?: string[]; // selected context namespaces
details?: string; // serialized resource details
selected?: string; // mark resource as selected
search?: string; // search-input value
sortBy?: string; // sorting params for table-list
orderBy?: string;
}
export function getQueryString(params?: Partial<IQueryParams>, merge = true) {
const searchParams = navigation.searchParams.copyWith(params);
if (!merge) {
Array.from(searchParams.keys()).forEach(key => {
if (!(key in params)) searchParams.delete(key);
});
}
return searchParams.toString({ withPrefix: true });
}
export function setQueryParams<T>(params?: T & IQueryParams, { merge = true, replace = false } = {}) {
const newSearch = getQueryString(params, merge);
navigation.merge({ search: newSearch }, replace);
}
export function getDetails() {
return navigation.searchParams.get("details");
}
export function getSelectedDetails() {
return navigation.searchParams.get("selected") || getDetails();
}
export function getDetailsUrl(details: string) {
if (!details) return "";
return getQueryString({
details,
selected: getSelectedDetails(),
});
}
/**
* Show details. Works only in renderer.
*/
export function showDetails(path: string, resetSelected = true) {
navigation.searchParams.merge({
details: path,
selected: resetSelected ? null : getSelectedDetails(),
});
}
/**
* Hide details. Works only in renderer.
*/
export function hideDetails() {
showDetails(null);
}
export function setSearch(text: string) {
navigation.replace({
search: getQueryString({ search: text })
});
}
export function getSearch() {
return navigation.searchParams.get("search") || "";
}
export function getMatchedClusterId(): string {
const matched = matchPath<IClusterViewRouteParams>(navigation.location.pathname, {
exact: true,
path: clusterViewRoute.path
});
return matched?.params.clusterId;
}
//-- EVENTS
if (process.isMainFrame) {
// Keep track of active cluster-id for handling IPC/menus/etc.
reaction(() => getMatchedClusterId(), clusterId => {
broadcastMessage("cluster-view:current-id", clusterId);
}, {
fireImmediately: true
});
}
// Handle navigation via IPC (e.g. from top menu)
subscribeToBroadcast("renderer:navigate", (event, location: LocationDescriptor) => {
logger.info(`[IPC]: ${event.type} ${JSON.stringify(location)}`, event);
navigate(location);
});
// Reload dashboard window
subscribeToBroadcast("renderer:reload", () => {
location.reload();
});

View File

@ -0,0 +1,31 @@
import { ipcRenderer } from "electron";
import { reaction } from "mobx";
import { getMatchedClusterId, navigate } from "./helpers";
import { broadcastMessage, subscribeToBroadcast } from "../../common/ipc";
import logger from "../../main/logger";
export function bindEvents() {
if (!ipcRenderer) {
return;
}
if (process.isMainFrame) {
// Keep track of active cluster-id for handling IPC/menus/etc.
reaction(() => getMatchedClusterId(), clusterId => {
broadcastMessage("cluster-view:current-id", clusterId);
}, {
fireImmediately: true
});
}
// Handle navigation via IPC (e.g. from top menu)
subscribeToBroadcast("renderer:navigate", (event, url: string) => {
logger.info(`[IPC]: ${event.type} ${JSON.stringify(url)}`, event);
navigate(url);
});
// Reload dashboard window
subscribeToBroadcast("renderer:reload", () => {
location.reload();
});
}

View File

@ -0,0 +1,36 @@
import type { LocationDescriptor } from "history";
import { matchPath, RouteProps } from "react-router";
import { PageParam, PageSystemParamInit } from "./page-param";
import { clusterViewRoute, IClusterViewRouteParams } from "../components/cluster-manager/cluster-view.route";
import { navigation } from "./history";
export function navigate(location: LocationDescriptor) {
const currentLocation = navigation.getPath();
navigation.push(location);
if (currentLocation === navigation.getPath()) {
navigation.goBack(); // prevent sequences of same url in history
}
}
export function createPageParam<V = string>(init: PageSystemParamInit<V>) {
return new PageParam<V>(init, navigation);
}
export function matchRoute<P>(route: string | string[] | RouteProps) {
return matchPath<P>(navigation.location.pathname, route);
}
export function isActiveRoute(route: string | string[] | RouteProps): boolean {
return !!matchRoute(route);
}
export function getMatchedClusterId(): string {
const matched = matchPath<IClusterViewRouteParams>(navigation.location.pathname, {
exact: true,
path: clusterViewRoute.path
});
return matched?.params.clusterId;
}

View File

@ -0,0 +1,6 @@
import { ipcRenderer } from "electron";
import { createBrowserHistory, createMemoryHistory } from "history";
import { createObservableHistory } from "mobx-observable-history";
export const history = ipcRenderer ? createBrowserHistory() : createMemoryHistory();
export const navigation = createObservableHistory(history);

View File

@ -0,0 +1,8 @@
// Navigation (renderer)
import { bindEvents } from "./events";
export * from "./history";
export * from "./helpers";
bindEvents();

View File

@ -0,0 +1,135 @@
// Manage observable URL-param from document.location.search
import { IObservableHistory } from "mobx-observable-history";
export interface PageParamInit<V = any> {
name: string;
defaultValue?: V;
defaultValueStringified?: string | string[]; // serialized version of "defaultValue"
multiValues?: boolean; // false == by default
multiValueSep?: string; // joining multiple values with separator, default: ","
skipEmpty?: boolean; // skip empty value(s), e.g. "?param=", default: true
parse?(value: string[]): V; // deserialize from URL
stringify?(value: V): string | string[]; // serialize params to URL
}
export interface PageSystemParamInit<V = any> extends PageParamInit<V> {
isSystem?: boolean;
}
export class PageParam<V = any> {
static SYSTEM_PREFIX = "lens-";
readonly name: string;
protected urlName: string;
constructor(readonly init: PageParamInit<V> | PageSystemParamInit<V>, protected history: IObservableHistory) {
const { isSystem, name } = init as PageSystemParamInit;
this.name = name;
this.init.skipEmpty ??= true;
this.init.multiValueSep ??= ",";
// prefixing to avoid collisions with extensions
this.urlName = `${isSystem ? PageParam.SYSTEM_PREFIX : ""}${name}`;
}
isEmpty(value: V | any) {
return [value].flat().every(value => value == "" || value == null);
}
parse(values: string[]): V {
const { parse, multiValues } = this.init;
if (!multiValues) values.splice(1); // reduce values to single item
const parsedValues = [parse ? parse(values) : values].flat();
return multiValues ? parsedValues : parsedValues[0] as any;
}
stringify(value: V = this.get()): string {
const { stringify, multiValues, multiValueSep, skipEmpty } = this.init;
if (skipEmpty && this.isEmpty(value)) {
return "";
}
if (multiValues) {
const values = [value].flat();
const stringValues = [stringify ? stringify(value) : values.map(String)].flat();
return stringValues.join(multiValueSep);
}
return [stringify ? stringify(value) : String(value)].flat()[0];
}
get(): V {
const value = this.parse(this.getRaw());
if (this.init.skipEmpty && this.isEmpty(value)) {
return this.getDefaultValue();
}
return value;
}
set(value: V, { mergeGlobals = true, replaceHistory = false } = {}) {
const search = this.toSearchString({ mergeGlobals, value });
this.history.merge({ search }, replaceHistory);
}
setRaw(value: string | string[]) {
const { history, urlName } = this;
const { multiValues, multiValueSep, skipEmpty } = this.init;
const paramValue = multiValues ? [value].flat().join(multiValueSep) : String(value);
if (skipEmpty && this.isEmpty(paramValue)) {
history.searchParams.delete(urlName);
} else {
history.searchParams.set(urlName, paramValue);
}
}
getRaw(): string[] {
const { history, urlName } = this;
const { multiValueSep } = this.init;
return history.searchParams.getAsArray(urlName, multiValueSep);
}
getDefaultValue() {
const { defaultValue, defaultValueStringified } = this.init;
return defaultValueStringified ? this.parse([defaultValueStringified].flat()) : defaultValue;
}
clear() {
this.history.searchParams.delete(this.urlName);
}
toSearchString({ withPrefix = true, mergeGlobals = true, value = this.get() } = {}): string {
const { history, urlName, init: { skipEmpty } } = this;
const searchParams = new URLSearchParams(mergeGlobals ? history.location.search : "");
searchParams.set(urlName, this.stringify(value));
if (skipEmpty) {
searchParams.forEach((value: any, paramName) => {
if (this.isEmpty(value)) searchParams.delete(paramName);
});
}
if (Array.from(searchParams).length > 0) {
return `${withPrefix ? "?" : ""}${searchParams}`;
}
return "";
}
toObjectParam(value = this.get()): Record<string, V> {
return {
[this.urlName]: value,
};
}
}

View File

@ -2272,10 +2272,17 @@
dependencies: dependencies:
"@types/react" "*" "@types/react" "*"
"@types/react-router-dom@^5.1.5": "@types/react-dom@^17.0.0":
version "5.1.5" version "17.0.0"
resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.1.5.tgz#7c334a2ea785dbad2b2dcdd83d2cf3d9973da090" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.0.tgz#b3b691eb956c4b3401777ee67b900cb28415d95a"
integrity sha512-ArBM4B1g3BWLGbaGvwBGO75GNFbLDUthrDojV2vHLih/Tq8M+tgvY1DSwkuNrPSwdp/GUL93WSEpTZs8nVyJLw== integrity sha512-lUqY7OlkF/RbNtD5nIq7ot8NquXrdFrjSOR6+w9a9RFQevGi1oZO1dcJbXMeONAPKtZ2UrZOEJ5UOCVsxbLk/g==
dependencies:
"@types/react" "*"
"@types/react-router-dom@^5.1.6":
version "5.1.6"
resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.1.6.tgz#07b14e7ab1893a837c8565634960dc398564b1fb"
integrity sha512-gjrxYqxz37zWEdMVvQtWPFMFj1dRDb4TGOcgyOfSXTrEXdF92L00WE3C471O3TV/RF1oskcStkXsOU0Ete4s/g==
dependencies: dependencies:
"@types/history" "*" "@types/history" "*"
"@types/react" "*" "@types/react" "*"
@ -2312,7 +2319,7 @@
dependencies: dependencies:
"@types/react" "*" "@types/react" "*"
"@types/react@*", "@types/react@^16.9.35": "@types/react@*":
version "16.9.35" version "16.9.35"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.35.tgz#a0830d172e8aadd9bd41709ba2281a3124bbd368" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.35.tgz#a0830d172e8aadd9bd41709ba2281a3124bbd368"
integrity sha512-q0n0SsWcGc8nDqH2GJfWQWUOmZSJhXV64CjVN5SvcNti3TdEaA3AH0D8DwNmMdzjMAC/78tB8nAZIlV8yTz+zQ== integrity sha512-q0n0SsWcGc8nDqH2GJfWQWUOmZSJhXV64CjVN5SvcNti3TdEaA3AH0D8DwNmMdzjMAC/78tB8nAZIlV8yTz+zQ==
@ -2320,6 +2327,14 @@
"@types/prop-types" "*" "@types/prop-types" "*"
csstype "^2.2.0" csstype "^2.2.0"
"@types/react@^17.0.0":
version "17.0.0"
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.0.tgz#5af3eb7fad2807092f0046a1302b7823e27919b8"
integrity sha512-aj/L7RIMsRlWML3YB6KZiXB3fV2t41+5RBGYF8z+tAKU43Px8C3cYUZsDvf1/+Bm4FK21QWBrDutu8ZJ/70qOw==
dependencies:
"@types/prop-types" "*"
csstype "^3.0.2"
"@types/relateurl@*": "@types/relateurl@*":
version "0.2.28" version "0.2.28"
resolved "https://registry.yarnpkg.com/@types/relateurl/-/relateurl-0.2.28.tgz#6bda7db8653fa62643f5ee69e9f69c11a392e3a6" resolved "https://registry.yarnpkg.com/@types/relateurl/-/relateurl-0.2.28.tgz#6bda7db8653fa62643f5ee69e9f69c11a392e3a6"
@ -5023,6 +5038,11 @@ csstype@^2.2.0, csstype@^2.5.2, csstype@^2.5.7, csstype@^2.6.5, csstype@^2.6.7:
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.10.tgz#e63af50e66d7c266edb6b32909cfd0aabe03928b" resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.10.tgz#e63af50e66d7c266edb6b32909cfd0aabe03928b"
integrity sha512-D34BqZU4cIlMCY93rZHbrq9pjTAQJ3U8S8rfBqjwHxkGPThWFjzZDQpgMJY0QViLxth6ZKYiwFBo14RdN44U/w== integrity sha512-D34BqZU4cIlMCY93rZHbrq9pjTAQJ3U8S8rfBqjwHxkGPThWFjzZDQpgMJY0QViLxth6ZKYiwFBo14RdN44U/w==
csstype@^3.0.2:
version "3.0.5"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.5.tgz#7fdec6a28a67ae18647c51668a9ff95bb2fa7bb8"
integrity sha512-uVDi8LpBUKQj6sdxNaTetL6FpeCqTjOvAQuQUa/qAqq8oOd4ivkbhgnqayl0dnPal8Tb/yB1tF+gOvCBiicaiQ==
currently-unhandled@^0.4.1: currently-unhandled@^0.4.1:
version "0.4.1" version "0.4.1"
resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"
@ -12108,15 +12128,14 @@ react-beautiful-dnd@^13.0.0:
redux "^4.0.4" redux "^4.0.4"
use-memo-one "^1.1.1" use-memo-one "^1.1.1"
react-dom@^16.13.1: react-dom@^17.0.1:
version "16.13.1" version "17.0.1"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.13.1.tgz#c1bd37331a0486c078ee54c4740720993b2e0e7f" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.1.tgz#1de2560474ec9f0e334285662ede52dbc5426fc6"
integrity sha512-81PIMmVLnCNLO/fFOQxdQkvEq/+Hfpv24XNJfpyZhTRfO0QcmQIF/PgCa1zCOj2w1hrn12MFLyaJ/G0+Mxtfag== integrity sha512-6eV150oJZ9U2t9svnsspTMrWNyHc6chX0KzDeAOXftRa8bNeOKTTfCJ7KorIwenkHd2xqVTBTCZd79yk/lx/Ug==
dependencies: dependencies:
loose-envify "^1.1.0" loose-envify "^1.1.0"
object-assign "^4.1.1" object-assign "^4.1.1"
prop-types "^15.6.2" scheduler "^0.20.1"
scheduler "^0.19.1"
react-input-autosize@^2.2.2: react-input-autosize@^2.2.2:
version "2.2.2" version "2.2.2"
@ -12217,15 +12236,6 @@ react-zlib-js@^1.0.4:
resolved "https://registry.yarnpkg.com/react-zlib-js/-/react-zlib-js-1.0.4.tgz#dd2b9fbf56d5ab224fa7a99affbbedeba9aa3dc7" resolved "https://registry.yarnpkg.com/react-zlib-js/-/react-zlib-js-1.0.4.tgz#dd2b9fbf56d5ab224fa7a99affbbedeba9aa3dc7"
integrity sha512-ynXD9DFxpE7vtGoa3ZwBtPmZrkZYw2plzHGbanUjBOSN4RtuXdektSfABykHtTiWEHMh7WdYj45LHtp228ZF1A== integrity sha512-ynXD9DFxpE7vtGoa3ZwBtPmZrkZYw2plzHGbanUjBOSN4RtuXdektSfABykHtTiWEHMh7WdYj45LHtp228ZF1A==
react@^16.14.0:
version "16.14.0"
resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d"
integrity sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
prop-types "^15.6.2"
react@^16.8.0: react@^16.8.0:
version "16.13.1" version "16.13.1"
resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e" resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e"
@ -12235,6 +12245,14 @@ react@^16.8.0:
object-assign "^4.1.1" object-assign "^4.1.1"
prop-types "^15.6.2" prop-types "^15.6.2"
react@^17.0.1:
version "17.0.1"
resolved "https://registry.yarnpkg.com/react/-/react-17.0.1.tgz#6e0600416bd57574e3f86d92edba3d9008726127"
integrity sha512-lG9c9UuMHdcAexXtigOZLX8exLWkW0Ku29qPRU8uhF2R9BN96dLCt0psvzPLlHc5OWkgymP3qwTRgbnw5BKx3w==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
read-cmd-shim@^1.0.1, read-cmd-shim@^1.0.5: read-cmd-shim@^1.0.1, read-cmd-shim@^1.0.5:
version "1.0.5" version "1.0.5"
resolved "https://registry.yarnpkg.com/read-cmd-shim/-/read-cmd-shim-1.0.5.tgz#87e43eba50098ba5a32d0ceb583ab8e43b961c16" resolved "https://registry.yarnpkg.com/read-cmd-shim/-/read-cmd-shim-1.0.5.tgz#87e43eba50098ba5a32d0ceb583ab8e43b961c16"
@ -12934,10 +12952,10 @@ saxes@^5.0.0:
dependencies: dependencies:
xmlchars "^2.2.0" xmlchars "^2.2.0"
scheduler@^0.19.1: scheduler@^0.20.1:
version "0.19.1" version "0.20.1"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.1.tgz#4f3e2ed2c1a7d65681f4c854fa8c5a1ccb40f196" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.1.tgz#da0b907e24026b01181ecbc75efdc7f27b5a000c"
integrity sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA== integrity sha512-LKTe+2xNJBNxu/QhHvDR14wUXHRQbVY5ZOYpOGWRzhydZUqrLb2JBvLPY7cAqFmqrWuDED0Mjk7013SZiOz6Bw==
dependencies: dependencies:
loose-envify "^1.1.0" loose-envify "^1.1.0"
object-assign "^4.1.1" object-assign "^4.1.1"
@ -14530,7 +14548,7 @@ typeface-roboto@^0.0.75:
resolved "https://registry.yarnpkg.com/typeface-roboto/-/typeface-roboto-0.0.75.tgz#98d5ba35ec234bbc7172374c8297277099cc712b" resolved "https://registry.yarnpkg.com/typeface-roboto/-/typeface-roboto-0.0.75.tgz#98d5ba35ec234bbc7172374c8297277099cc712b"
integrity sha512-VrR/IiH00Z1tFP4vDGfwZ1esNqTiDMchBEXYY9kilT6wRGgFoCAlgkEUMHb1E3mB0FsfZhv756IF0+R+SFPfdg== integrity sha512-VrR/IiH00Z1tFP4vDGfwZ1esNqTiDMchBEXYY9kilT6wRGgFoCAlgkEUMHb1E3mB0FsfZhv756IF0+R+SFPfdg==
typescript@^4.0.2: typescript@4.0.2:
version "4.0.2" version "4.0.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.2.tgz#7ea7c88777c723c681e33bf7988be5d008d05ac2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.2.tgz#7ea7c88777c723c681e33bf7988be5d008d05ac2"
integrity sha512-e4ERvRV2wb+rRZ/IQeb3jm2VxBsirQLpQhdxplZ2MEzGvDkkMmPglecnNDfSUBivMjP93vRbngYYDQqQ/78bcQ== integrity sha512-e4ERvRV2wb+rRZ/IQeb3jm2VxBsirQLpQhdxplZ2MEzGvDkkMmPglecnNDfSUBivMjP93vRbngYYDQqQ/78bcQ==