mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
decentralizing page url-params management -- PoC / tsc 4.1 random fixes
Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
parent
b8e190e8fd
commit
b7be386e6b
@ -14,7 +14,6 @@ export * from "./splitArray";
|
||||
export * from "./saveToAppFiles";
|
||||
export * from "./singleton";
|
||||
export * from "./openExternal";
|
||||
export * from "./rectify-array";
|
||||
export * from "./downloadFile";
|
||||
export * from "./escapeRegExp";
|
||||
export * from "./tar";
|
||||
|
||||
@ -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];
|
||||
}
|
||||
@ -44,7 +44,7 @@ export abstract class ClusterFeature {
|
||||
*
|
||||
* @param cluster the cluster that the feature is to be installed on
|
||||
*/
|
||||
abstract async install(cluster: Cluster): Promise<void>;
|
||||
abstract install(cluster: Cluster): Promise<void>;
|
||||
|
||||
/**
|
||||
* to be implemented in the derived class, this method is typically called by Lens when a user has indicated that this feature is to be upgraded. The implementation
|
||||
@ -52,7 +52,7 @@ export abstract class ClusterFeature {
|
||||
*
|
||||
* @param cluster the cluster that the feature is to be upgraded on
|
||||
*/
|
||||
abstract async upgrade(cluster: Cluster): Promise<void>;
|
||||
abstract upgrade(cluster: Cluster): Promise<void>;
|
||||
|
||||
/**
|
||||
* to be implemented in the derived class, this method is typically called by Lens when a user has indicated that this feature is to be uninstalled. The implementation
|
||||
@ -60,7 +60,7 @@ export abstract class ClusterFeature {
|
||||
*
|
||||
* @param cluster the cluster that the feature is to be uninstalled from
|
||||
*/
|
||||
abstract async uninstall(cluster: Cluster): Promise<void>;
|
||||
abstract uninstall(cluster: Cluster): Promise<void>;
|
||||
|
||||
/**
|
||||
* to be implemented in the derived class, this method is called periodically by Lens to determine details about the feature's current status. The implementation
|
||||
@ -72,7 +72,7 @@ export abstract class ClusterFeature {
|
||||
*
|
||||
* @return a promise, resolved with the updated ClusterFeatureStatus
|
||||
*/
|
||||
abstract async updateStatus(cluster: Cluster): Promise<ClusterFeatureStatus>;
|
||||
abstract updateStatus(cluster: Cluster): Promise<ClusterFeatureStatus>;
|
||||
|
||||
/**
|
||||
* this is a helper method that conveniently applies kubernetes resources to the cluster.
|
||||
|
||||
@ -70,31 +70,31 @@ describe("globalPageRegistry", () => {
|
||||
], ext);
|
||||
});
|
||||
|
||||
describe("getByPageMenuTarget", () => {
|
||||
it("matching to first registered page without id", () => {
|
||||
const page = globalPageRegistry.getByPageMenuTarget({ extensionId: ext.name });
|
||||
|
||||
expect(page.id).toEqual(undefined);
|
||||
expect(page.extensionId).toEqual(ext.name);
|
||||
expect(page.routePath).toEqual(getExtensionPageUrl({ extensionId: ext.name }));
|
||||
});
|
||||
|
||||
it("returns matching page", () => {
|
||||
const page = globalPageRegistry.getByPageMenuTarget({
|
||||
pageId: "test-page",
|
||||
extensionId: ext.name
|
||||
});
|
||||
|
||||
expect(page.id).toEqual("test-page");
|
||||
});
|
||||
|
||||
it("returns null if target not found", () => {
|
||||
const page = globalPageRegistry.getByPageMenuTarget({
|
||||
pageId: "wrong-page",
|
||||
extensionId: ext.name
|
||||
});
|
||||
|
||||
expect(page).toBeNull();
|
||||
});
|
||||
});
|
||||
// describe("getByPageMenuTarget", () => {
|
||||
// it("matching to first registered page without id", () => {
|
||||
// const page = globalPageRegistry.getByPageMenuTarget({ extensionId: ext.name });
|
||||
//
|
||||
// expect(page.id).toEqual(undefined);
|
||||
// expect(page.extensionId).toEqual(ext.name);
|
||||
// expect(page.url).toEqual(getExtensionPageUrl({ extensionId: ext.name }));
|
||||
// });
|
||||
//
|
||||
// it("returns matching page", () => {
|
||||
// const page = globalPageRegistry.getByPageMenuTarget({
|
||||
// pageId: "test-page",
|
||||
// extensionId: ext.name
|
||||
// });
|
||||
//
|
||||
// expect(page.id).toEqual("test-page");
|
||||
// });
|
||||
//
|
||||
// it("returns null if target not found", () => {
|
||||
// const page = globalPageRegistry.getByPageMenuTarget({
|
||||
// pageId: "wrong-page",
|
||||
// extensionId: ext.name
|
||||
// });
|
||||
//
|
||||
// expect(page).toBeNull();
|
||||
// });
|
||||
// });
|
||||
});
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
// Base class for extensions-api registries
|
||||
import { action, observable } from "mobx";
|
||||
import { LensExtension } from "../lens-extension";
|
||||
import { rectify } from "../../common/utils";
|
||||
|
||||
export class BaseRegistry<T> {
|
||||
private items = observable<T>([], { deep: false });
|
||||
@ -13,7 +12,7 @@ export class BaseRegistry<T> {
|
||||
add(items: T | T[], ext?: LensExtension): () => void; // allow method overloading with required "ext"
|
||||
@action
|
||||
add(items: T | T[]) {
|
||||
const itemArray = rectify(items);
|
||||
const itemArray = [items].flat() as T[];
|
||||
|
||||
this.items.push(...itemArray);
|
||||
|
||||
|
||||
@ -1,19 +1,13 @@
|
||||
// Extensions-api -> Register page menu items
|
||||
import type { IconProps } from "../../renderer/components/icon";
|
||||
import type React from "react";
|
||||
import type { PageTarget, RegisteredPage } from "./page-registry";
|
||||
import { action } from "mobx";
|
||||
import { BaseRegistry } from "./base-registry";
|
||||
import { LensExtension } from "../lens-extension";
|
||||
import { RegisteredPage } from "./page-registry";
|
||||
|
||||
export interface PageMenuTarget<P extends object = any> {
|
||||
extensionId?: string;
|
||||
pageId?: string;
|
||||
params?: P;
|
||||
}
|
||||
|
||||
export interface PageMenuRegistration {
|
||||
target?: PageMenuTarget;
|
||||
target?: PageTarget;
|
||||
title: React.ReactNode;
|
||||
components: PageMenuComponents;
|
||||
}
|
||||
@ -27,9 +21,9 @@ export interface PageMenuComponents {
|
||||
Icon: React.ComponentType<IconProps>;
|
||||
}
|
||||
|
||||
export class GlobalPageMenuRegistry extends BaseRegistry<PageMenuRegistration> {
|
||||
export class PageMenuRegistry<T extends PageMenuRegistration = any> extends BaseRegistry<T> {
|
||||
@action
|
||||
add(items: PageMenuRegistration[], ext: LensExtension) {
|
||||
add(items: T[], ext: LensExtension) {
|
||||
const normalizedItems = items.map(menuItem => {
|
||||
menuItem.target = {
|
||||
extensionId: ext.name,
|
||||
@ -43,33 +37,23 @@ export class GlobalPageMenuRegistry extends BaseRegistry<PageMenuRegistration> {
|
||||
}
|
||||
}
|
||||
|
||||
export class ClusterPageMenuRegistry extends BaseRegistry<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);
|
||||
}
|
||||
|
||||
export class ClusterPageMenuRegistry extends PageMenuRegistry<ClusterPageMenuRegistration> {
|
||||
getRootItems() {
|
||||
return this.getItems().filter((item) => !item.parentId);
|
||||
}
|
||||
|
||||
getSubItems(parent: ClusterPageMenuRegistration) {
|
||||
return this.getItems().filter((item) => item.parentId === parent.id && item.target.extensionId === parent.target.extensionId);
|
||||
return this.getItems().filter((item) => {
|
||||
return item.parentId === parent.id && item.target.extensionId === parent.target.extensionId;
|
||||
});
|
||||
}
|
||||
|
||||
getByPage(page: RegisteredPage) {
|
||||
return this.getItems().find((item) => item.target?.pageId == page.id && item.target?.extensionId === page.extensionId);
|
||||
getByPage({ id: pageId, extensionId }: RegisteredPage) {
|
||||
return this.getItems().find((item) => {
|
||||
return item.target.pageId == pageId && item.target.extensionId === extensionId;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const globalPageMenuRegistry = new GlobalPageMenuRegistry();
|
||||
export const globalPageMenuRegistry = new PageMenuRegistry();
|
||||
export const clusterPageMenuRegistry = new ClusterPageMenuRegistry();
|
||||
|
||||
@ -1,93 +1,94 @@
|
||||
// Extensions-api -> Custom page registration
|
||||
import type { PageMenuTarget } from "./page-menu-registry";
|
||||
import type React from "react";
|
||||
import type { UrlParam } from "../../renderer/navigation/url-param";
|
||||
|
||||
import path from "path";
|
||||
import { action } from "mobx";
|
||||
import { compile } from "path-to-regexp";
|
||||
import { BaseRegistry } from "./base-registry";
|
||||
import { LensExtension, sanitizeExtensionName } from "../lens-extension";
|
||||
import logger from "../../main/logger";
|
||||
import { rectify } from "../../common/utils";
|
||||
|
||||
export interface PageRegistration {
|
||||
/**
|
||||
* Page ID or additional route path to indicate uniqueness within current extension registered pages
|
||||
* Might contain special url placeholders, e.g. "/users/:userId?" (? - marks as optional param)
|
||||
* Page-id, part of 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;
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
export interface RegisteredPage extends PageRegistration {
|
||||
extensionId: string; // required for compiling registered page to url with page-menu-target to compare
|
||||
routePath: string; // full route-path to registered extension page
|
||||
/**
|
||||
* Registered page params.
|
||||
* Used to generate page url when provided in getExtensionPageUrl()-helper.
|
||||
*/
|
||||
params?: UrlParam[];
|
||||
}
|
||||
|
||||
export interface PageComponents {
|
||||
Page: React.ComponentType<any>;
|
||||
}
|
||||
|
||||
export function getExtensionPageUrl<P extends object>({ extensionId, pageId = "", params }: PageMenuTarget<P>): string {
|
||||
const extensionBaseUrl = compile(`/extension/:name`)({
|
||||
name: sanitizeExtensionName(extensionId), // compile only with extension-id first and define base path
|
||||
});
|
||||
const extPageRoutePath = path.posix.join(extensionBaseUrl, pageId);
|
||||
export interface PageTarget<P = {}> {
|
||||
extensionId?: string;
|
||||
pageId?: string;
|
||||
params?: Record<string, any | any[]> & P;
|
||||
}
|
||||
|
||||
export interface RegisteredPage extends PageRegistration {
|
||||
extensionId: string;
|
||||
url: string; // registered extension's page URL (without page params)
|
||||
}
|
||||
|
||||
export function getExtensionPageUrl<P extends object>(target: PageTarget): string {
|
||||
const { extensionId, pageId = "", params } = target;
|
||||
let stringifiedParams = "";
|
||||
|
||||
// stringify params to matched target page
|
||||
if (params) {
|
||||
return compile(extPageRoutePath)(params); // might throw error when required params not passed
|
||||
const page = globalPageRegistry.getByPageTarget(target) || clusterPageRegistry.getByPageTarget(target);
|
||||
if (page?.params) {
|
||||
const searchParams: string[] = [];
|
||||
page.params.forEach(urlParam => {
|
||||
const paramValue = params[urlParam.name];
|
||||
if (paramValue == undefined) return;
|
||||
searchParams.push(
|
||||
urlParam.toSearchString(paramValue, { mergeGlobals: false, withPrefix: false }) // e.g. "param=value"
|
||||
);
|
||||
});
|
||||
if (searchParams.length > 0) {
|
||||
stringifiedParams = `?${searchParams.join("&")}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return extPageRoutePath;
|
||||
return path.posix.join("/extension", sanitizeExtensionName(extensionId), pageId, stringifiedParams);
|
||||
}
|
||||
|
||||
export class PageRegistry extends BaseRegistry<RegisteredPage> {
|
||||
@action
|
||||
add(items: PageRegistration | PageRegistration[], ext: LensExtension) {
|
||||
const itemArray = rectify(items);
|
||||
let registeredPages: RegisteredPage[] = [];
|
||||
|
||||
add(pages: PageRegistration | PageRegistration[], extension: LensExtension) {
|
||||
try {
|
||||
registeredPages = itemArray.map(page => ({
|
||||
...page,
|
||||
extensionId: ext.name,
|
||||
routePath: getExtensionPageUrl({ extensionId: ext.name, pageId: page.id }),
|
||||
}));
|
||||
} catch (err) {
|
||||
logger.error(`[EXTENSION]: page-registration failed`, {
|
||||
items,
|
||||
extension: ext,
|
||||
error: String(err),
|
||||
});
|
||||
const items = [pages].flat().map(page => this.registerPage(page, extension));
|
||||
return super.add(items);
|
||||
} catch (error) {
|
||||
return Function; // no-op
|
||||
}
|
||||
|
||||
return super.add(registeredPages);
|
||||
}
|
||||
|
||||
getUrl<P extends object>({ extensionId, id: pageId }: RegisteredPage, params?: P) {
|
||||
return getExtensionPageUrl({ extensionId, pageId, params });
|
||||
registerPage(page: PageRegistration, ext: LensExtension): RegisteredPage {
|
||||
try {
|
||||
const { id: pageId } = page;
|
||||
const extensionId = ext.name;
|
||||
return {
|
||||
...page,
|
||||
extensionId,
|
||||
url: getExtensionPageUrl({ extensionId, pageId }),
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`Failed to register page: ${error}`, { error });
|
||||
}
|
||||
}
|
||||
|
||||
getByPageMenuTarget(target: PageMenuTarget = {}): RegisteredPage | null {
|
||||
const targetUrl = getExtensionPageUrl(target);
|
||||
|
||||
return this.getItems().find(({ id: pageId, extensionId }) => {
|
||||
const pageUrl = getExtensionPageUrl({ extensionId, pageId, params: target.params }); // compiled with provided params
|
||||
|
||||
return targetUrl === pageUrl;
|
||||
}) || null;
|
||||
getByPageTarget(target: PageTarget): RegisteredPage | null {
|
||||
return this.getItems().find(page => page.extensionId === target.extensionId && page.id === target.pageId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
export { navigate } from "../../renderer/navigation";
|
||||
export { hideDetails, showDetails, getDetailsUrl } from "../../renderer/navigation";
|
||||
export { navigate, UrlParamInit, isActiveRoute, createUrlParam, UrlParam } from "../../renderer/navigation";
|
||||
export { hideDetails, showDetails, getDetailsUrl } from "../../renderer/components/kube-object/kube-object-details";
|
||||
export { IURLParams } from "../../common/utils/buildUrl";
|
||||
|
||||
@ -272,7 +272,7 @@ export class Kubectl {
|
||||
|
||||
logger.info(`Downloading kubectl ${this.kubectlVersion} from ${this.url} to ${this.path}`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const stream = customRequest({
|
||||
url: this.url,
|
||||
gzip: true,
|
||||
|
||||
@ -183,7 +183,7 @@ export class LensBinary {
|
||||
throw(error);
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
file.on("close", () => {
|
||||
this.logger.debug(`${this.originalBinaryName} binary download closed`);
|
||||
if (!this.tarPath) fs.chmod(binaryPath, 0o755, (err) => {
|
||||
|
||||
@ -20,13 +20,13 @@ import { Button } from "../button";
|
||||
import { releaseStore } from "./release.store";
|
||||
import { Notifications } from "../notifications";
|
||||
import { createUpgradeChartTab } from "../dock/upgrade-chart.store";
|
||||
import { getDetailsUrl } from "../../navigation";
|
||||
import { _i18n } from "../../i18n";
|
||||
import { themeStore } from "../../theme.store";
|
||||
import { apiManager } from "../../api/api-manager";
|
||||
import { SubTitle } from "../layout/sub-title";
|
||||
import { secretsStore } from "../+config-secrets/secrets.store";
|
||||
import { Secret } from "../../api/endpoints";
|
||||
import { getDetailsUrl } from "../kube-object";
|
||||
|
||||
interface Props {
|
||||
release: HelmRelease;
|
||||
@ -161,10 +161,7 @@ export class ReleaseDetails extends Component<Props> {
|
||||
const name = item.getName();
|
||||
const namespace = item.getNs();
|
||||
const api = apiManager.getApi(item.metadata.selfLink);
|
||||
const detailsUrl = api ? getDetailsUrl(api.getUrl({
|
||||
name,
|
||||
namespace,
|
||||
})) : "";
|
||||
const detailsUrl = api ? getDetailsUrl(api.getUrl({ name, namespace })) : "";
|
||||
|
||||
return (
|
||||
<TableRow key={item.getId()}>
|
||||
|
||||
@ -4,12 +4,12 @@ import { Trans } from "@lingui/macro";
|
||||
import { TabLayout, TabLayoutRoute } from "../layout/tab-layout";
|
||||
import { HelmCharts, helmChartsRoute, helmChartsURL } from "../+apps-helm-charts";
|
||||
import { HelmReleases, releaseRoute, releaseURL } from "../+apps-releases";
|
||||
import { namespaceStore } from "../+namespaces/namespace.store";
|
||||
import { namespaceUrlParam } from "../+namespaces/namespace.store";
|
||||
|
||||
@observer
|
||||
export class Apps extends React.Component {
|
||||
static get tabRoutes(): TabLayoutRoute[] {
|
||||
const query = namespaceStore.getContextParams();
|
||||
const query = namespaceUrlParam.toObjectParam();
|
||||
|
||||
return [
|
||||
{
|
||||
|
||||
@ -10,11 +10,11 @@ import { Table, TableCell, TableHead, TableRow } from "../table";
|
||||
import { nodesStore } from "../+nodes/nodes.store";
|
||||
import { eventStore } from "../+events/event.store";
|
||||
import { autobind, cssNames, prevDefault } from "../../utils";
|
||||
import { getSelectedDetails, showDetails } from "../../navigation";
|
||||
import { ItemObject } from "../../item.store";
|
||||
import { Spinner } from "../spinner";
|
||||
import { themeStore } from "../../theme.store";
|
||||
import { lookupApiLink } from "../../api/kube-api";
|
||||
import { kubeSelectedUrlParam, showDetails } from "../kube-object";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
@ -85,7 +85,7 @@ export class ClusterIssues extends React.Component<Props> {
|
||||
<TableRow
|
||||
key={getId()}
|
||||
sortItem={warning}
|
||||
selected={selfLink === getSelectedDetails()}
|
||||
selected={selfLink === kubeSelectedUrlParam.get()}
|
||||
onClick={prevDefault(() => showDetails(selfLink))}
|
||||
>
|
||||
<TableCell className="message">
|
||||
|
||||
@ -5,13 +5,12 @@ import { observer } from "mobx-react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { DrawerItem, DrawerTitle } from "../drawer";
|
||||
import { Badge } from "../badge";
|
||||
import { KubeObjectDetailsProps } from "../kube-object";
|
||||
import { KubeObjectDetailsProps, getDetailsUrl } from "../kube-object";
|
||||
import { cssNames } from "../../utils";
|
||||
import { HorizontalPodAutoscaler, HpaMetricType, IHpaMetric } from "../../api/endpoints/hpa.api";
|
||||
import { KubeEventDetails } from "../+events/kube-event-details";
|
||||
import { Trans } from "@lingui/macro";
|
||||
import { Table, TableCell, TableHead, TableRow } from "../table";
|
||||
import { getDetailsUrl } from "../../navigation";
|
||||
import { lookupApiLink } from "../../api/kube-api";
|
||||
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
||||
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
|
||||
|
||||
@ -17,8 +17,8 @@ import { Icon } from "../icon";
|
||||
import { IKubeObjectMetadata } from "../../api/kube-object";
|
||||
import { base64 } from "../../utils";
|
||||
import { Notifications } from "../notifications";
|
||||
import { showDetails } from "../../navigation";
|
||||
import upperFirst from "lodash/upperFirst";
|
||||
import { showDetails } from "../kube-object";
|
||||
|
||||
interface Props extends Partial<DialogProps> {
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ import { Trans } from "@lingui/macro";
|
||||
import { TabLayout, TabLayoutRoute } from "../layout/tab-layout";
|
||||
import { ConfigMaps, configMapsRoute, configMapsURL } from "../+config-maps";
|
||||
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 { pdbRoute, pdbURL, PodDisruptionBudgets } from "../+config-pod-disruption-budgets";
|
||||
import { HorizontalPodAutoscalers, hpaRoute, hpaURL } from "../+config-autoscalers";
|
||||
@ -13,7 +13,7 @@ import { isAllowedResource } from "../../../common/rbac";
|
||||
@observer
|
||||
export class Config extends React.Component {
|
||||
static get tabRoutes(): TabLayoutRoute[] {
|
||||
const query = namespaceStore.getContextParams();
|
||||
const query = namespaceUrlParam.toObjectParam();
|
||||
const routes: TabLayoutRoute[] = [];
|
||||
|
||||
if (isAllowedResource("configmaps")) {
|
||||
|
||||
@ -10,9 +10,16 @@ import { KubeObjectListLayout } from "../kube-object";
|
||||
import { crdStore } from "./crd.store";
|
||||
import { CustomResourceDefinition } from "../../api/endpoints/crd.api";
|
||||
import { Select, SelectOption } from "../select";
|
||||
import { navigation, setQueryParams } from "../../navigation";
|
||||
import { createUrlParam } from "../../navigation";
|
||||
import { Icon } from "../icon";
|
||||
|
||||
export const crdGroupsUrlParam = createUrlParam<string[]>({
|
||||
name: "groups",
|
||||
multiValues: true,
|
||||
isSystem: true,
|
||||
defaultValue: [],
|
||||
});
|
||||
|
||||
enum sortBy {
|
||||
kind = "kind",
|
||||
group = "group",
|
||||
@ -23,17 +30,18 @@ enum sortBy {
|
||||
|
||||
@observer
|
||||
export class CrdList extends React.Component {
|
||||
@computed get groups() {
|
||||
return navigation.searchParams.getAsArray("groups");
|
||||
@computed get groups(): string[] {
|
||||
return crdGroupsUrlParam.get();
|
||||
}
|
||||
|
||||
onGroupChange(group: string) {
|
||||
const groups = [...this.groups];
|
||||
const index = groups.findIndex(item => item == group);
|
||||
|
||||
if (index !== -1) groups.splice(index, 1);
|
||||
else groups.push(group);
|
||||
setQueryParams({ groups });
|
||||
onSelectGroup(group: string) {
|
||||
const groups = new Set(this.groups);
|
||||
if (groups.has(group)) {
|
||||
groups.delete(group); // toggle selection
|
||||
} else {
|
||||
groups.add(group);
|
||||
}
|
||||
crdGroupsUrlParam.set(Array.from(groups));
|
||||
}
|
||||
|
||||
render() {
|
||||
@ -71,7 +79,7 @@ export class CrdList extends React.Component {
|
||||
className="group-select"
|
||||
placeholder={placeholder}
|
||||
options={Object.keys(crdStore.groups)}
|
||||
onChange={({ value: group }: SelectOption) => this.onGroupChange(group)}
|
||||
onChange={({ value: group }: SelectOption) => this.onSelectGroup(group)}
|
||||
controlShouldRenderValue={false}
|
||||
formatOptionLabel={({ value: group }: SelectOption) => {
|
||||
const isSelected = selectedGroups.includes(group);
|
||||
|
||||
@ -6,10 +6,9 @@ import { Trans } from "@lingui/macro";
|
||||
import { DrawerItem, DrawerTitle } from "../drawer";
|
||||
import { Link } from "react-router-dom";
|
||||
import { observer } from "mobx-react";
|
||||
import { KubeObjectDetailsProps } from "../kube-object";
|
||||
import { KubeObjectDetailsProps, getDetailsUrl } from "../kube-object";
|
||||
import { KubeEvent } from "../../api/endpoints/events.api";
|
||||
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
||||
import { getDetailsUrl } from "../../navigation";
|
||||
import { Table, TableCell, TableHead, TableRow } from "../table";
|
||||
import { lookupApiLink } from "../../api/kube-api";
|
||||
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
|
||||
|
||||
@ -4,14 +4,13 @@ import React, { Fragment } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { TabLayout } from "../layout/tab-layout";
|
||||
import { eventStore } from "./event.store";
|
||||
import { KubeObjectListLayout, KubeObjectListLayoutProps } from "../kube-object";
|
||||
import { KubeObjectListLayout, KubeObjectListLayoutProps, getDetailsUrl } from "../kube-object";
|
||||
import { Trans } from "@lingui/macro";
|
||||
import { KubeEvent } from "../../api/endpoints/events.api";
|
||||
import { Tooltip } from "../tooltip";
|
||||
import { Link } from "react-router-dom";
|
||||
import { cssNames, IClassName, stopPropagation } from "../../utils";
|
||||
import { Icon } from "../icon";
|
||||
import { getDetailsUrl } from "../../navigation";
|
||||
import { lookupApiLink } from "../../api/kube-api";
|
||||
|
||||
enum sortBy {
|
||||
|
||||
@ -7,9 +7,8 @@ import { Trans } from "@lingui/macro";
|
||||
import { DrawerItem } from "../drawer";
|
||||
import { cssNames } from "../../utils";
|
||||
import { Namespace } from "../../api/endpoints";
|
||||
import { KubeObjectDetailsProps } from "../kube-object";
|
||||
import { getDetailsUrl, KubeObjectDetailsProps } from "../kube-object";
|
||||
import { Link } from "react-router-dom";
|
||||
import { getDetailsUrl } from "../../navigation";
|
||||
import { Spinner } from "../spinner";
|
||||
import { resourceQuotaStore } from "../+config-resource-quotas/resource-quotas.store";
|
||||
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
||||
|
||||
@ -1,45 +1,44 @@
|
||||
import { action, observable, reaction } from "mobx";
|
||||
import { action, comparer, observable, reaction } from "mobx";
|
||||
import { autobind, createStorage } from "../../utils";
|
||||
import { KubeObjectStore } from "../../kube-object.store";
|
||||
import { Namespace, namespacesApi } from "../../api/endpoints";
|
||||
import { IQueryParams, navigation, setQueryParams } from "../../navigation";
|
||||
import { createUrlParam } from "../../navigation";
|
||||
import { apiManager } from "../../api/api-manager";
|
||||
import { isAllowedResource } from "../../../common/rbac";
|
||||
import { getHostedCluster } from "../../../common/cluster-store";
|
||||
|
||||
const storage = createStorage<string[]>("context_namespaces", []);
|
||||
|
||||
export const namespaceUrlParam = createUrlParam<string[]>({
|
||||
name: "namespaces",
|
||||
isSystem: true,
|
||||
multiValues: true,
|
||||
get defaultValue() {
|
||||
return storage.get();
|
||||
}
|
||||
});
|
||||
|
||||
@autobind()
|
||||
export class NamespaceStore extends KubeObjectStore<Namespace> {
|
||||
api = namespacesApi;
|
||||
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();
|
||||
}
|
||||
contextNs = observable.array<string>(storage.get());
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// restore context namespaces
|
||||
const { initNamespaces: namespaces } = this;
|
||||
|
||||
this.setContext(namespaces);
|
||||
this.updateUrl(namespaces);
|
||||
|
||||
// sync with local-storage & url-search-params
|
||||
reaction(() => this.contextNs.toJS(), namespaces => {
|
||||
this.storage.set(namespaces);
|
||||
this.updateUrl(namespaces);
|
||||
});
|
||||
this.init();
|
||||
}
|
||||
|
||||
getContextParams(): Partial<IQueryParams> {
|
||||
return {
|
||||
namespaces: this.contextNs
|
||||
};
|
||||
private init() {
|
||||
// setup initial context namespaces from URL (when provided) or local-storage (default)
|
||||
this.setContext(namespaceUrlParam.get());
|
||||
|
||||
return reaction(() => this.contextNs.toJS(), namespaces => {
|
||||
storage.set(namespaces); // save to local-storage
|
||||
namespaceUrlParam.set(namespaces, { replaceHistory: true }); // update url
|
||||
}, {
|
||||
fireImmediately: true,
|
||||
equals: comparer.identity,
|
||||
});
|
||||
}
|
||||
|
||||
subscribe(apis = [this.api]) {
|
||||
@ -53,10 +52,6 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
||||
return super.subscribe(apis);
|
||||
}
|
||||
|
||||
protected updateUrl(namespaces: string[]) {
|
||||
setQueryParams({ namespaces }, { replace: true });
|
||||
}
|
||||
|
||||
protected async loadItems(namespaces?: string[]) {
|
||||
if (!isAllowedResource("namespaces")) {
|
||||
if (namespaces) return namespaces.map(this.getDummyNamespace);
|
||||
@ -84,6 +79,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
setContext(namespaces: string[]) {
|
||||
this.contextNs.replace(namespaces);
|
||||
}
|
||||
@ -94,6 +90,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
||||
return context.every(namespace => this.contextNs.includes(namespace));
|
||||
}
|
||||
|
||||
@action
|
||||
toggleContext(namespace: string) {
|
||||
if (this.hasContext(namespace)) this.contextNs.remove(namespace);
|
||||
else this.contextNs.push(namespace);
|
||||
@ -105,6 +102,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
||||
this.contextNs.clear();
|
||||
}
|
||||
|
||||
@action
|
||||
async remove(item: Namespace) {
|
||||
await super.remove(item);
|
||||
this.contextNs.remove(item.getName());
|
||||
|
||||
@ -7,8 +7,8 @@ import { Trans } from "@lingui/macro";
|
||||
import { Table, TableCell, TableHead, TableRow } from "../table";
|
||||
import { autobind } from "../../utils";
|
||||
import { lookupApiLink } from "../../api/kube-api";
|
||||
import { getDetailsUrl } from "../../navigation";
|
||||
import { Link } from "react-router-dom";
|
||||
import { getDetailsUrl } from "../kube-object";
|
||||
|
||||
interface Props {
|
||||
subset: EndpointSubset;
|
||||
|
||||
@ -3,10 +3,10 @@ import { observer } from "mobx-react";
|
||||
import React from "react";
|
||||
import { Table, TableHead, TableCell, TableRow } from "../table";
|
||||
import { prevDefault } from "../../utils";
|
||||
import { showDetails } from "../../navigation";
|
||||
import { Trans } from "@lingui/macro";
|
||||
import { endpointStore } from "../+network-endpoints/endpoints.store";
|
||||
import { Spinner } from "../spinner";
|
||||
import { showDetails } from "../kube-object";
|
||||
|
||||
interface Props {
|
||||
endpoint: KubeObject;
|
||||
|
||||
@ -8,13 +8,13 @@ import { Services, servicesRoute, servicesURL } from "../+network-services";
|
||||
import { endpointRoute, Endpoints, endpointURL } from "../+network-endpoints";
|
||||
import { Ingresses, ingressRoute, ingressURL } from "../+network-ingresses";
|
||||
import { NetworkPolicies, networkPoliciesRoute, networkPoliciesURL } from "../+network-policies";
|
||||
import { namespaceStore } from "../+namespaces/namespace.store";
|
||||
import { namespaceUrlParam } from "../+namespaces/namespace.store";
|
||||
import { isAllowedResource } from "../../../common/rbac";
|
||||
|
||||
@observer
|
||||
export class Network extends React.Component {
|
||||
static get tabRoutes(): TabLayoutRoute[] {
|
||||
const query = namespaceStore.getContextParams();
|
||||
const query = namespaceUrlParam.toObjectParam();
|
||||
const routes: TabLayoutRoute[] = [];
|
||||
|
||||
if (isAllowedResource("services")) {
|
||||
|
||||
@ -10,13 +10,11 @@ import { podsStore } from "../+workloads-pods/pods.store";
|
||||
import { Link } from "react-router-dom";
|
||||
import { KubeEventDetails } from "../+events/kube-event-details";
|
||||
import { volumeClaimStore } from "./volume-claim.store";
|
||||
import { getDetailsUrl } from "../../navigation";
|
||||
import { ResourceMetrics } from "../resource-metrics";
|
||||
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 { _i18n } from "../../i18n";
|
||||
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
||||
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<PersistentVolumeClaim> {
|
||||
|
||||
@ -7,11 +7,10 @@ import { Trans } from "@lingui/macro";
|
||||
import { volumeClaimStore } from "./volume-claim.store";
|
||||
import { PersistentVolumeClaim } from "../../api/endpoints/persistent-volume-claims.api";
|
||||
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 { unitsToBytes } from "../../utils/convertMemory";
|
||||
import { stopPropagation } from "../../utils";
|
||||
import { getDetailsUrl } from "../../navigation";
|
||||
import { storageClassApi } from "../../api/endpoints";
|
||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||
|
||||
|
||||
@ -8,9 +8,8 @@ import { observer } from "mobx-react";
|
||||
import { DrawerItem, DrawerTitle } from "../drawer";
|
||||
import { Badge } from "../badge";
|
||||
import { KubeEventDetails } from "../+events/kube-event-details";
|
||||
import { getDetailsUrl } from "../../navigation";
|
||||
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 { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
|
||||
|
||||
|
||||
@ -5,10 +5,9 @@ import { observer } from "mobx-react";
|
||||
import { Trans } from "@lingui/macro";
|
||||
import { Link, RouteComponentProps } from "react-router-dom";
|
||||
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 { stopPropagation } from "../../utils";
|
||||
import { getDetailsUrl } from "../../navigation";
|
||||
import { volumesStore } from "./volumes.store";
|
||||
import { pvcApi, storageClassApi } from "../../api/endpoints";
|
||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||
|
||||
@ -7,14 +7,14 @@ import { TabLayout, TabLayoutRoute } from "../layout/tab-layout";
|
||||
import { PersistentVolumes, volumesRoute, volumesURL } from "../+storage-volumes";
|
||||
import { StorageClasses, storageClassesRoute, storageClassesURL } from "../+storage-classes";
|
||||
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";
|
||||
|
||||
@observer
|
||||
export class Storage extends React.Component {
|
||||
static get tabRoutes() {
|
||||
const tabRoutes: TabLayoutRoute[] = [];
|
||||
const query = namespaceStore.getContextParams();
|
||||
const query = namespaceUrlParam.toObjectParam();
|
||||
|
||||
tabRoutes.push({
|
||||
title: <Trans>Persistent Volume Claims</Trans>,
|
||||
|
||||
@ -16,11 +16,11 @@ import { NamespaceSelect } from "../+namespaces/namespace-select";
|
||||
import { Checkbox } from "../checkbox";
|
||||
import { KubeObject } from "../../api/kube-object";
|
||||
import { Notifications } from "../notifications";
|
||||
import { showDetails } from "../../navigation";
|
||||
import { rolesStore } from "../+user-management-roles/roles.store";
|
||||
import { namespaceStore } from "../+namespaces/namespace.store";
|
||||
import { serviceAccountsStore } from "../+user-management-service-accounts/service-accounts.store";
|
||||
import { roleBindingsStore } from "./role-bindings.store";
|
||||
import { showDetails } from "../kube-object";
|
||||
|
||||
interface BindingSelectOption extends SelectOption {
|
||||
value: string; // binding name
|
||||
|
||||
@ -10,7 +10,7 @@ import { Wizard, WizardStep } from "../wizard";
|
||||
import { Notifications } from "../notifications";
|
||||
import { rolesStore } from "./roles.store";
|
||||
import { Input } from "../input";
|
||||
import { showDetails } from "../../navigation";
|
||||
import { showDetails } from "../kube-object";
|
||||
|
||||
interface Props extends Partial<DialogProps> {
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ import { Input } from "../input";
|
||||
import { systemName } from "../input/input_validators";
|
||||
import { NamespaceSelect } from "../+namespaces/namespace-select";
|
||||
import { Notifications } from "../notifications";
|
||||
import { showDetails } from "../../navigation";
|
||||
import { showDetails } from "../kube-object";
|
||||
|
||||
interface Props extends Partial<DialogProps> {
|
||||
}
|
||||
|
||||
@ -11,8 +11,7 @@ import { secretsStore } from "../+config-secrets/secrets.store";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Secret, ServiceAccount } from "../../api/endpoints";
|
||||
import { KubeEventDetails } from "../+events/kube-event-details";
|
||||
import { getDetailsUrl } from "../../navigation";
|
||||
import { KubeObjectDetailsProps } from "../kube-object";
|
||||
import { getDetailsUrl, KubeObjectDetailsProps } from "../kube-object";
|
||||
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
||||
import { Icon } from "../icon";
|
||||
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
|
||||
|
||||
@ -7,7 +7,7 @@ import { Roles } from "../+user-management-roles";
|
||||
import { RoleBindings } from "../+user-management-roles-bindings";
|
||||
import { ServiceAccounts } from "../+user-management-service-accounts";
|
||||
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 { isAllowedResource } from "../../../common/rbac";
|
||||
|
||||
@ -15,7 +15,7 @@ import { isAllowedResource } from "../../../common/rbac";
|
||||
export class UserManagement extends React.Component {
|
||||
static get tabRoutes() {
|
||||
const tabRoutes: TabLayoutRoute[] = [];
|
||||
const query = namespaceStore.getContextParams();
|
||||
const query = namespaceUrlParam.toObjectParam();
|
||||
|
||||
tabRoutes.push(
|
||||
{
|
||||
|
||||
@ -10,8 +10,7 @@ import { jobStore } from "../+workloads-jobs/job.store";
|
||||
import { Link } from "react-router-dom";
|
||||
import { KubeEventDetails } from "../+events/kube-event-details";
|
||||
import { cronJobStore } from "./cronjob.store";
|
||||
import { getDetailsUrl } from "../../navigation";
|
||||
import { KubeObjectDetailsProps } from "../kube-object";
|
||||
import { getDetailsUrl, KubeObjectDetailsProps } from "../kube-object";
|
||||
import { CronJob, Job } from "../../api/endpoints";
|
||||
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
||||
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
|
||||
|
||||
@ -13,8 +13,7 @@ import { PodDetailsAffinities } from "../+workloads-pods/pod-details-affinities"
|
||||
import { KubeEventDetails } from "../+events/kube-event-details";
|
||||
import { podsStore } from "../+workloads-pods/pods.store";
|
||||
import { jobStore } from "./job.store";
|
||||
import { getDetailsUrl } from "../../navigation";
|
||||
import { KubeObjectDetailsProps } from "../kube-object";
|
||||
import { getDetailsUrl, KubeObjectDetailsProps } from "../kube-object";
|
||||
import { Job } from "../../api/endpoints";
|
||||
import { PodDetailsList } from "../+workloads-pods/pod-details-list";
|
||||
import { lookupApiLink } from "../../api/kube-api";
|
||||
|
||||
@ -2,6 +2,7 @@ import "./pod-details-list.scss";
|
||||
|
||||
import React from "react";
|
||||
import kebabCase from "lodash/kebabCase";
|
||||
import { reaction } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { Trans } from "@lingui/macro";
|
||||
import { podsStore } from "./pods.store";
|
||||
@ -10,11 +11,10 @@ import { autobind, bytesToUnits, cssNames, interval, prevDefault } from "../../u
|
||||
import { LineProgress } from "../line-progress";
|
||||
import { KubeObject } from "../../api/kube-object";
|
||||
import { Table, TableCell, TableHead, TableRow } from "../table";
|
||||
import { showDetails } from "../../navigation";
|
||||
import { reaction } from "mobx";
|
||||
import { Spinner } from "../spinner";
|
||||
import { DrawerTitle } from "../drawer";
|
||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||
import { showDetails } from "../kube-object";
|
||||
|
||||
enum sortBy {
|
||||
name = "name",
|
||||
|
||||
@ -5,7 +5,7 @@ import { Link } from "react-router-dom";
|
||||
import { autorun, observable } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { Pod, Secret, secretsApi } from "../../api/endpoints";
|
||||
import { getDetailsUrl } from "../../navigation";
|
||||
import { getDetailsUrl } from "../kube-object";
|
||||
|
||||
interface Props {
|
||||
pod: Pod;
|
||||
|
||||
@ -18,8 +18,7 @@ import { KubeEventDetails } from "../+events/kube-event-details";
|
||||
import { PodDetailsSecrets } from "./pod-details-secrets";
|
||||
import { ResourceMetrics } from "../resource-metrics";
|
||||
import { podsStore } from "./pods.store";
|
||||
import { getDetailsUrl } from "../../navigation";
|
||||
import { KubeObjectDetailsProps } from "../kube-object";
|
||||
import { getDetailsUrl, KubeObjectDetailsProps } from "../kube-object";
|
||||
import { getItemMetrics } from "../../api/endpoints/metrics.api";
|
||||
import { PodCharts, podMetricTabs } from "./pod-charts";
|
||||
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
||||
|
||||
@ -9,11 +9,10 @@ import { RouteComponentProps } from "react-router";
|
||||
import { volumeClaimStore } from "../+storage-volume-claims/volume-claim.store";
|
||||
import { IPodsRouteParams } from "../+workloads";
|
||||
import { eventStore } from "../+events/event.store";
|
||||
import { KubeObjectListLayout } from "../kube-object";
|
||||
import { getDetailsUrl, KubeObjectListLayout } from "../kube-object";
|
||||
import { Pod } from "../../api/endpoints";
|
||||
import { StatusBrick } from "../status-brick";
|
||||
import { cssNames, stopPropagation } from "../../utils";
|
||||
import { getDetailsUrl } from "../../navigation";
|
||||
import toPairs from "lodash/toPairs";
|
||||
import startCase from "lodash/startCase";
|
||||
import kebabCase from "lodash/kebabCase";
|
||||
|
||||
@ -10,8 +10,8 @@ import { Spinner } from "../spinner";
|
||||
import { prevDefault, stopPropagation } from "../../utils";
|
||||
import { DrawerTitle } from "../drawer";
|
||||
import { Table, TableCell, TableHead, TableRow } from "../table";
|
||||
import { showDetails } from "../../navigation";
|
||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||
import { showDetails } from "../kube-object";
|
||||
|
||||
enum sortBy {
|
||||
name = "name",
|
||||
|
||||
@ -6,7 +6,7 @@ import { Trans } from "@lingui/macro";
|
||||
import { TabLayout, TabLayoutRoute } from "../layout/tab-layout";
|
||||
import { WorkloadsOverview } from "../+workloads-overview/overview";
|
||||
import { cronJobsRoute, cronJobsURL, daemonSetsRoute, daemonSetsURL, deploymentsRoute, deploymentsURL, jobsRoute, jobsURL, overviewRoute, overviewURL, podsRoute, podsURL, statefulSetsRoute, statefulSetsURL } from "./workloads.route";
|
||||
import { namespaceStore } from "../+namespaces/namespace.store";
|
||||
import { namespaceUrlParam } from "../+namespaces/namespace.store";
|
||||
import { Pods } from "../+workloads-pods";
|
||||
import { Deployments } from "../+workloads-deployments";
|
||||
import { DaemonSets } from "../+workloads-daemonsets";
|
||||
@ -18,7 +18,7 @@ import { isAllowedResource } from "../../../common/rbac";
|
||||
@observer
|
||||
export class Workloads extends React.Component {
|
||||
static get tabRoutes(): TabLayoutRoute[] {
|
||||
const query = namespaceStore.getContextParams();
|
||||
const query = namespaceUrlParam.toObjectParam();
|
||||
const routes: TabLayoutRoute[] = [
|
||||
{
|
||||
title: <Trans>Overview</Trans>,
|
||||
|
||||
@ -34,17 +34,17 @@ import { Terminal } from "./dock/terminal";
|
||||
import { getHostedCluster, getHostedClusterId } from "../../common/cluster-store";
|
||||
import logger from "../../main/logger";
|
||||
import { webFrame } from "electron";
|
||||
import { clusterPageRegistry, getExtensionPageUrl } from "../../extensions/registries/page-registry";
|
||||
import { clusterPageRegistry } from "../../extensions/registries/page-registry";
|
||||
import { extensionLoader } from "../../extensions/extension-loader";
|
||||
import { appEventBus } from "../../common/event-bus";
|
||||
import { broadcastMessage, requestMain } from "../../common/ipc";
|
||||
import whatInput from "what-input";
|
||||
import { clusterSetFrameIdHandler } from "../../common/cluster-ipc";
|
||||
import { ClusterPageMenuRegistration, clusterPageMenuRegistry } from "../../extensions/registries";
|
||||
import { TabLayoutRoute, TabLayout } from "./layout/tab-layout";
|
||||
import { TabLayout, TabLayoutRoute } from "./layout/tab-layout";
|
||||
import { StatefulSetScaleDialog } from "./+workloads-statefulsets/statefulset-scale-dialog";
|
||||
import { eventStore } from "./+events/event.store";
|
||||
import { reaction, computed } from "mobx";
|
||||
import { computed, reaction } from "mobx";
|
||||
import { nodesStore } from "./+nodes/nodes.store";
|
||||
import { podsStore } from "./+workloads-pods/pods.store";
|
||||
import { sum } from "lodash";
|
||||
@ -127,15 +127,14 @@ export class App extends React.Component {
|
||||
return routes;
|
||||
}
|
||||
clusterPageMenuRegistry.getSubItems(menuItem).forEach((item) => {
|
||||
const page = clusterPageRegistry.getByPageMenuTarget(item.target);
|
||||
const page = clusterPageRegistry.getByPageTarget(item.target);
|
||||
|
||||
if (page) {
|
||||
routes.push({
|
||||
routePath: page.routePath,
|
||||
url: getExtensionPageUrl({ extensionId: page.extensionId, pageId: page.id, params: item.target.params }),
|
||||
routePath: page.url,
|
||||
url: page.url,
|
||||
title: item.title,
|
||||
component: page.components.Page,
|
||||
exact: page.exact
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -148,16 +147,16 @@ export class App extends React.Component {
|
||||
const tabRoutes = this.getTabLayoutRoutes(menu);
|
||||
|
||||
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 {
|
||||
const page = clusterPageRegistry.getByPageMenuTarget(menu.target);
|
||||
const page = clusterPageRegistry.getByPageTarget(menu.target);
|
||||
|
||||
if (page) {
|
||||
const pageComponent = () => <page.components.Page />;
|
||||
const pageComponent = () => <page.components.Page/>;
|
||||
|
||||
return <Route key={`extension-tab-layout-route-${index}`} path={page.routePath} exact={page.exact} component={pageComponent}/>;
|
||||
return <Route key={`extension-tab-layout-route-${index}`} path={page.url} component={pageComponent}/>;
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -168,7 +167,7 @@ export class App extends React.Component {
|
||||
const menu = clusterPageMenuRegistry.getByPage(page);
|
||||
|
||||
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}/>;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -71,8 +71,8 @@ export class ClusterManager extends React.Component {
|
||||
<Route component={AddCluster} {...addClusterRoute} />
|
||||
<Route component={ClusterView} {...clusterViewRoute} />
|
||||
<Route component={ClusterSettings} {...clusterSettingsRoute} />
|
||||
{globalPageRegistry.getItems().map(({ routePath, exact, components: { Page } }) => {
|
||||
return <Route key={routePath} path={routePath} component={Page} exact={exact}/>;
|
||||
{globalPageRegistry.getItems().map(({ url, components: { Page } }) => {
|
||||
return <Route key={url} path={url} component={Page}/>;
|
||||
})}
|
||||
<Redirect exact to={this.startUrl}/>
|
||||
</Switch>
|
||||
|
||||
@ -15,14 +15,14 @@ import { ClusterIcon } from "../cluster-icon";
|
||||
import { Icon } from "../icon";
|
||||
import { autobind, cssNames, IClassName } from "../../utils";
|
||||
import { Badge } from "../badge";
|
||||
import { navigate, navigation } from "../../navigation";
|
||||
import { isActiveRoute, navigate } from "../../navigation";
|
||||
import { addClusterURL } from "../+add-cluster";
|
||||
import { clusterSettingsURL } from "../+cluster-settings";
|
||||
import { landingURL } from "../+landing-page";
|
||||
import { Tooltip } from "../tooltip";
|
||||
import { ConfirmDialog } from "../confirm-dialog";
|
||||
import { clusterViewURL } from "./cluster-view.route";
|
||||
import { getExtensionPageUrl, globalPageMenuRegistry, globalPageRegistry } from "../../../extensions/registries";
|
||||
import { globalPageMenuRegistry, globalPageRegistry } from "../../../extensions/registries";
|
||||
import { clusterDisconnectHandler } from "../../../common/cluster-ipc";
|
||||
|
||||
interface Props {
|
||||
@ -158,18 +158,14 @@ export class ClustersMenu extends React.Component<Props> {
|
||||
</div>
|
||||
<div className="extensions">
|
||||
{globalPageMenuRegistry.getItems().map(({ title, target, components: { Icon } }) => {
|
||||
const registeredPage = globalPageRegistry.getByPageMenuTarget(target);
|
||||
|
||||
const registeredPage = globalPageRegistry.getByPageTarget(target);
|
||||
if (!registeredPage) return;
|
||||
const { extensionId, id: pageId } = registeredPage;
|
||||
const pageUrl = getExtensionPageUrl({ extensionId, pageId, params: target.params });
|
||||
const isActive = pageUrl === navigation.location.pathname;
|
||||
|
||||
const { url: pageUrl } = registeredPage;
|
||||
return (
|
||||
<Icon
|
||||
key={pageUrl}
|
||||
tooltip={title}
|
||||
active={isActive}
|
||||
active={isActiveRoute(pageUrl)}
|
||||
onClick={() => navigate(pageUrl)}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -2,9 +2,15 @@ import React from "react";
|
||||
import debounce from "lodash/debounce";
|
||||
import { autorun, observable } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { getSearch, setSearch } from "../../navigation";
|
||||
import { InputProps } from "./input";
|
||||
import { SearchInput } from "./search-input";
|
||||
import { createUrlParam } from "../../navigation";
|
||||
|
||||
export const searchUrlParam = createUrlParam({
|
||||
name: "search",
|
||||
isSystem: true,
|
||||
defaultValue: "",
|
||||
});
|
||||
|
||||
interface Props extends InputProps {
|
||||
compact?: boolean; // show only search-icon when not focused
|
||||
@ -12,11 +18,11 @@ interface Props extends InputProps {
|
||||
|
||||
@observer
|
||||
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
|
||||
updateInput = autorun(() => this.inputVal = getSearch());
|
||||
updateUrl = debounce((val: string) => setSearch(val), 250);
|
||||
updateInput = autorun(() => this.inputVal = searchUrlParam.get());
|
||||
updateUrl = debounce((val: string) => searchUrlParam.set(val), 250);
|
||||
|
||||
setValue = (value: string) => {
|
||||
this.inputVal = value;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { computed, observable, reaction } from "mobx";
|
||||
import { autobind } from "../../utils";
|
||||
import { getSearch, setSearch } from "../../navigation";
|
||||
import { namespaceStore } from "../+namespaces/namespace.store";
|
||||
import { searchUrlParam } from "../input/search-input-url";
|
||||
|
||||
export enum FilterType {
|
||||
SEARCH = "search",
|
||||
@ -54,8 +54,8 @@ export class PageFiltersStore {
|
||||
|
||||
protected syncWithGlobalSearch() {
|
||||
const disposers = [
|
||||
reaction(() => this.getValues(FilterType.SEARCH)[0], setSearch),
|
||||
reaction(() => getSearch(), search => {
|
||||
reaction(() => this.getValues(FilterType.SEARCH)[0], search => searchUrlParam.set(search)),
|
||||
reaction(() => searchUrlParam.get(), search => {
|
||||
const filter = this.getByType(FilterType.SEARCH);
|
||||
|
||||
if (filter) {
|
||||
|
||||
@ -4,7 +4,7 @@ import React from "react";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { computed, observable, reaction } from "mobx";
|
||||
import { Trans } from "@lingui/macro";
|
||||
import { getDetails, hideDetails } from "../../navigation";
|
||||
import { createUrlParam, navigation } from "../../navigation";
|
||||
import { Drawer } from "../drawer";
|
||||
import { KubeObject } from "../../api/kube-object";
|
||||
import { Spinner } from "../spinner";
|
||||
@ -14,6 +14,38 @@ import { CrdResourceDetails } from "../+custom-resources";
|
||||
import { KubeObjectMenu } from "./kube-object-menu";
|
||||
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
|
||||
|
||||
export const kubeDetailsUrlParam = createUrlParam({
|
||||
name: "kube-details",
|
||||
isSystem: true,
|
||||
});
|
||||
|
||||
export const kubeSelectedUrlParam = createUrlParam({
|
||||
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(details);
|
||||
if (resetSelected) {
|
||||
const params = new URLSearchParams(detailsUrl);
|
||||
params.delete(kubeSelectedUrlParam.name);
|
||||
return `?${params.toString()}`;
|
||||
}
|
||||
return detailsUrl;
|
||||
}
|
||||
|
||||
export interface KubeObjectDetailsProps<T = KubeObject> {
|
||||
className?: string;
|
||||
object: T;
|
||||
@ -25,7 +57,7 @@ export class KubeObjectDetails extends React.Component {
|
||||
@observable.ref loadingError: React.ReactNode;
|
||||
|
||||
@computed get path() {
|
||||
return getDetails();
|
||||
return kubeDetailsUrlParam.get();
|
||||
}
|
||||
|
||||
@computed get object() {
|
||||
@ -70,7 +102,7 @@ export class KubeObjectDetails extends React.Component {
|
||||
const { object, isLoading, loadingError, isCrdInstance } = this;
|
||||
const isOpen = !!(object || isLoading || loadingError);
|
||||
let title = "";
|
||||
let details: JSX.Element[];
|
||||
let details: React.ReactNode[];
|
||||
|
||||
if (object) {
|
||||
const { kind, getName } = object;
|
||||
@ -81,7 +113,7 @@ export class KubeObjectDetails extends React.Component {
|
||||
});
|
||||
|
||||
if (isCrdInstance && details.length === 0) {
|
||||
details.push(<CrdResourceDetails object={object} />);
|
||||
details.push(<CrdResourceDetails object={object}/>);
|
||||
}
|
||||
}
|
||||
|
||||
@ -90,7 +122,7 @@ export class KubeObjectDetails extends React.Component {
|
||||
className="KubeObjectDetails flex column"
|
||||
open={isOpen}
|
||||
title={title}
|
||||
toolbar={<KubeObjectMenu object={object} toolbar={true} />}
|
||||
toolbar={<KubeObjectMenu object={object} toolbar={true}/>}
|
||||
onClose={hideDetails}
|
||||
>
|
||||
{isLoading && <Spinner center/>}
|
||||
|
||||
@ -3,10 +3,10 @@ import { computed } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { cssNames } from "../../utils";
|
||||
import { KubeObject } from "../../api/kube-object";
|
||||
import { getSelectedDetails, showDetails } from "../../navigation";
|
||||
import { ItemListLayout, ItemListLayoutProps } from "../item-object-list/item-list-layout";
|
||||
import { KubeObjectStore } from "../../kube-object.store";
|
||||
import { KubeObjectMenu } from "./kube-object-menu";
|
||||
import { kubeSelectedUrlParam, showDetails } from "./kube-object-details";
|
||||
|
||||
export interface KubeObjectListLayoutProps extends ItemListLayoutProps {
|
||||
store: KubeObjectStore;
|
||||
@ -15,14 +15,13 @@ export interface KubeObjectListLayoutProps extends ItemListLayoutProps {
|
||||
@observer
|
||||
export class KubeObjectListLayout extends React.Component<KubeObjectListLayoutProps> {
|
||||
@computed get selectedItem() {
|
||||
return this.props.store.getByPath(getSelectedDetails());
|
||||
return this.props.store.getByPath(kubeSelectedUrlParam.get());
|
||||
}
|
||||
|
||||
onDetails = (item: KubeObject) => {
|
||||
if (this.props.onDetails) {
|
||||
this.props.onDetails(item);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
showDetails(item.selfLink);
|
||||
}
|
||||
};
|
||||
|
||||
@ -4,7 +4,7 @@ import { autobind, cssNames } from "../../utils";
|
||||
import { KubeObject } from "../../api/kube-object";
|
||||
import { editResourceTab } from "../dock/edit-resource.store";
|
||||
import { MenuActions, MenuActionsProps } from "../menu/menu-actions";
|
||||
import { hideDetails } from "../../navigation";
|
||||
import { hideDetails } from "./kube-object-details";
|
||||
import { apiManager } from "../../api/api-manager";
|
||||
import { kubeObjectMenuRegistry } from "../../../extensions/registries/kube-object-menu-registry";
|
||||
|
||||
|
||||
@ -2,10 +2,10 @@ import React from "react";
|
||||
import { Trans } from "@lingui/macro";
|
||||
import { IKubeMetaField, KubeObject } from "../../api/kube-object";
|
||||
import { DrawerItem, DrawerItemLabels } from "../drawer";
|
||||
import { getDetailsUrl } from "../../navigation";
|
||||
import { lookupApiLink } from "../../api/kube-api";
|
||||
import { Link } from "react-router-dom";
|
||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||
import { getDetailsUrl } from "./kube-object-details";
|
||||
|
||||
export interface KubeObjectMetaProps {
|
||||
object: KubeObject;
|
||||
|
||||
@ -18,7 +18,7 @@ import { clusterRoute, clusterURL } from "../+cluster";
|
||||
import { Config, configRoute, configURL } from "../+config";
|
||||
import { eventRoute, eventsURL } from "../+events";
|
||||
import { Apps, appsRoute, appsURL } from "../+apps";
|
||||
import { namespaceStore } from "../+namespaces/namespace.store";
|
||||
import { namespaceUrlParam } from "../+namespaces/namespace.store";
|
||||
import { Workloads } from "../+workloads";
|
||||
import { UserManagement } from "../+user-management";
|
||||
import { Storage } from "../+storage";
|
||||
@ -29,7 +29,7 @@ import { CustomResources } from "../+custom-resources/custom-resources";
|
||||
import { isActiveRoute } from "../../navigation";
|
||||
import { isAllowedResource } from "../../../common/rbac";
|
||||
import { Spinner } from "../spinner";
|
||||
import { ClusterPageMenuRegistration, clusterPageMenuRegistry, clusterPageRegistry, getExtensionPageUrl } from "../../../extensions/registries";
|
||||
import { ClusterPageMenuRegistration, clusterPageMenuRegistry, clusterPageRegistry } from "../../../extensions/registries";
|
||||
|
||||
const SidebarContext = React.createContext<SidebarContextValue>({ pinned: false });
|
||||
|
||||
@ -79,21 +79,19 @@ export class Sidebar extends React.Component<Props> {
|
||||
}
|
||||
|
||||
getTabLayoutRoutes(menu: ClusterPageMenuRegistration): TabLayoutRoute[] {
|
||||
if (!menu.id) {
|
||||
return [];
|
||||
}
|
||||
const routes: TabLayoutRoute[] = [];
|
||||
if (!menu.id) {
|
||||
return routes;
|
||||
}
|
||||
|
||||
clusterPageMenuRegistry.getSubItems(menu).forEach((subItem) => {
|
||||
const subPage = clusterPageRegistry.getByPageMenuTarget(subItem.target);
|
||||
|
||||
const subPage = clusterPageRegistry.getByPageTarget(subItem.target);
|
||||
if (subPage) {
|
||||
routes.push({
|
||||
routePath: subPage.routePath,
|
||||
url: getExtensionPageUrl({ extensionId: subPage.extensionId, pageId: subPage.id, params: subItem.target.params }),
|
||||
routePath: subPage.url,
|
||||
url: subPage.url,
|
||||
title: subItem.title,
|
||||
component: subPage.components.Page,
|
||||
exact: subPage.exact
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -102,27 +100,24 @@ export class Sidebar extends React.Component<Props> {
|
||||
}
|
||||
|
||||
renderRegisteredMenus() {
|
||||
return clusterPageMenuRegistry.getRootItems().map((menuItem, index) => {
|
||||
const registeredPage = clusterPageRegistry.getByPageMenuTarget(menuItem.target);
|
||||
const tabRoutes = this.getTabLayoutRoutes(menuItem);
|
||||
let pageUrl: string;
|
||||
let isActive = false;
|
||||
|
||||
if (registeredPage) {
|
||||
const { extensionId, id: pageId } = registeredPage;
|
||||
|
||||
pageUrl = getExtensionPageUrl({ extensionId, pageId, params: menuItem.target.params });
|
||||
isActive = isActiveRoute(registeredPage.routePath);
|
||||
} else if (tabRoutes.length > 0) {
|
||||
pageUrl = tabRoutes[0].url;
|
||||
isActive = isActiveRoute(tabRoutes.map((tab) => tab.routePath));
|
||||
} else {
|
||||
return clusterPageMenuRegistry.getRootItems().map((menuItem) => {
|
||||
const registeredPage = clusterPageRegistry.getByPageTarget(menuItem.target);
|
||||
if (!registeredPage) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tabRoutes = this.getTabLayoutRoutes(menuItem);
|
||||
let pageUrl = registeredPage.url;
|
||||
let isActive = isActiveRoute(pageUrl);
|
||||
|
||||
if (tabRoutes.length > 0) {
|
||||
pageUrl = (tabRoutes.find(tab => tab.default) || tabRoutes[0]).url;
|
||||
isActive = isActiveRoute(tabRoutes.map((tab) => tab.routePath));
|
||||
}
|
||||
|
||||
return (
|
||||
<SidebarNavItem
|
||||
key={`registered-item-${index}`}
|
||||
key={pageUrl}
|
||||
url={pageUrl}
|
||||
text={menuItem.title}
|
||||
icon={<menuItem.components.Icon/>}
|
||||
@ -135,7 +130,7 @@ export class Sidebar extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { toggle, isPinned, className } = this.props;
|
||||
const query = namespaceStore.getContextParams();
|
||||
const query = namespaceUrlParam.toObjectParam();
|
||||
|
||||
return (
|
||||
<SidebarContext.Provider value={{ pinned: isPinned }}>
|
||||
|
||||
@ -50,7 +50,7 @@ export class Select extends React.Component<SelectProps> {
|
||||
}),
|
||||
};
|
||||
|
||||
protected isValidOption(opt: SelectOption | any) {
|
||||
protected isValidOption(opt: SelectOption | any): opt is SelectOption {
|
||||
return typeof opt === "object" && opt.value !== undefined;
|
||||
}
|
||||
|
||||
@ -72,7 +72,7 @@ export class Select extends React.Component<SelectProps> {
|
||||
const { autoConvertOptions, options } = this.props;
|
||||
|
||||
if (autoConvertOptions && Array.isArray(options)) {
|
||||
return options.map(opt => {
|
||||
return (options as any[]).map(opt => {
|
||||
return this.isValidOption(opt) ? opt : { value: opt, label: String(opt) };
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,19 +1,17 @@
|
||||
import "./table.scss";
|
||||
|
||||
import React from "react";
|
||||
import lodash from "lodash";
|
||||
import { observer } from "mobx-react";
|
||||
import { computed, observable } from "mobx";
|
||||
import { observable } from "mobx";
|
||||
import { autobind, cssNames, noop } from "../../utils";
|
||||
import { TableRow, TableRowElem, TableRowProps } from "./table-row";
|
||||
import { TableHead, TableHeadElem, TableHeadProps } from "./table-head";
|
||||
import { TableCellElem } from "./table-cell";
|
||||
import { VirtualList } from "../virtual-list";
|
||||
import { navigation, setQueryParams } from "../../navigation";
|
||||
import orderBy from "lodash/orderBy";
|
||||
import { createUrlParam } from "../../navigation";
|
||||
import { ItemObject } from "../../item.store";
|
||||
|
||||
// todo: refactor + decouple search from location
|
||||
|
||||
export type TableSortBy = string;
|
||||
export type TableOrderBy = "asc" | "desc" | string;
|
||||
export type TableSortParams = { sortBy: TableSortBy; orderBy: TableOrderBy };
|
||||
@ -43,6 +41,16 @@ export interface TableProps extends React.DOMAttributes<HTMLDivElement> {
|
||||
getTableRow?: (uid: string) => React.ReactElement<TableRowProps>;
|
||||
}
|
||||
|
||||
export const sortByUrlParam = createUrlParam({
|
||||
name: "sort",
|
||||
isSystem: true,
|
||||
});
|
||||
|
||||
export const orderByUrlParam = createUrlParam({
|
||||
name: "order",
|
||||
isSystem: true,
|
||||
});
|
||||
|
||||
@observer
|
||||
export class Table extends React.Component<TableProps> {
|
||||
static defaultProps: TableProps = {
|
||||
@ -53,18 +61,13 @@ export class Table extends React.Component<TableProps> {
|
||||
sortSyncWithUrl: true,
|
||||
};
|
||||
|
||||
@observable sortParamsLocal = this.props.sortByDefault;
|
||||
|
||||
@computed get sortParams(): Partial<TableSortParams> {
|
||||
if (this.props.sortSyncWithUrl) {
|
||||
const sortBy = navigation.searchParams.get("sortBy");
|
||||
const orderBy = navigation.searchParams.get("orderBy");
|
||||
|
||||
return { sortBy, orderBy };
|
||||
}
|
||||
|
||||
return this.sortParamsLocal || {};
|
||||
}
|
||||
@observable sortParams: Partial<TableSortParams> = Object.assign(
|
||||
this.props.sortSyncWithUrl ? {
|
||||
sortBy: sortByUrlParam.get(),
|
||||
orderBy: orderByUrlParam.get(),
|
||||
} : {},
|
||||
this.props.sortByDefault,
|
||||
);
|
||||
|
||||
renderHead() {
|
||||
const { sortable, children } = this.props;
|
||||
@ -101,29 +104,24 @@ export class Table extends React.Component<TableProps> {
|
||||
}
|
||||
|
||||
getSorted(items: any[]) {
|
||||
const { sortParams } = this;
|
||||
const sortingCallback = this.props.sortable[sortParams.sortBy] || noop;
|
||||
const { sortBy, orderBy } = this.sortParams;
|
||||
const sortingCallback = this.props.sortable[sortBy] || noop;
|
||||
|
||||
return orderBy(
|
||||
items,
|
||||
sortingCallback,
|
||||
sortParams.orderBy as any
|
||||
);
|
||||
return lodash.orderBy(items, sortingCallback, orderBy as any);
|
||||
}
|
||||
|
||||
@autobind()
|
||||
protected onSort(params: TableSortParams) {
|
||||
protected onSort({ sortBy, orderBy }: TableSortParams) {
|
||||
this.sortParams = { sortBy, orderBy };
|
||||
const { sortSyncWithUrl, onSort } = this.props;
|
||||
|
||||
if (sortSyncWithUrl) {
|
||||
setQueryParams(params);
|
||||
}
|
||||
else {
|
||||
this.sortParamsLocal = params;
|
||||
sortByUrlParam.set(sortBy);
|
||||
orderByUrlParam.set(orderBy);
|
||||
}
|
||||
|
||||
if (onSort) {
|
||||
onSort(params);
|
||||
onSort({ sortBy, orderBy });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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();
|
||||
});
|
||||
30
src/renderer/navigation/helpers.ts
Normal file
30
src/renderer/navigation/helpers.ts
Normal file
@ -0,0 +1,30 @@
|
||||
// Navigation helpers
|
||||
|
||||
import { matchPath, RouteProps } from "react-router";
|
||||
import { LocationDescriptor } from "history";
|
||||
import { clusterViewRoute, IClusterViewRouteParams } from "../components/cluster-manager/cluster-view.route";
|
||||
import { navigation } from "./index";
|
||||
|
||||
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;
|
||||
}
|
||||
47
src/renderer/navigation/index.ts
Normal file
47
src/renderer/navigation/index.ts
Normal file
@ -0,0 +1,47 @@
|
||||
// Navigation helpers
|
||||
|
||||
import { ipcRenderer } from "electron";
|
||||
import logger from "../../main/logger";
|
||||
import { reaction } from "mobx";
|
||||
import { createObservableHistory } from "mobx-observable-history";
|
||||
import { createBrowserHistory, createMemoryHistory } from "history";
|
||||
import { broadcastMessage, subscribeToBroadcast } from "../../common/ipc";
|
||||
import { getMatchedClusterId, navigate } from "./helpers";
|
||||
import { UrlParam, UrlParamInit } from "./url-param";
|
||||
|
||||
export let history = ipcRenderer ? createBrowserHistory() : createMemoryHistory();
|
||||
export let navigation = createObservableHistory(history);
|
||||
|
||||
export function createUrlParam<V = string>(init: UrlParamInit<V>) {
|
||||
return new UrlParam<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();
|
||||
});
|
||||
}
|
||||
|
||||
// Re-exports from sub-modules
|
||||
export * from "./helpers";
|
||||
export * from "./url-param";
|
||||
|
||||
101
src/renderer/navigation/url-param.ts
Normal file
101
src/renderer/navigation/url-param.ts
Normal file
@ -0,0 +1,101 @@
|
||||
// Manage observable URL-param via location.search
|
||||
import { IObservableHistory } from "mobx-observable-history";
|
||||
|
||||
export interface UrlParamInit<V = any> {
|
||||
name: string;
|
||||
isSystem?: boolean;
|
||||
defaultValue?: V;
|
||||
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
|
||||
}
|
||||
|
||||
export class UrlParam<V = any | any[]> {
|
||||
static SYSTEM_PREFIX = "lens-";
|
||||
|
||||
public name: string;
|
||||
public urlName: string;
|
||||
|
||||
constructor(private init: UrlParamInit<V>, private history: IObservableHistory) {
|
||||
const { isSystem, name, skipEmpty = true } = init;
|
||||
|
||||
this.name = name;
|
||||
this.init.skipEmpty = skipEmpty;
|
||||
|
||||
// prefixing to avoid collisions with extensions
|
||||
this.urlName = `${isSystem ? UrlParam.SYSTEM_PREFIX : ""}${name}`;
|
||||
}
|
||||
|
||||
isEmpty(value: V) {
|
||||
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 { history, urlName } = this;
|
||||
const { multiValueSep, multiValues, defaultValue, skipEmpty } = this.init;
|
||||
const value = this.parse(history.searchParams.getAsArray(urlName, multiValueSep));
|
||||
|
||||
if (skipEmpty && this.isEmpty(value)) {
|
||||
return defaultValue;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
set(value: V, { mergeGlobals = true, replaceHistory = false } = {}) {
|
||||
const search = this.toSearchString(value, { mergeGlobals });
|
||||
this.history.merge({ search }, replaceHistory);
|
||||
}
|
||||
|
||||
isDefault() {
|
||||
return this.get() === this.init.defaultValue;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.history.searchParams.delete(this.urlName);
|
||||
}
|
||||
|
||||
toSearchString(value = this.get(), { withPrefix = true, mergeGlobals = true } = {}): 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -14545,12 +14545,7 @@ typeface-roboto@^0.0.75:
|
||||
resolved "https://registry.yarnpkg.com/typeface-roboto/-/typeface-roboto-0.0.75.tgz#98d5ba35ec234bbc7172374c8297277099cc712b"
|
||||
integrity sha512-VrR/IiH00Z1tFP4vDGfwZ1esNqTiDMchBEXYY9kilT6wRGgFoCAlgkEUMHb1E3mB0FsfZhv756IF0+R+SFPfdg==
|
||||
|
||||
typescript@^4.0.2:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.2.tgz#7ea7c88777c723c681e33bf7988be5d008d05ac2"
|
||||
integrity sha512-e4ERvRV2wb+rRZ/IQeb3jm2VxBsirQLpQhdxplZ2MEzGvDkkMmPglecnNDfSUBivMjP93vRbngYYDQqQ/78bcQ==
|
||||
|
||||
typescript@^4.0.3:
|
||||
typescript@^4.0.2, typescript@^4.0.3:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.2.tgz#6369ef22516fe5e10304aae5a5c4862db55380e9"
|
||||
integrity sha512-thGloWsGH3SOxv1SoY7QojKi0tc+8FnOmiarEGMbd/lar7QOEd3hvlx3Fp5y6FlDUGl9L+pd4n2e+oToGMmhRQ==
|
||||
|
||||
Loading…
Reference in New Issue
Block a user