mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
fine-tuning
Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
parent
3a4a07ca7f
commit
daf373168f
@ -1,43 +1,61 @@
|
|||||||
import { Component, LensRendererExtension, Navigation } from "@k8slens/extensions";
|
import { Component, Interface, K8sApi, LensRendererExtension } from "@k8slens/extensions";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import path from "path";
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { CoffeeDoodle } from "react-open-doodles";
|
import { CoffeeDoodle } from "react-open-doodles";
|
||||||
|
|
||||||
export const exampleId = Navigation.createPageParam({
|
export interface ExamplePageProps extends Interface.PageComponentProps<ExamplePageParams> {
|
||||||
name: "exampleId",
|
extension: LensRendererExtension; // provided in "./renderer.tsx"
|
||||||
defaultValue: "demo",
|
|
||||||
});
|
|
||||||
|
|
||||||
export function ExampleIcon(props: Component.IconProps) {
|
|
||||||
return <Component.Icon {...props} material="pages" tooltip={path.basename(__filename)}/>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ExamplePageParams {
|
||||||
|
exampleId: string;
|
||||||
|
selectedNamespaces: K8sApi.Namespace[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const namespaceStore = K8sApi.apiManager.getStore<K8sApi.NamespaceStore>(K8sApi.namespacesApi);
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class ExamplePage extends React.Component<{ extension: LensRendererExtension }> {
|
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();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
renderSelectedNamespaces() {
|
||||||
|
const { selectedNamespaces } = this.props.params;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex gaps inline">
|
||||||
|
{selectedNamespaces.get().map(ns => {
|
||||||
|
const name = ns.getName();
|
||||||
|
|
||||||
|
return <Component.Badge key={name} label={name} tooltip={`Created: ${ns.getAge()}`}/>;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const exampleName = exampleId.get();
|
const { exampleId } = this.props.params;
|
||||||
const doodleStyle = {
|
|
||||||
width: "200px"
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex column gaps align-flex-start" style={{ padding: 24 }}>
|
<div className="flex column gaps align-flex-start" style={{ padding: 24 }}>
|
||||||
<div style={doodleStyle}><CoffeeDoodle accent="#3d90ce"/></div>
|
<div style={{ width: 200 }}>
|
||||||
|
<CoffeeDoodle accent="#3d90ce"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p>Hello from Example extension!</p>
|
<div>Hello from Example extension!</div>
|
||||||
<p>File: <i>{__filename}</i></p>
|
<div>Location: <i>{location.href}</i></div>
|
||||||
<p>Location: <i>{location.href}</i></p>
|
<div>Namespaces: {this.renderSelectedNamespaces()}</div>
|
||||||
|
|
||||||
<p className="url-params-demo flex column gaps">
|
<p className="url-params-demo flex column gaps">
|
||||||
<a onClick={() => exampleId.set("secret")}>Show secret button</a>
|
<a onClick={() => exampleId.set("secret")}>Show secret button</a>
|
||||||
{exampleName === "secret" && (
|
{exampleId.get() === "secret" && (
|
||||||
<Component.Button accent label="Deactivate" onClick={this.deactivate}/>
|
<Component.Button accent label="Deactivate" onClick={this.deactivate}/>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@ -1,47 +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: {
|
params: {
|
||||||
// setup param "exampleId" with default value "demo"
|
// setup basic param "exampleId" with default value "demo"
|
||||||
// could be also {[paramName: string]: UrlParam} for advanced use-cases (custom parse/stringify)
|
exampleId: "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.PageMenuRegistration[] = [
|
||||||
{
|
{
|
||||||
title: "Example extension",
|
title: "Example extension",
|
||||||
components: {
|
components: {
|
||||||
Icon: ExampleIcon,
|
Icon: ExampleIcon,
|
||||||
},
|
},
|
||||||
target: {
|
|
||||||
pageId: "example",
|
|
||||||
params: {
|
|
||||||
exampleId: "demo-sample-2"
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Example secret page",
|
|
||||||
components: {
|
|
||||||
Icon: ExampleIcon,
|
|
||||||
},
|
|
||||||
target: {
|
|
||||||
pageId: "example",
|
|
||||||
params: {
|
|
||||||
exampleId: "secret"
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function ExampleIcon(props: Component.IconProps) {
|
||||||
|
return <Component.Icon {...props} material="pages" tooltip={path.basename(__filename)}/>;
|
||||||
|
}
|
||||||
|
|||||||
@ -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, PageMenuComponents } from "../registries/page-menu-registry";
|
||||||
export type { StatusBarRegistration } from "../registries/status-bar-registry";
|
export type { StatusBarRegistration } from "../registries/status-bar-registry";
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { getExtensionPageUrl, globalPageRegistry, PageTargetParams } 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";
|
||||||
|
|
||||||
@ -50,7 +50,7 @@ describe("getPageUrl", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("gets page url with custom params", () => {
|
it("gets page url with custom params", () => {
|
||||||
const params: PageTargetParams<string> = { test1: "one", test2: "2" };
|
const params: PageParams<string> = { test1: "one", test2: "2" };
|
||||||
const searchParams = new URLSearchParams(params);
|
const searchParams = new URLSearchParams(params);
|
||||||
const pageUrl = getExtensionPageUrl({ extensionId: ext.name, pageId: "page-with-params", params });
|
const pageUrl = getExtensionPageUrl({ extensionId: ext.name, pageId: "page-with-params", params });
|
||||||
|
|
||||||
|
|||||||
@ -2,27 +2,33 @@
|
|||||||
import { action, observable } from "mobx";
|
import { action, observable } from "mobx";
|
||||||
import { LensExtension } from "../lens-extension";
|
import { LensExtension } from "../lens-extension";
|
||||||
|
|
||||||
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 = [items].flat() as T[];
|
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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,7 +21,7 @@ export interface PageMenuComponents {
|
|||||||
Icon: React.ComponentType<IconProps>;
|
Icon: React.ComponentType<IconProps>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PageMenuRegistry<T extends PageMenuRegistration = any> extends BaseRegistry<T> {
|
export class PageMenuRegistry<T extends PageMenuRegistration> extends BaseRegistry<T> {
|
||||||
@action
|
@action
|
||||||
add(items: T[], ext: LensExtension) {
|
add(items: T[], ext: LensExtension) {
|
||||||
const normalizedItems = items.map(menuItem => {
|
const normalizedItems = items.map(menuItem => {
|
||||||
|
|||||||
@ -1,43 +1,51 @@
|
|||||||
// Extensions-api -> Custom page registration
|
// Extensions-api -> Custom page registration
|
||||||
import type React from "react";
|
|
||||||
import { action } from "mobx";
|
import React from "react";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
import { BaseRegistry } from "./base-registry";
|
import { BaseRegistry } from "./base-registry";
|
||||||
import { LensExtension, sanitizeExtensionName } from "../lens-extension";
|
import { LensExtension, sanitizeExtensionName } from "../lens-extension";
|
||||||
import { PageParam } from "../../renderer/navigation/page-param";
|
import { isPageParamInit, PageParam, PageParamInit } from "../../renderer/navigation/page-param";
|
||||||
import logger from "../../main/logger";
|
import { createPageParam } from "../../renderer/navigation/helpers";
|
||||||
|
|
||||||
export interface PageRegistration {
|
export interface PageRegistration {
|
||||||
/**
|
/**
|
||||||
* Page-id, part of of extension's page url, must be unique within same extension
|
* Page ID, part of extension's page url, must be unique within same extension
|
||||||
* 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>;
|
||||||
components: PageComponents;
|
components: PageComponents;
|
||||||
/**
|
|
||||||
* Registered page params.
|
|
||||||
* Used to generate final page url when provided in getExtensionPageUrl()-helper.
|
|
||||||
* Advanced usage: provide `UrlParam` as values to customize parsing/stringification from/to URL.
|
|
||||||
*/
|
|
||||||
params?: PageTargetParams<string | PageParam>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// exclude "name" field since provided as key in page.params
|
||||||
|
export type ExtensionPageParamInit = Omit<PageParamInit, "name" | "isSystem">;
|
||||||
|
|
||||||
export interface PageComponents {
|
export interface PageComponents {
|
||||||
Page: React.ComponentType<any>;
|
Page: React.ComponentType<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PageTarget<P = PageTargetParams> {
|
export interface PageTarget<P = PageParams> {
|
||||||
extensionId?: string;
|
extensionId?: string;
|
||||||
pageId?: string;
|
pageId?: string;
|
||||||
params?: P;
|
params?: P;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PageTargetParams<V = any> {
|
export interface PageParams<V = any> {
|
||||||
[paramName: string]: V;
|
[paramName: string]: V;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RegisteredPage extends PageRegistration {
|
export interface PageComponentProps<P extends PageParams = {}> {
|
||||||
|
params?: {
|
||||||
|
[N in keyof P]: PageParam<P[N]>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegisteredPage {
|
||||||
|
id: string;
|
||||||
extensionId: string;
|
extensionId: string;
|
||||||
url: string; // registered extension's page URL (without page params)
|
url: string; // registered extension's page URL (without page params)
|
||||||
|
params: PageParams<PageParam>; // normalized params
|
||||||
|
components: PageComponents; // normalized components
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getExtensionPageUrl(target: PageTarget): string {
|
export function getExtensionPageUrl(target: PageTarget): string {
|
||||||
@ -54,16 +62,11 @@ export function getExtensionPageUrl(target: PageTarget): string {
|
|||||||
|
|
||||||
if (registeredPage?.params) {
|
if (registeredPage?.params) {
|
||||||
Object.entries(registeredPage.params).forEach(([name, param]) => {
|
Object.entries(registeredPage.params).forEach(([name, param]) => {
|
||||||
const targetParamValue = targetParams[name];
|
const paramValue = param.stringify(targetParams[name]);
|
||||||
|
if (param.init.skipEmpty && param.isEmpty(paramValue)) {
|
||||||
if (param instanceof PageParam) {
|
pageUrl.searchParams.delete(name);
|
||||||
pageUrl.searchParams.set(name, param.stringify(targetParamValue));
|
|
||||||
} else {
|
} else {
|
||||||
const value = String(targetParamValue ?? param);
|
pageUrl.searchParams.set(name, paramValue);
|
||||||
|
|
||||||
if (value) {
|
|
||||||
pageUrl.searchParams.set(name, value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -71,31 +74,41 @@ export function getExtensionPageUrl(target: PageTarget): string {
|
|||||||
return pageUrl.href.replace(pageUrl.origin, "");
|
return pageUrl.href.replace(pageUrl.origin, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PageRegistry extends BaseRegistry<RegisteredPage> {
|
export class PageRegistry extends BaseRegistry<PageRegistration, RegisteredPage> {
|
||||||
@action
|
protected getRegisteredItem(page: PageRegistration, ext: LensExtension): RegisteredPage {
|
||||||
add(pages: PageRegistration | PageRegistration[], extension: LensExtension) {
|
const { id: pageId } = page;
|
||||||
try {
|
const extensionId = ext.name;
|
||||||
const items = [pages].flat().map(page => this.registerPage(page, extension));
|
const params = this.normalizeParams(page.params);
|
||||||
|
const components = this.normalizeComponents(page.components, params);
|
||||||
|
const url = getExtensionPageUrl({ extensionId, pageId });
|
||||||
|
|
||||||
return super.add(items);
|
return {
|
||||||
} catch (error) {
|
id: pageId, extensionId, params, components, url,
|
||||||
return Function; // no-op
|
};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
registerPage(page: PageRegistration, ext: LensExtension): RegisteredPage {
|
protected normalizeComponents(components: PageComponents, params?: PageParams<PageParam>): PageComponents {
|
||||||
try {
|
if (params) {
|
||||||
const { id: pageId } = page;
|
const { Page } = components;
|
||||||
const extensionId = ext.name;
|
|
||||||
|
|
||||||
return {
|
components.Page = observer((props: object) => React.createElement(Page, { params, ...props }));
|
||||||
...page,
|
|
||||||
extensionId,
|
|
||||||
url: getExtensionPageUrl({ extensionId, pageId }),
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Failed to register page: ${error}`, { error });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return components;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected normalizeParams(params?: PageParams<string | ExtensionPageParamInit>): PageParams<PageParam> {
|
||||||
|
if (!params) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Object.entries(params).forEach(([name, value]) => {
|
||||||
|
const paramInit: PageParamInit = isPageParamInit(value) ? value : { name, defaultValue: value };
|
||||||
|
|
||||||
|
paramInit.name ??= name;
|
||||||
|
params[name] = createPageParam(paramInit);
|
||||||
|
});
|
||||||
|
|
||||||
|
return params as PageParams<PageParam>;
|
||||||
}
|
}
|
||||||
|
|
||||||
getByPageTarget(target: PageTarget): RegisteredPage | null {
|
getByPageTarget(target: PageTarget): RegisteredPage | null {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
export { PageParam, PageParamInit } from "../../renderer/navigation/page-param";
|
export { PageParamInit, PageParam } from "../../renderer/navigation/page-param";
|
||||||
export { navigate, isActiveRoute, createPageParam } from "../../renderer/navigation";
|
export { navigate, isActiveRoute, createPageParam } from "../../renderer/navigation/helpers";
|
||||||
export { hideDetails, showDetails, getDetailsUrl } from "../../renderer/components/kube-object/kube-object-details";
|
export { hideDetails, showDetails, getDetailsUrl } from "../../renderer/components/kube-object/kube-object-details";
|
||||||
export { IURLParams } from "../../common/utils/buildUrl";
|
export { IURLParams } from "../../common/utils/buildUrl";
|
||||||
|
|||||||
@ -46,8 +46,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
31
src/renderer/navigation/events.ts
Normal file
31
src/renderer/navigation/events.ts
Normal 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();
|
||||||
|
});
|
||||||
|
}
|
||||||
36
src/renderer/navigation/helpers.ts
Normal file
36
src/renderer/navigation/helpers.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import type { LocationDescriptor } from "history";
|
||||||
|
import { matchPath, RouteProps } from "react-router";
|
||||||
|
import { PageParam, PageParamInit } 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: PageParamInit<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;
|
||||||
|
}
|
||||||
6
src/renderer/navigation/history.ts
Normal file
6
src/renderer/navigation/history.ts
Normal 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);
|
||||||
@ -1,70 +1,8 @@
|
|||||||
// Navigation helpers
|
// Navigation (renderer)
|
||||||
|
|
||||||
import { ipcRenderer } from "electron";
|
import { bindEvents } from "./events";
|
||||||
import { reaction } from "mobx";
|
|
||||||
import { matchPath, RouteProps } from "react-router";
|
|
||||||
import { createObservableHistory } from "mobx-observable-history";
|
|
||||||
import { createBrowserHistory, createMemoryHistory, LocationDescriptor } from "history";
|
|
||||||
import { broadcastMessage, subscribeToBroadcast } from "../../common/ipc";
|
|
||||||
import { PageParam, PageParamInit } from "./page-param";
|
|
||||||
import { clusterViewRoute, IClusterViewRouteParams } from "../components/cluster-manager/cluster-view.route";
|
|
||||||
import logger from "../../main/logger";
|
|
||||||
|
|
||||||
export let history = ipcRenderer ? createBrowserHistory() : createMemoryHistory();
|
export * from "./history";
|
||||||
export let navigation = createObservableHistory(history);
|
export * from "./helpers";
|
||||||
|
|
||||||
export function navigate(location: LocationDescriptor) {
|
bindEvents();
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getMatchedClusterId(): string {
|
|
||||||
const matched = matchPath<IClusterViewRouteParams>(navigation.location.pathname, {
|
|
||||||
exact: true,
|
|
||||||
path: clusterViewRoute.path
|
|
||||||
});
|
|
||||||
|
|
||||||
return matched?.params.clusterId;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createPageParam<V = string>(init: PageParamInit<V>) {
|
|
||||||
return new PageParam<V>(init, navigation);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ipcRenderer) {
|
|
||||||
history = createBrowserHistory();
|
|
||||||
navigation = createObservableHistory(history);
|
|
||||||
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -1,24 +1,25 @@
|
|||||||
// Manage observable URL-param via location.search
|
// Manage observable URL-param from document.location.search
|
||||||
import { IObservableHistory } from "mobx-observable-history";
|
import { IObservableHistory } from "mobx-observable-history";
|
||||||
|
|
||||||
export interface PageParamInit<V = any> {
|
export interface PageParamInit<V = any> {
|
||||||
name: string;
|
name: string;
|
||||||
isSystem?: boolean;
|
isSystem?: boolean;
|
||||||
defaultValue?: V;
|
defaultValue?: V;
|
||||||
|
defaultValueStringified?: string | string[]; // serialized version of "defaultValue"
|
||||||
multiValues?: boolean; // false == by default
|
multiValues?: boolean; // false == by default
|
||||||
multiValueSep?: string; // joining multiple values with separator, default: ","
|
multiValueSep?: string; // joining multiple values with separator, default: ","
|
||||||
skipEmpty?: boolean; // skip empty value(s), e.g. "?param=", default: true
|
skipEmpty?: boolean; // skip empty value(s), e.g. "?param=", default: true
|
||||||
parse?(values: string[]): V; // deserialize from URL
|
parse?(value: string[]): V; // deserialize from URL
|
||||||
stringify?(values: V): string | string[]; // serialize params to URL
|
stringify?(value: V): string | string[]; // serialize params to URL
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PageParam<V = any | any[]> {
|
export class PageParam<V = any> {
|
||||||
static SYSTEM_PREFIX = "lens-";
|
static SYSTEM_PREFIX = "lens-";
|
||||||
|
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
protected urlName: string;
|
protected urlName: string;
|
||||||
|
|
||||||
constructor(private init: PageParamInit<V>, private history: IObservableHistory) {
|
constructor(readonly init: PageParamInit<V>, protected history: IObservableHistory) {
|
||||||
const { isSystem, name, skipEmpty = true } = init;
|
const { isSystem, name, skipEmpty = true } = init;
|
||||||
|
|
||||||
this.name = name;
|
this.name = name;
|
||||||
@ -28,7 +29,7 @@ export class PageParam<V = any | any[]> {
|
|||||||
this.urlName = `${isSystem ? PageParam.SYSTEM_PREFIX : ""}${name}`;
|
this.urlName = `${isSystem ? PageParam.SYSTEM_PREFIX : ""}${name}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
isEmpty(value: V) {
|
isEmpty(value: V | any) {
|
||||||
return [value].flat().every(value => value == "" || value == null);
|
return [value].flat().every(value => value == "" || value == null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,12 +60,10 @@ export class PageParam<V = any | any[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get(): V {
|
get(): V {
|
||||||
const { history, urlName } = this;
|
const value = this.parse(this.getRaw());
|
||||||
const { multiValueSep, defaultValue, skipEmpty } = this.init;
|
|
||||||
const value = this.parse(history.searchParams.getAsArray(urlName, multiValueSep));
|
|
||||||
|
|
||||||
if (skipEmpty && this.isEmpty(value)) {
|
if (this.init.skipEmpty && this.isEmpty(value)) {
|
||||||
return defaultValue;
|
return this.getDefaultValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
@ -76,12 +75,29 @@ export class PageParam<V = any | any[]> {
|
|||||||
this.history.merge({ search }, replaceHistory);
|
this.history.merge({ search }, replaceHistory);
|
||||||
}
|
}
|
||||||
|
|
||||||
getDefaultValue(){
|
setRaw(value: string | string[]) {
|
||||||
return this.init.defaultValue;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isDefault() {
|
getRaw(): string[] {
|
||||||
return this.get() === this.getDefaultValue();
|
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() {
|
clear() {
|
||||||
@ -113,3 +129,12 @@ export class PageParam<V = any | any[]> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isPageParamInit(paramInit: PageParamInit | any = {}): paramInit is PageParamInit {
|
||||||
|
const init: PageParamInit = paramInit;
|
||||||
|
|
||||||
|
return [
|
||||||
|
init.defaultValue !== undefined || init.defaultValueStringified !== undefined,
|
||||||
|
typeof init.parse === "function" && typeof init.stringify === "function",
|
||||||
|
].some(Boolean);
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user