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 path from "path";
|
||||
import { observer } from "mobx-react";
|
||||
import { CoffeeDoodle } from "react-open-doodles";
|
||||
|
||||
export const exampleId = Navigation.createPageParam({
|
||||
name: "exampleId",
|
||||
defaultValue: "demo",
|
||||
});
|
||||
|
||||
export function ExampleIcon(props: Component.IconProps) {
|
||||
return <Component.Icon {...props} material="pages" tooltip={path.basename(__filename)}/>;
|
||||
export interface ExamplePageProps extends Interface.PageComponentProps<ExamplePageParams> {
|
||||
extension: LensRendererExtension; // provided in "./renderer.tsx"
|
||||
}
|
||||
|
||||
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<{ extension: LensRendererExtension }> {
|
||||
export class ExamplePage extends React.Component<ExamplePageProps> {
|
||||
async componentDidMount() {
|
||||
await namespaceStore.loadAll();
|
||||
}
|
||||
|
||||
deactivate = () => {
|
||||
const { extension } = this.props;
|
||||
|
||||
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() {
|
||||
const exampleName = exampleId.get();
|
||||
const doodleStyle = {
|
||||
width: "200px"
|
||||
};
|
||||
const { exampleId } = this.props.params;
|
||||
|
||||
return (
|
||||
<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>
|
||||
<p>File: <i>{__filename}</i></p>
|
||||
<p>Location: <i>{location.href}</i></p>
|
||||
<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>
|
||||
{exampleName === "secret" && (
|
||||
{exampleId.get() === "secret" && (
|
||||
<Component.Button accent label="Deactivate" onClick={this.deactivate}/>
|
||||
)}
|
||||
</p>
|
||||
|
||||
@ -1,47 +1,45 @@
|
||||
import { LensRendererExtension } from "@k8slens/extensions";
|
||||
import { ExampleIcon, ExamplePage } from "./page";
|
||||
import { Component, Interface, K8sApi, LensRendererExtension } from "@k8slens/extensions";
|
||||
import { ExamplePage, ExamplePageParams, namespaceStore } from "./page";
|
||||
import React from "react";
|
||||
import path from "path";
|
||||
|
||||
export default class ExampleExtension extends LensRendererExtension {
|
||||
clusterPages = [
|
||||
clusterPages: Interface.PageRegistration[] = [
|
||||
{
|
||||
id: "example",
|
||||
title: "Example Extension",
|
||||
components: {
|
||||
Page: () => <ExamplePage extension={this}/>,
|
||||
Page: (props: Interface.PageComponentProps<ExamplePageParams>) => {
|
||||
return <ExamplePage {...props} extension={this}/>;
|
||||
},
|
||||
},
|
||||
params: {
|
||||
// setup param "exampleId" with default value "demo"
|
||||
// could be also {[paramName: string]: UrlParam} for advanced use-cases (custom parse/stringify)
|
||||
exampleId: "demo"
|
||||
// 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.PageMenuRegistration[] = [
|
||||
{
|
||||
title: "Example extension",
|
||||
components: {
|
||||
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 { KubeObjectMenuRegistration, KubeObjectMenuComponents } from "../registries/kube-object-menu-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 { 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 React from "react";
|
||||
|
||||
@ -50,7 +50,7 @@ describe("getPageUrl", () => {
|
||||
});
|
||||
|
||||
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 pageUrl = getExtensionPageUrl({ extensionId: ext.name, pageId: "page-with-params", params });
|
||||
|
||||
|
||||
@ -2,27 +2,33 @@
|
||||
import { action, observable } from "mobx";
|
||||
import { LensExtension } from "../lens-extension";
|
||||
|
||||
export class BaseRegistry<T> {
|
||||
private items = observable<T>([], { deep: false });
|
||||
export class BaseRegistry<T, I = T> {
|
||||
private items = observable.map<T, I>();
|
||||
|
||||
getItems(): T[] {
|
||||
return this.items.toJS();
|
||||
getItems(): I[] {
|
||||
return Array.from(this.items.values());
|
||||
}
|
||||
|
||||
add(items: T | T[], ext?: LensExtension): () => void; // allow method overloading with required "ext"
|
||||
@action
|
||||
add(items: T | T[]) {
|
||||
add(items: T | T[], extension?: LensExtension) {
|
||||
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);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars-ts
|
||||
protected getRegisteredItem(item: T, extension?: LensExtension): I {
|
||||
return item as any;
|
||||
}
|
||||
|
||||
@action
|
||||
remove(...items: T[]) {
|
||||
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>;
|
||||
}
|
||||
|
||||
export class PageMenuRegistry<T extends PageMenuRegistration = any> extends BaseRegistry<T> {
|
||||
export class PageMenuRegistry<T extends PageMenuRegistration> extends BaseRegistry<T> {
|
||||
@action
|
||||
add(items: T[], ext: LensExtension) {
|
||||
const normalizedItems = items.map(menuItem => {
|
||||
|
||||
@ -1,43 +1,51 @@
|
||||
// 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 { LensExtension, sanitizeExtensionName } from "../lens-extension";
|
||||
import { PageParam } from "../../renderer/navigation/page-param";
|
||||
import logger from "../../main/logger";
|
||||
import { isPageParamInit, PageParam, PageParamInit } from "../../renderer/navigation/page-param";
|
||||
import { createPageParam } from "../../renderer/navigation/helpers";
|
||||
|
||||
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
|
||||
*/
|
||||
id?: string;
|
||||
params?: PageParams<string | ExtensionPageParamInit>;
|
||||
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 {
|
||||
Page: React.ComponentType<any>;
|
||||
}
|
||||
|
||||
export interface PageTarget<P = PageTargetParams> {
|
||||
export interface PageTarget<P = PageParams> {
|
||||
extensionId?: string;
|
||||
pageId?: string;
|
||||
params?: P;
|
||||
}
|
||||
|
||||
export interface PageTargetParams<V = any> {
|
||||
export interface PageParams<V = any> {
|
||||
[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;
|
||||
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 {
|
||||
@ -54,16 +62,11 @@ export function getExtensionPageUrl(target: PageTarget): string {
|
||||
|
||||
if (registeredPage?.params) {
|
||||
Object.entries(registeredPage.params).forEach(([name, param]) => {
|
||||
const targetParamValue = targetParams[name];
|
||||
|
||||
if (param instanceof PageParam) {
|
||||
pageUrl.searchParams.set(name, param.stringify(targetParamValue));
|
||||
const paramValue = param.stringify(targetParams[name]);
|
||||
if (param.init.skipEmpty && param.isEmpty(paramValue)) {
|
||||
pageUrl.searchParams.delete(name);
|
||||
} else {
|
||||
const value = String(targetParamValue ?? param);
|
||||
|
||||
if (value) {
|
||||
pageUrl.searchParams.set(name, value);
|
||||
}
|
||||
pageUrl.searchParams.set(name, paramValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -71,31 +74,41 @@ export function getExtensionPageUrl(target: PageTarget): string {
|
||||
return pageUrl.href.replace(pageUrl.origin, "");
|
||||
}
|
||||
|
||||
export class PageRegistry extends BaseRegistry<RegisteredPage> {
|
||||
@action
|
||||
add(pages: PageRegistration | PageRegistration[], extension: LensExtension) {
|
||||
try {
|
||||
const items = [pages].flat().map(page => this.registerPage(page, extension));
|
||||
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 super.add(items);
|
||||
} catch (error) {
|
||||
return Function; // no-op
|
||||
}
|
||||
return {
|
||||
id: pageId, extensionId, params, components, url,
|
||||
};
|
||||
}
|
||||
|
||||
registerPage(page: PageRegistration, ext: LensExtension): RegisteredPage {
|
||||
try {
|
||||
const { id: pageId } = page;
|
||||
const extensionId = ext.name;
|
||||
protected normalizeComponents(components: PageComponents, params?: PageParams<PageParam>): PageComponents {
|
||||
if (params) {
|
||||
const { Page } = components;
|
||||
|
||||
return {
|
||||
...page,
|
||||
extensionId,
|
||||
url: getExtensionPageUrl({ extensionId, pageId }),
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`Failed to register page: ${error}`, { error });
|
||||
components.Page = observer((props: object) => React.createElement(Page, { params, ...props }));
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
export { PageParam, PageParamInit } from "../../renderer/navigation/page-param";
|
||||
export { navigate, isActiveRoute, createPageParam } from "../../renderer/navigation";
|
||||
export { PageParamInit, PageParam } from "../../renderer/navigation/page-param";
|
||||
export { navigate, isActiveRoute, createPageParam } from "../../renderer/navigation/helpers";
|
||||
export { hideDetails, showDetails, getDetailsUrl } from "../../renderer/components/kube-object/kube-object-details";
|
||||
export { IURLParams } from "../../common/utils/buildUrl";
|
||||
|
||||
@ -46,8 +46,8 @@ export class ApiManager {
|
||||
});
|
||||
}
|
||||
|
||||
getStore(api: string | KubeApi): KubeObjectStore {
|
||||
return this.stores.get(this.resolveApi(api));
|
||||
getStore<S extends KubeObjectStore>(api: string | KubeApi): S {
|
||||
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 { 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";
|
||||
import { bindEvents } from "./events";
|
||||
|
||||
export let history = ipcRenderer ? createBrowserHistory() : createMemoryHistory();
|
||||
export let navigation = createObservableHistory(history);
|
||||
export * from "./history";
|
||||
export * from "./helpers";
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
bindEvents();
|
||||
@ -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";
|
||||
|
||||
export interface PageParamInit<V = any> {
|
||||
name: string;
|
||||
isSystem?: boolean;
|
||||
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?(values: string[]): V; // deserialize from URL
|
||||
stringify?(values: V): string | string[]; // serialize params to URL
|
||||
parse?(value: string[]): V; // deserialize from 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-";
|
||||
|
||||
readonly name: 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;
|
||||
|
||||
this.name = name;
|
||||
@ -28,7 +29,7 @@ export class PageParam<V = any | any[]> {
|
||||
this.urlName = `${isSystem ? PageParam.SYSTEM_PREFIX : ""}${name}`;
|
||||
}
|
||||
|
||||
isEmpty(value: V) {
|
||||
isEmpty(value: V | any) {
|
||||
return [value].flat().every(value => value == "" || value == null);
|
||||
}
|
||||
|
||||
@ -59,12 +60,10 @@ export class PageParam<V = any | any[]> {
|
||||
}
|
||||
|
||||
get(): V {
|
||||
const { history, urlName } = this;
|
||||
const { multiValueSep, defaultValue, skipEmpty } = this.init;
|
||||
const value = this.parse(history.searchParams.getAsArray(urlName, multiValueSep));
|
||||
const value = this.parse(this.getRaw());
|
||||
|
||||
if (skipEmpty && this.isEmpty(value)) {
|
||||
return defaultValue;
|
||||
if (this.init.skipEmpty && this.isEmpty(value)) {
|
||||
return this.getDefaultValue();
|
||||
}
|
||||
|
||||
return value;
|
||||
@ -76,12 +75,29 @@ export class PageParam<V = any | any[]> {
|
||||
this.history.merge({ search }, replaceHistory);
|
||||
}
|
||||
|
||||
getDefaultValue(){
|
||||
return this.init.defaultValue;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
isDefault() {
|
||||
return this.get() === this.getDefaultValue();
|
||||
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() {
|
||||
@ -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