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

PageRegistration refactoring -- part 2

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman 2020-11-11 17:10:44 +02:00
parent ab097c410c
commit 9b55a5fd0a
44 changed files with 422 additions and 300 deletions

View File

@ -57,31 +57,30 @@ export class ExtensionLoader {
loadOnMain() {
logger.info('[EXTENSIONS-LOADER]: load on main')
this.autoInitExtensions((extension: LensMainExtension) => [
registries.menuRegistry.add(...extension.appMenus)
registries.menuRegistry.add(extension, extension.appMenus)
]);
}
loadOnClusterManagerRenderer() {
logger.info('[EXTENSIONS-LOADER]: load on main renderer (cluster manager)')
this.autoInitExtensions((extension: LensRendererExtension) => [
registries.globalPageRegistry.add(...extension.globalPages),
registries.globalPageMenuRegistry.add(...extension.globalPageMenus),
registries.appPreferenceRegistry.add(...extension.appPreferences),
registries.clusterFeatureRegistry.add(...extension.clusterFeatures),
registries.statusBarRegistry.add(...extension.statusBarItems),
registries.globalPageRegistry.add(extension, extension.globalPages),
registries.globalPageMenuRegistry.add(extension, extension.globalPageMenus),
registries.appPreferenceRegistry.add(extension, extension.appPreferences),
registries.clusterFeatureRegistry.add(extension, extension.clusterFeatures),
registries.statusBarRegistry.add(extension, extension.statusBarItems),
]);
}
loadOnClusterRenderer() {
logger.info('[EXTENSIONS-LOADER]: load on cluster renderer (dashboard)')
this.autoInitExtensions((extension: LensRendererExtension) => [
registries.clusterPageRegistry.add(...extension.clusterPages),
registries.clusterPageMenuRegistry.add(...extension.clusterPageMenus),
registries.kubeObjectMenuRegistry.add(...extension.kubeObjectMenuItems),
registries.kubeObjectDetailRegistry.add(...extension.kubeObjectDetailItems),
registries.kubeObjectStatusRegistry.add(...extension.kubeObjectStatusTexts)
registries.clusterPageRegistry.add(extension, extension.clusterPages),
registries.clusterPageMenuRegistry.add(extension, extension.clusterPageMenus),
registries.kubeObjectMenuRegistry.add(extension, extension.kubeObjectMenuItems),
registries.kubeObjectDetailRegistry.add(extension, extension.kubeObjectDetailItems),
registries.kubeObjectStatusRegistry.add(extension, extension.kubeObjectStatusTexts)
])
}
protected autoInitExtensions(register: (ext: LensExtension) => Function[]) {

View File

@ -4,4 +4,5 @@ export type { KubeObjectDetailRegistration, KubeObjectDetailComponents } from ".
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 { PageMenuRegistration, PageMenuComponents } from "../registries/page-menu-registry"
export type { StatusBarRegistration } from "../registries/status-bar-registry"

View File

@ -1,5 +1,6 @@
import type { InstalledExtension } from "./extension-manager";
import { action, observable, reaction } from "mobx";
import { compile } from "path-to-regexp"
import logger from "../main/logger";
export type LensExtensionId = string; // path to manifest (package.json)
@ -14,6 +15,7 @@ export interface LensExtensionManifest {
}
export class LensExtension {
readonly routePrefix = "/extensions/:name"
readonly manifest: LensExtensionManifest;
readonly manifestPath: string;
readonly isBundled: boolean;
@ -42,6 +44,14 @@ export class LensExtension {
return this.manifest.description
}
getPageUrl(baseUrl: string) {
return compile(this.routePrefix)({ name: this.name }) + "/" + baseUrl
}
getPageRoute(baseRoute: string) {
return this.routePrefix + "/" + baseRoute;
}
@action
async enable() {
if (this.isEnabled) return;

View File

@ -1,16 +1,16 @@
import type {
AppPreferenceRegistration, ClusterFeatureRegistration,
KubeObjectMenuRegistration, KubeObjectDetailRegistration, StatusBarRegistration, KubeObjectStatusRegistration,
PageRegistration, PageMenuRegistration, PageRegistrationCluster, PageMenuRegistrationCluster,
PageRegistration, PageMenuRegistration,
} from "./registries"
import { observable } from "mobx";
import { LensExtension } from "./lens-extension"
export class LensRendererExtension extends LensExtension {
@observable.shallow globalPages: PageRegistration[] = []
@observable.shallow clusterPages: PageRegistrationCluster[] = []
@observable.shallow clusterPages: PageRegistration[] = []
@observable.shallow globalPageMenus: PageMenuRegistration[] = []
@observable.shallow clusterPageMenus: PageMenuRegistrationCluster[] = []
@observable.shallow clusterPageMenus: PageMenuRegistration[] = []
@observable.shallow kubeObjectStatusTexts: KubeObjectStatusRegistration[] = []
@observable.shallow appPreferences: AppPreferenceRegistration[] = []
@observable.shallow clusterFeatures: ClusterFeatureRegistration[] = []

View File

@ -1,23 +1,37 @@
// Base class for extensions-api registries
import { action, observable } from "mobx";
import { LensExtension } from "../lens-extension";
export class BaseRegistry<T = any> {
protected items = observable<T>([], { deep: false });
export class BaseRegistry<T extends object = any> {
private items = observable.map<LensExtension, T[]>([], { deep: false });
getItems(): T[] {
return this.items.toJS();
getItems(): (T & { extension?: LensExtension })[] {
return Array.from(this.items).map(([ext, items]) => {
return items.map(item => ({
...item,
extension: ext,
}))
}).flat()
}
@action
add(...items: T[]) {
this.items.push(...items);
return () => this.remove(...items);
add(ext: LensExtension | null, items: T[], merge = true) {
if (merge && this.items.has(ext)) {
const newItems = new Set(this.items.get(ext));
items.forEach(item => newItems.add(item))
this.items.set(ext, [...newItems]);
} else {
this.items.set(ext, items);
}
return () => this.remove(ext, items)
}
@action
remove(...items: T[]) {
items.forEach(item => {
this.items.remove(item); // works because of {deep: false};
})
remove(ext: LensExtension | null, items: T[]) {
const storedItems = this.items.get(ext);
if (storedItems) {
const newItems = storedItems.filter(item => !items.includes(item)); // works because of {deep: false};
this.items.set(ext, newItems);
}
}
}

View File

@ -13,7 +13,7 @@ export interface KubeObjectDetailRegistration {
export class KubeObjectDetailRegistry extends BaseRegistry<KubeObjectDetailRegistration> {
getItemsForKind(kind: string, apiVersion: string) {
return this.items.filter((item) => {
return this.getItems().filter((item) => {
return item.kind === kind && item.apiVersions.includes(apiVersion)
})
}

View File

@ -13,7 +13,7 @@ export interface KubeObjectMenuRegistration {
export class KubeObjectMenuRegistry extends BaseRegistry<KubeObjectMenuRegistration> {
getItemsForKind(kind: string, apiVersion: string) {
return this.items.filter((item) => {
return this.getItems().filter((item) => {
return item.kind === kind && item.apiVersions.includes(apiVersion)
})
}

View File

@ -9,7 +9,7 @@ export interface KubeObjectStatusRegistration {
export class KubeObjectStatusRegistry extends BaseRegistry<KubeObjectStatusRegistration> {
getItemsForKind(kind: string, apiVersion: string) {
return this.items.filter((item) => {
return this.getItems().filter((item) => {
return item.kind === kind && item.apiVersions.includes(apiVersion)
})
}

View File

@ -3,14 +3,12 @@
import type React from "react";
import type { IconProps } from "../../renderer/components/icon";
import { BaseRegistry } from "./base-registry";
import { matchPath } from "react-router";
export interface PageMenuRegistration {
url: string;
title: React.ReactNode;
components: PageMenuComponents;
}
export interface PageMenuRegistrationCluster extends PageMenuRegistration {
subMenus?: Omit<PageMenuRegistration, "components" | "subMenus">[];
}
@ -18,8 +16,21 @@ export interface PageMenuComponents {
Icon: React.ComponentType<IconProps>;
}
export class PageMenuRegistry<T extends PageMenuRegistration> extends BaseRegistry<T> {
export class PageMenuRegistry extends BaseRegistry<PageMenuRegistration> {
getByMatchingRoute(routePath: string | string[], exact?: boolean) {
return this.getItems().find(item => !!matchPath(item.url, {
path: routePath,
exact,
}))
}
getItems() {
return super.getItems().map(item => {
item.url = item.extension.getPageUrl(item.url)
return item
});
}
}
export const globalPageMenuRegistry = new PageMenuRegistry<PageMenuRegistration>();
export const clusterPageMenuRegistry = new PageMenuRegistry<PageMenuRegistrationCluster>();
export const globalPageMenuRegistry = new PageMenuRegistry();
export const clusterPageMenuRegistry = new PageMenuRegistry();

View File

@ -1,29 +1,34 @@
// Extensions-api -> Custom page registration
import type React from "react";
import React from "react";
import { matchPath } from "react-router";
import { BaseRegistry } from "./base-registry";
export interface PageRegistration {
routePath: string; // react-router's path, e.g. "/page/:id"
exact?: boolean; // route matching flag, see: https://reactrouter.com/web/api/NavLink/exact-bool
components: PageComponents;
}
export interface PageRegistrationCluster extends PageRegistration {
subPages?: Omit<PageRegistration, "subPages">;
subPages?: Omit<PageRegistration, "subPages">[];
}
export interface PageComponents {
Page: React.ComponentType<any>;
}
export class PageRegistry<T extends PageRegistration> extends BaseRegistry<T> {
protected routePrefixPath = "/extensions/:name" // todo: figure out how to provide inside extension
export class PageRegistry extends BaseRegistry<PageRegistration> {
getByMatchingUrl(baseUrl: string) {
return this.getItems().find(({ routePath: path, exact }) => {
return !!matchPath(baseUrl, { path, exact });
})
}
getItems() {
return super.getItems();
return super.getItems().map(item => {
item.routePath = item.extension.getPageRoute(item.routePath)
return item
});
}
}
export const globalPageRegistry = new PageRegistry<PageRegistration>();
export const clusterPageRegistry = new PageRegistry<PageRegistrationCluster>();
export const globalPageRegistry = new PageRegistry();
export const clusterPageRegistry = new PageRegistry();

View File

@ -128,10 +128,12 @@ export class HpaDetails extends React.Component<Props> {
}
}
kubeObjectDetailRegistry.add({
kind: "HorizontalPodAutoscaler",
apiVersions: ["autoscaling/v1"],
components: {
Details: (props) => <HpaDetails {...props} />
kubeObjectDetailRegistry.add(null, [
{
kind: "HorizontalPodAutoscaler",
apiVersions: ["autoscaling/v1"],
components: {
Details: (props) => <HpaDetails {...props} />
}
}
})
])

View File

@ -94,10 +94,12 @@ export class ConfigMapDetails extends React.Component<Props> {
}
}
kubeObjectDetailRegistry.add({
kind: "ConfigMap",
apiVersions: ["v1"],
components: {
Details: (props) => <ConfigMapDetails {...props} />
kubeObjectDetailRegistry.add(null, [
{
kind: "ConfigMap",
apiVersions: ["v1"],
components: {
Details: (props) => <ConfigMapDetails {...props} />
}
}
})
])

View File

@ -54,10 +54,12 @@ export class PodDisruptionBudgetDetails extends React.Component<Props> {
}
}
kubeObjectDetailRegistry.add({
kind: "PodDisruptionBudget",
apiVersions: ["policy/v1beta1"],
components: {
Details: (props) => <PodDisruptionBudgetDetails {...props} />
kubeObjectDetailRegistry.add(null, [
{
kind: "PodDisruptionBudget",
apiVersions: ["policy/v1beta1"],
components: {
Details: (props) => <PodDisruptionBudgetDetails {...props} />
}
}
})
])

View File

@ -16,8 +16,6 @@ import { ReplicaSetDetails } from "../+workloads-replicasets";
interface Props extends KubeObjectDetailsProps<ResourceQuota> {
}
const onlyNumbers = /$[0-9]*^/g;
function transformUnit(name: string, value: string): number {
if (name.includes("memory") || name.includes("storage")) {
return unitsToBytes(value)
@ -98,10 +96,12 @@ export class ResourceQuotaDetails extends React.Component<Props> {
}
}
kubeObjectDetailRegistry.add({
kind: "ResourceQuota",
apiVersions: ["v1"],
components: {
Details: (props) => <ReplicaSetDetails {...props} />
kubeObjectDetailRegistry.add(null, [
{
kind: "ResourceQuota",
apiVersions: ["v1"],
components: {
Details: (props) => <ReplicaSetDetails {...props} />
}
}
})
])

View File

@ -113,10 +113,12 @@ export class SecretDetails extends React.Component<Props> {
}
}
kubeObjectDetailRegistry.add({
kind: "Secret",
apiVersions: ["v1"],
components: {
Details: (props) => <SecretDetails {...props} />
kubeObjectDetailRegistry.add(null, [
{
kind: "Secret",
apiVersions: ["v1"],
components: {
Details: (props) => <SecretDetails {...props} />
}
}
})
])

View File

@ -134,10 +134,12 @@ export class CRDDetails extends React.Component<Props> {
}
}
kubeObjectDetailRegistry.add({
kind: "CustomResourceDefinition",
apiVersions: ["apiextensions.k8s.io/v1", "apiextensions.k8s.io/v1beta1"],
components: {
Details: (props) => <CRDDetails {...props} />
kubeObjectDetailRegistry.add(null, [
{
kind: "CustomResourceDefinition",
apiVersions: ["apiextensions.k8s.io/v1", "apiextensions.k8s.io/v1beta1"],
components: {
Details: (props) => <CRDDetails {...props} />
}
}
})
])

View File

@ -74,10 +74,12 @@ export class EventDetails extends React.Component<Props> {
}
}
kubeObjectDetailRegistry.add({
kind: "Event",
apiVersions: ["v1"],
components: {
Details: (props) => <EventDetails {...props}/>
kubeObjectDetailRegistry.add(null, [
{
kind: "Event",
apiVersions: ["v1"],
components: {
Details: (props) => <EventDetails {...props}/>
}
}
})
])

View File

@ -56,10 +56,12 @@ export class NamespaceDetails extends React.Component<Props> {
}
}
kubeObjectDetailRegistry.add({
kind: "Namespace",
apiVersions: ["v1"],
components: {
Details: (props) => <NamespaceDetails {...props} />
kubeObjectDetailRegistry.add(null, [
{
kind: "Namespace",
apiVersions: ["v1"],
components: {
Details: (props) => <NamespaceDetails {...props} />
}
}
})
])

View File

@ -7,7 +7,6 @@ import { DrawerTitle } from "../drawer";
import { KubeEventDetails } from "../+events/kube-event-details";
import { KubeObjectDetailsProps } from "../kube-object";
import { Endpoint } from "../../api/endpoints";
import { _i18n } from "../../i18n";
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { EndpointSubsetList } from "./endpoint-subset-list";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
@ -38,10 +37,12 @@ export class EndpointDetails extends React.Component<Props> {
}
}
kubeObjectDetailRegistry.add({
kind: "Endpoints",
apiVersions: ["v1"],
components: {
Details: (props) => <EndpointDetails {...props} />
kubeObjectDetailRegistry.add(null, [
{
kind: "Endpoints",
apiVersions: ["v1"],
components: {
Details: (props) => <EndpointDetails {...props} />
}
}
})
])

View File

@ -134,10 +134,12 @@ export class IngressDetails extends React.Component<Props> {
}
}
kubeObjectDetailRegistry.add({
kind: "Ingress",
apiVersions: ["extensions/v1beta1"],
components: {
Details: (props) => <IngressDetails {...props} />
kubeObjectDetailRegistry.add(null, [
{
kind: "Ingress",
apiVersions: ["extensions/v1beta1"],
components: {
Details: (props) => <IngressDetails {...props} />
}
}
})
])

View File

@ -144,10 +144,12 @@ export class NetworkPolicyDetails extends React.Component<Props> {
}
}
kubeObjectDetailRegistry.add({
kind: "NetworkPolicy",
apiVersions: ["networking.k8s.io/v1"],
components: {
Details: (props) => <NetworkPolicyDetails {...props} />
kubeObjectDetailRegistry.add(null, [
{
kind: "NetworkPolicy",
apiVersions: ["networking.k8s.io/v1"],
components: {
Details: (props) => <NetworkPolicyDetails {...props} />
}
}
})
])

View File

@ -85,10 +85,12 @@ export class ServiceDetails extends React.Component<Props> {
}
}
kubeObjectDetailRegistry.add({
kind: "Service",
apiVersions: ["v1"],
components: {
Details: (props) => <ServiceDetails {...props} />
kubeObjectDetailRegistry.add(null, [
{
kind: "Service",
apiVersions: ["v1"],
components: {
Details: (props) => <ServiceDetails {...props} />
}
}
})
])

View File

@ -155,10 +155,12 @@ export class NodeDetails extends React.Component<Props> {
}
}
kubeObjectDetailRegistry.add({
kind: "Node",
apiVersions: ["v1"],
components: {
Details: (props) => <NodeDetails {...props} />
kubeObjectDetailRegistry.add(null, [
{
kind: "Node",
apiVersions: ["v1"],
components: {
Details: (props) => <NodeDetails {...props} />
}
}
})
])

View File

@ -209,10 +209,12 @@ export class PodSecurityPolicyDetails extends React.Component<Props> {
}
}
kubeObjectDetailRegistry.add({
kind: "PodSecurityPolicy",
apiVersions: ["policy/v1beta1"],
components: {
Details: (props) => <PodSecurityPolicyDetails {...props}/>
kubeObjectDetailRegistry.add(null, [
{
kind: "PodSecurityPolicy",
apiVersions: ["policy/v1beta1"],
components: {
Details: (props) => <PodSecurityPolicyDetails {...props}/>
}
}
})
])

View File

@ -62,10 +62,12 @@ export class StorageClassDetails extends React.Component<Props> {
}
}
kubeObjectDetailRegistry.add({
kind: "StorageClass",
apiVersions: ["storage.k8s.io/v1"],
components: {
Details: (props) => <StorageClassDetails {...props} />
kubeObjectDetailRegistry.add(null, [
{
kind: "StorageClass",
apiVersions: ["storage.k8s.io/v1"],
components: {
Details: (props) => <StorageClassDetails {...props} />
}
}
})
])

View File

@ -95,10 +95,12 @@ export class PersistentVolumeClaimDetails extends React.Component<Props> {
}
}
kubeObjectDetailRegistry.add({
kind: "PersistentVolumeClaim",
apiVersions: ["v1"],
components: {
Details: (props) => <PersistentVolumeClaimDetails {...props} />
kubeObjectDetailRegistry.add(null, [
{
kind: "PersistentVolumeClaim",
apiVersions: ["v1"],
components: {
Details: (props) => <PersistentVolumeClaimDetails {...props} />
}
}
})
])

View File

@ -103,10 +103,12 @@ export class PersistentVolumeDetails extends React.Component<Props> {
}
}
kubeObjectDetailRegistry.add({
kind: "PersistentVolume",
apiVersions: ["v1"],
components: {
Details: (props) => <PersistentVolumeDetails {...props} />
kubeObjectDetailRegistry.add(null, [
{
kind: "PersistentVolume",
apiVersions: ["v1"],
components: {
Details: (props) => <PersistentVolumeDetails {...props} />
}
}
})
])

View File

@ -125,17 +125,19 @@ export class RoleBindingDetails extends React.Component<Props> {
}
}
kubeObjectDetailRegistry.add({
kind: "RoleBinding",
apiVersions: ["rbac.authorization.k8s.io/v1"],
components: {
Details: (props) => <RoleBindingDetails {...props} />
kubeObjectDetailRegistry.add(null, [
{
kind: "RoleBinding",
apiVersions: ["rbac.authorization.k8s.io/v1"],
components: {
Details: (props) => <RoleBindingDetails {...props} />
}
},
{
kind: "ClusterRoleBinding",
apiVersions: ["rbac.authorization.k8s.io/v1"],
components: {
Details: (props) => <RoleBindingDetails {...props} />
}
}
})
kubeObjectDetailRegistry.add({
kind: "ClusterRoleBinding",
apiVersions: ["rbac.authorization.k8s.io/v1"],
components: {
Details: (props) => <RoleBindingDetails {...props} />
}
})
])

View File

@ -66,18 +66,19 @@ export class RoleDetails extends React.Component<Props> {
}
}
kubeObjectDetailRegistry.add({
kind: "Role",
apiVersions: ["rbac.authorization.k8s.io/v1"],
components: {
Details: (props) => <RoleDetails {...props}/>
kubeObjectDetailRegistry.add(null, [
{
kind: "Role",
apiVersions: ["rbac.authorization.k8s.io/v1"],
components: {
Details: (props) => <RoleDetails {...props}/>
}
},
{
kind: "ClusterRole",
apiVersions: ["rbac.authorization.k8s.io/v1"],
components: {
Details: (props) => <RoleDetails {...props}/>
}
}
})
kubeObjectDetailRegistry.add({
kind: "ClusterRole",
apiVersions: ["rbac.authorization.k8s.io/v1"],
components: {
Details: (props) => <RoleDetails {...props}/>
}
})
])

View File

@ -132,10 +132,12 @@ export class ServiceAccountsDetails extends React.Component<Props> {
}
}
kubeObjectDetailRegistry.add({
kind: "ServiceAccount",
apiVersions: ["v1"],
components: {
Details: (props) => <ServiceAccountsDetails {...props} />
kubeObjectDetailRegistry.add(null, [
{
kind: "ServiceAccount",
apiVersions: ["v1"],
components: {
Details: (props) => <ServiceAccountsDetails {...props} />
}
}
})
])

View File

@ -77,10 +77,12 @@ function ServiceAccountMenu(props: KubeObjectMenuProps<ServiceAccount>) {
)
}
kubeObjectMenuRegistry.add({
kind: "ServiceAccount",
apiVersions: ["v1"],
components: {
MenuItem: ServiceAccountMenu
kubeObjectMenuRegistry.add(null, [
{
kind: "ServiceAccount",
apiVersions: ["v1"],
components: {
MenuItem: ServiceAccountMenu
}
}
})
])

View File

@ -87,10 +87,12 @@ export class CronJobDetails extends React.Component<Props> {
}
}
kubeObjectDetailRegistry.add({
kind: "CronJob",
apiVersions: ["batch/v1"],
components: {
Details: (props) => <CronJobDetails {...props} />
kubeObjectDetailRegistry.add(null, [
{
kind: "CronJob",
apiVersions: ["batch/v1"],
components: {
Details: (props) => <CronJobDetails {...props} />
}
}
})
])

View File

@ -88,10 +88,12 @@ export function CronJobMenu(props: KubeObjectMenuProps<CronJob>) {
)
}
kubeObjectMenuRegistry.add({
kind: "CronJob",
apiVersions: ["batch/v1beta1"],
components: {
MenuItem: CronJobMenu
kubeObjectMenuRegistry.add(null, [
{
kind: "CronJob",
apiVersions: ["batch/v1beta1"],
components: {
MenuItem: CronJobMenu
}
}
})
])

View File

@ -97,10 +97,12 @@ export class DaemonSetDetails extends React.Component<Props> {
}
}
kubeObjectDetailRegistry.add({
kind: "DaemonSet",
apiVersions: ["apps/v1"],
components: {
Details: (props: any) => <DaemonSetDetails {...props} />
kubeObjectDetailRegistry.add(null, [
{
kind: "DaemonSet",
apiVersions: ["apps/v1"],
components: {
Details: (props: any) => <DaemonSetDetails {...props} />
}
}
})
])

View File

@ -6,7 +6,7 @@ import { disposeOnUnmount, observer } from "mobx-react";
import { t, Trans } from "@lingui/macro";
import { DrawerItem } from "../drawer";
import { Badge } from "../badge";
import { Deployment, deploymentApi } from "../../api/endpoints";
import { Deployment } from "../../api/endpoints";
import { cssNames } from "../../utils";
import { PodDetailsTolerations } from "../+workloads-pods/pod-details-tolerations";
import { PodDetailsAffinities } from "../+workloads-pods/pod-details-affinities";
@ -122,10 +122,12 @@ export class DeploymentDetails extends React.Component<Props> {
}
}
kubeObjectDetailRegistry.add({
kind: "Deployment",
apiVersions: ["apps/v1"],
components: {
Details: (props: any) => <DeploymentDetails {...props} />
kubeObjectDetailRegistry.add(null, [
{
kind: "Deployment",
apiVersions: ["apps/v1"],
components: {
Details: (props: any) => <DeploymentDetails {...props} />
}
}
})
])

View File

@ -129,10 +129,12 @@ export function DeploymentMenu(props: KubeObjectMenuProps<Deployment>) {
)
}
kubeObjectMenuRegistry.add({
kind: "Deployment",
apiVersions: ["apps/v1"],
components: {
MenuItem: DeploymentMenu
kubeObjectMenuRegistry.add(null, [
{
kind: "Deployment",
apiVersions: ["apps/v1"],
components: {
MenuItem: DeploymentMenu
}
}
})
])

View File

@ -107,10 +107,12 @@ export class JobDetails extends React.Component<Props> {
}
}
kubeObjectDetailRegistry.add({
kind: "Job",
apiVersions: ["batch/v1"],
components: {
Details: (props: any) => <JobDetails {...props}/>
kubeObjectDetailRegistry.add(null, [
{
kind: "Job",
apiVersions: ["batch/v1"],
components: {
Details: (props: any) => <JobDetails {...props}/>
}
}
})
])

View File

@ -221,10 +221,12 @@ export class PodDetails extends React.Component<Props> {
}
}
kubeObjectDetailRegistry.add({
kind: "Pod",
apiVersions: ["v1"],
components: {
Details: (props: any) => <PodDetails {...props} />
kubeObjectDetailRegistry.add(null, [
{
kind: "Pod",
apiVersions: ["v1"],
components: {
Details: (props: any) => <PodDetails {...props} />
}
}
})
])

View File

@ -97,10 +97,12 @@ export class ReplicaSetDetails extends React.Component<Props> {
}
}
kubeObjectDetailRegistry.add({
kind: "ReplicaSet",
apiVersions: ["apps/v1"],
components: {
Details: (props: any) => <ReplicaSetDetails {...props} />
kubeObjectDetailRegistry.add(null, [
{
kind: "ReplicaSet",
apiVersions: ["apps/v1"],
components: {
Details: (props: any) => <ReplicaSetDetails {...props} />
}
}
})
])

View File

@ -96,10 +96,12 @@ export class StatefulSetDetails extends React.Component<Props> {
}
kubeObjectDetailRegistry.add({
kind: "StatefulSet",
apiVersions: ["apps/v1"],
components: {
Details: (props: any) => <StatefulSetDetails {...props} />
kubeObjectDetailRegistry.add(null, [
{
kind: "StatefulSet",
apiVersions: ["apps/v1"],
components: {
Details: (props: any) => <StatefulSetDetails {...props} />
}
}
})
])

View File

@ -37,6 +37,7 @@ import logger from "../../main/logger";
import { clusterIpc } from "../../common/cluster-ipc";
import { webFrame } from "electron";
import { clusterPageRegistry } from "../../extensions/registries/page-registry";
import { clusterPageMenuRegistry } from "../../extensions/registries";
import { extensionLoader } from "../../extensions/extension-loader";
import { appEventBus } from "../../common/event-bus";
import whatInput from 'what-input';
@ -72,6 +73,34 @@ export class App extends React.Component {
return workloadsURL();
}
renderExtensionRoutes() {
return clusterPageRegistry.getItems().map(({ components: { Page }, exact, routePath, subPages }) => {
const Component = () => {
if (subPages) {
const tabs: TabLayoutRoute[] = subPages.map(({ exact, routePath, components: { Page } }) => {
const matchingUrl = clusterPageMenuRegistry.getByMatchingRoute(routePath, exact)
if (!matchingUrl) return;
return {
routePath, exact,
component: Page,
url: matchingUrl.url,
title: matchingUrl.title,
}
}).filter(Boolean);
if (tabs.length > 0) {
return (
<Page>
<TabLayout tabs={tabs}/>
</Page>
)
}
}
return <Page/>
};
return <Route key={routePath} path={routePath} exact={exact} component={Component}/>
})
}
render() {
return (
<I18nProvider i18n={_i18n}>
@ -90,15 +119,7 @@ export class App extends React.Component {
<Route component={CustomResources} {...crdRoute}/>
<Route component={UserManagement} {...usersManagementRoute}/>
<Route component={Apps} {...appsRoute}/>
{clusterPageRegistry.getItems().map(({ components: { Page }, subPages = [], exact, routePath }) => {
// return (
// <Route key={routePath} path={routePath} exact={exact} render={() => (
// <TabLayout tabs={subPages}>
// <Page/>
// </TabLayout>
// )}/>
// )
})}
{this.renderExtensionRoutes()}
<Redirect exact from="/" to={this.startURL}/>
<Route component={NotFound}/>
</Switch>

View File

@ -23,7 +23,7 @@ import { Tooltip } from "../tooltip";
import { ConfirmDialog } from "../confirm-dialog";
import { clusterIpc } from "../../../common/cluster-ipc";
import { clusterViewURL } from "./cluster-view.route";
import { globalPageMenuRegistry } from "../../../extensions/registries";
import { globalPageMenuRegistry, globalPageRegistry } from "../../../extensions/registries";
interface Props {
className?: IClassName;
@ -150,8 +150,10 @@ export class ClustersMenu extends React.Component<Props> {
</div>
<div className="extensions">
{globalPageMenuRegistry.getItems().map(({ title, url, components: { Icon } }) => {
const routePath = "" // todo: find matching route in page-registry + exact
const isActive = !!matchPath(navigation.location.pathname, { path: routePath/*, exact: false*/ });
const registeredPage = globalPageRegistry.getByMatchingUrl(url);
if (!registeredPage) return;
const { routePath, exact } = registeredPage;
const isActive = !!matchPath(navigation.location.pathname, { path: routePath, exact });
return (
<Icon
key={routePath}

View File

@ -4,17 +4,17 @@ import "./sidebar.scss";
import React from "react";
import { computed, observable, reaction } from "mobx";
import { observer } from "mobx-react";
import { matchPath, NavLink } from "react-router-dom";
import { NavLink } from "react-router-dom";
import { Trans } from "@lingui/macro";
import { createStorage, cssNames } from "../../utils";
import { Icon } from "../icon";
import { workloadsRoute, workloadsURL } from "../+workloads/workloads.route";
import { namespacesURL } from "../+namespaces/namespaces.route";
import { nodesURL } from "../+nodes/nodes.route";
import { namespacesRoute, namespacesURL } from "../+namespaces/namespaces.route";
import { nodesRoute, nodesURL } from "../+nodes/nodes.route";
import { usersManagementRoute, usersManagementURL } from "../+user-management/user-management.route";
import { networkRoute, networkURL } from "../+network/network.route";
import { storageRoute, storageURL } from "../+storage/storage.route";
import { clusterURL } from "../+cluster";
import { clusterRoute, clusterURL } from "../+cluster";
import { Config, configRoute, configURL } from "../+config";
import { eventRoute, eventsURL } from "../+events";
import { Apps, appsRoute, appsURL } from "../+apps";
@ -26,10 +26,10 @@ import { Network } from "../+network";
import { crdStore } from "../+custom-resources/crd.store";
import { CrdList, crdResourcesRoute, crdRoute, crdURL } from "../+custom-resources";
import { CustomResources } from "../+custom-resources/custom-resources";
import { navigation } from "../../navigation";
import { isActiveRoute } from "../../navigation";
import { isAllowedResource } from "../../../common/rbac"
import { Spinner } from "../spinner";
import { clusterPageMenuRegistry } from "../../../extensions/registries";
import { clusterPageMenuRegistry, clusterPageRegistry } from "../../../extensions/registries";
const SidebarContext = React.createContext<SidebarContextValue>({ pinned: false });
type SidebarContextValue = {
@ -52,7 +52,7 @@ export class Sidebar extends React.Component<Props> {
renderCustomResources() {
if (crdStore.isLoading) {
return <Spinner centerHorizontal />
return <Spinner centerHorizontal/>
}
return Object.entries(crdStore.groups).map(([group, crds]) => {
@ -84,7 +84,7 @@ export class Sidebar extends React.Component<Props> {
<div className={cssNames("Sidebar flex column", className, { pinned: isPinned })}>
<div className="header flex align-center">
<NavLink exact to="/" className="box grow">
<Icon svg="logo-lens" className="logo-icon" />
<Icon svg="logo-lens" className="logo-icon"/>
<div className="logo-text">Lens</div>
</NavLink>
<Icon
@ -97,93 +97,100 @@ export class Sidebar extends React.Component<Props> {
</div>
<div className="sidebar-nav flex column box grow-fixed">
<SidebarNavItem
isActive={isActiveRoute(clusterRoute)}
isHidden={!isAllowedResource("nodes")}
url={clusterURL()}
text={<Trans>Cluster</Trans>}
icon={<Icon svg="kube" />}
icon={<Icon svg="kube"/>}
/>
<SidebarNavItem
isActive={isActiveRoute(nodesRoute)}
isHidden={!isAllowedResource("nodes")}
url={nodesURL()}
text={<Trans>Nodes</Trans>}
icon={<Icon svg="nodes" />}
icon={<Icon svg="nodes"/>}
/>
<SidebarNavItem
isActive={isActiveRoute(workloadsRoute)}
isHidden={Workloads.tabRoutes.length == 0}
url={workloadsURL({ query })}
routePath={workloadsRoute.path}
subMenus={Workloads.tabRoutes}
text={<Trans>Workloads</Trans>}
icon={<Icon svg="workloads" />}
icon={<Icon svg="workloads"/>}
/>
<SidebarNavItem
isActive={isActiveRoute(configRoute)}
isHidden={Config.tabRoutes.length == 0}
url={configURL({ query })}
routePath={configRoute.path}
subMenus={Config.tabRoutes}
text={<Trans>Configuration</Trans>}
icon={<Icon material="list" />}
icon={<Icon material="list"/>}
/>
<SidebarNavItem
isActive={isActiveRoute(networkRoute)}
isHidden={Network.tabRoutes.length == 0}
url={networkURL({ query })}
routePath={networkRoute.path}
subMenus={Network.tabRoutes}
text={<Trans>Network</Trans>}
icon={<Icon material="device_hub" />}
icon={<Icon material="device_hub"/>}
/>
<SidebarNavItem
isActive={isActiveRoute(storageRoute)}
isHidden={Storage.tabRoutes.length == 0}
url={storageURL({ query })}
routePath={storageRoute.path}
subMenus={Storage.tabRoutes}
icon={<Icon svg="storage" />}
icon={<Icon svg="storage"/>}
text={<Trans>Storage</Trans>}
/>
<SidebarNavItem
isActive={isActiveRoute(namespacesRoute)}
isHidden={!isAllowedResource("namespaces")}
url={namespacesURL()}
icon={<Icon material="layers" />}
icon={<Icon material="layers"/>}
text={<Trans>Namespaces</Trans>}
/>
<SidebarNavItem
isActive={isActiveRoute(eventRoute)}
isHidden={!isAllowedResource("events")}
url={eventsURL({ query })}
routePath={eventRoute.path}
icon={<Icon material="access_time" />}
icon={<Icon material="access_time"/>}
text={<Trans>Events</Trans>}
/>
<SidebarNavItem
isActive={isActiveRoute(appsRoute)}
url={appsURL({ query })}
subMenus={Apps.tabRoutes}
routePath={appsRoute.path}
icon={<Icon material="apps" />}
icon={<Icon material="apps"/>}
text={<Trans>Apps</Trans>}
/>
<SidebarNavItem
isActive={isActiveRoute(usersManagementRoute)}
url={usersManagementURL({ query })}
routePath={usersManagementRoute.path}
subMenus={UserManagement.tabRoutes}
icon={<Icon material="security" />}
icon={<Icon material="security"/>}
text={<Trans>Access Control</Trans>}
/>
<SidebarNavItem
isActive={isActiveRoute(crdRoute)}
isHidden={!isAllowedResource("customresourcedefinitions")}
url={crdURL()}
subMenus={CustomResources.tabRoutes}
routePath={crdRoute.path}
icon={<Icon material="extension" />}
icon={<Icon material="extension"/>}
text={<Trans>Custom Resources</Trans>}
>
{this.renderCustomResources()}
</SidebarNavItem>
{clusterPageMenuRegistry.getItems().map(({ title, url, components: { Icon } }) => {
const routePath = "" // todo: find in page-registry
const registeredPage = clusterPageRegistry.getByMatchingUrl(url);
if (!registeredPage) return;
const { routePath, exact } = registeredPage;
return (
<SidebarNavItem
key={url} url={url}
routePath={routePath}
text={title} icon={<Icon />}
key={url}
url={url}
text={title}
icon={<Icon/>}
isActive={isActiveRoute({ path: routePath, exact })}
/>
)
})}
@ -200,7 +207,7 @@ interface SidebarNavItemProps {
className?: string;
icon?: React.ReactNode;
isHidden?: boolean;
routePath?: string | string[];
isActive?: boolean;
subMenus?: TabLayoutRoute[];
}
@ -225,27 +232,19 @@ class SidebarNavItem extends React.Component<SidebarNavItemProps> {
navItemState.set(this.itemId, !this.isExpanded);
};
isActive = () => {
const { url, routePath = url } = this.props;
return !!matchPath(navigation.location.pathname, {
path: routePath
});
};
render() {
const { isHidden, subMenus = [], icon, text, url, children, className } = this.props;
const { isHidden, isActive, subMenus = [], icon, text, url, children, className } = this.props;
if (isHidden) {
return null;
}
const extendedView = (subMenus.length > 0 || children) && this.context.pinned;
if (extendedView) {
const isActive = this.isActive();
return (
<div className={cssNames("SidebarNavItem", className)}>
<div className={cssNames("nav-item", { active: isActive })} onClick={this.toggleSubMenu}>
{icon}
<span className="link-text">{text}</span>
<Icon className="expand-icon" material={this.isExpanded ? "keyboard_arrow_up" : "keyboard_arrow_down"} />
<Icon className="expand-icon" material={this.isExpanded ? "keyboard_arrow_up" : "keyboard_arrow_down"}/>
</div>
<ul className={cssNames("sub-menu", { active: isActive })}>
{subMenus.map(({ title, url }) => (
@ -263,7 +262,7 @@ class SidebarNavItem extends React.Component<SidebarNavItemProps> {
);
}
return (
<NavLink className={cssNames("SidebarNavItem", className)} to={url} isActive={this.isActive}>
<NavLink className={cssNames("SidebarNavItem", className)} to={url} isActive={() => isActive}>
{icon}
<span className="link-text">{text}</span>
</NavLink>

View File

@ -1,7 +1,7 @@
// Navigation helpers
import { ipcRenderer } from "electron";
import { matchPath } from "react-router";
import { matchPath, RouteProps } from "react-router";
import { reaction } from "mobx";
import { createObservableHistory } from "mobx-observable-history";
import { createBrowserHistory, createMemoryHistory, LocationDescriptor } from "history";
@ -19,6 +19,10 @@ export function navigate(location: LocationDescriptor) {
}
}
export function isActiveRoute(route: string | string[] | RouteProps): boolean {
return !!matchPath(navigation.location.pathname, route);
}
// common params for all pages
export interface IQueryParams {
namespaces?: string[]; // selected context namespaces