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

Rework ClusterCatalogRegistry to be a source of truth

Signed-off-by: Sebastian Malton <sebastian@malton.name>

move to not storing the Categories anywhere else besides the registry

Signed-off-by: Sebastian Malton <sebastian@malton.name>

use KubernetesCluster

Signed-off-by: Sebastian Malton <sebastian@malton.name>

more work towards ClusterManager being the sole owner of ClusterInstances

Signed-off-by: Sebastian Malton <sebastian@malton.name>

making ClusterManger the source of KubernetesClusters status

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2021-05-04 11:12:23 -04:00
parent 33422ce975
commit 111356521a
48 changed files with 1027 additions and 685 deletions

View File

@ -18,11 +18,12 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { Catalog, Interface, LensRendererExtension } from "@k8slens/extensions";
import React from "react";
import { LensRendererExtension, Catalog } from "@k8slens/extensions";
import { MetricsSettings } from "./src/metrics-settings";
export default class ClusterMetricsFeatureExtension extends LensRendererExtension {
entitySettings = [
{
@ -39,4 +40,67 @@ export default class ClusterMetricsFeatureExtension extends LensRendererExtensio
}
}
];
onActivate() {
this.disposers.push(
Catalog.CatalogCategoryRegistry.registerHandler(
"entity.k8slens.dev/v1alpha1",
"KubernetesCluster",
"onContextMenuOpen",
this.clusterContextMenuOpen,
),
);
}
clusterContextMenuOpen = (cluster: Catalog.KubernetesCluster): Interface.ContextMenu[] => {
if (!cluster.status.active) {
return [];
}
const metricsFeature = new MetricsFeature();
return [
{
icon: "refresh",
title: "Upgrade Lens Metrics stack",
onClick: async () => {
metricsFeature.upgrade(cluster);
}
},
];
// const metricsFeature = new MetricsFeature();
// await metricsFeature.updateStatus(cluster);
// if (metricsFeature.status.installed) {
// if (metricsFeature.status.canUpgrade) {
// ctx.menuItems.unshift({
// icon: "refresh",
// title: "Upgrade Lens Metrics stack",
// onClick: async () => {
// metricsFeature.upgrade(cluster);
// }
// });
// }
// ctx.menuItems.unshift({
// icon: "toggle_off",
// title: "Uninstall Lens Metrics stack",
// onClick: async () => {
// await metricsFeature.uninstall(cluster);
// Component.Notifications.info(`Lens Metrics has been removed from ${cluster.metadata.name}`, { timeout: 10_000 });
// }
// });
// } else {
// ctx.menuItems.unshift({
// icon: "toggle_on",
// title: "Install Lens Metrics stack",
// onClick: async () => {
// metricsFeature.install(cluster);
// Component.Notifications.info(`Lens Metrics is now installed to ${cluster.metadata.name}`, { timeout: 10_000 });
// }
// });
// }
};
}

View File

@ -18,16 +18,17 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import path from "path";
import Config from "conf";
import type { Options as ConfOptions } from "conf/dist/source/types";
import { app, ipcMain, ipcRenderer, remote } from "electron";
import isEqual from "lodash/isEqual";
import { IReactionOptions, observable, reaction, runInAction, when } from "mobx";
import { Singleton, getAppVersion } from "./utils";
import path from "path";
import logger from "../main/logger";
import { broadcastMessage, ipcMainOn, ipcRendererOn } from "./ipc";
import isEqual from "lodash/isEqual";
import { getAppVersion } from "./utils/app-version";
import Singleton from "./utils/singleton";
export interface BaseStoreParams<T = any> extends ConfOptions<T> {
autoLoad?: boolean;

View File

@ -18,16 +18,15 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
import { CatalogEntity, CatalogEntityActionContext, CatalogEntityAddMenuContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus, CatalogCategory, CatalogCategorySpec } from "../catalog";
import { clusterActivateHandler, clusterDisconnectHandler } from "../cluster-ipc";
import { app } from "electron";
import { CatalogEntity, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog";
import type { ActionContext, ContextMenu, MenuContext } from "../catalog/catalog-entity";
import * as clusterIpc from "../cluster-ipc";
import { ClusterStore } from "../cluster-store";
import { requestMain } from "../ipc";
import { productName } from "../vars";
import { addClusterURL } from "../routes";
import { storedKubeConfigFolder } from "../utils";
import { app } from "electron";
import { productName } from "../vars";
export type KubernetesClusterPrometheusMetrics = {
address?: {
@ -49,7 +48,7 @@ export type KubernetesClusterSpec = {
};
export interface KubernetesClusterStatus extends CatalogEntityStatus {
phase: "connected" | "disconnected";
phase?: "connected" | "disconnected";
}
export class KubernetesCluster extends CatalogEntity<CatalogEntityMetadata, KubernetesClusterStatus, KubernetesClusterSpec> {
@ -67,7 +66,7 @@ export class KubernetesCluster extends CatalogEntity<CatalogEntityMetadata, Kube
return;
}
await requestMain(clusterActivateHandler, this.metadata.uid, false);
await requestMain(clusterIpc.activate, this.metadata.uid, false);
return;
}
@ -83,34 +82,39 @@ export class KubernetesCluster extends CatalogEntity<CatalogEntityMetadata, Kube
return;
}
await requestMain(clusterDisconnectHandler, this.metadata.uid, false);
await requestMain(clusterIpc.disconnect, this.metadata.uid, false);
return;
}
async onRun(context: CatalogEntityActionContext) {
onRun = (context: ActionContext) => {
context.navigate(`/cluster/${this.metadata.uid}`);
}
};
onDetailsOpen(): void {
//
}
onContextMenuOpen = (context: MenuContext) => {
const res: ContextMenu[] = [];
onSettingsOpen(): void {
//
}
if (this.status.phase == "connected") {
res.push({
icon: "link_off",
title: "Disconnect",
onClick: async () => {
ClusterStore.getInstance().deactivate(this.metadata.uid);
requestMain(clusterIpc.disconnect, this.metadata.uid);
}
});
}
async onContextMenuOpen(context: CatalogEntityContextMenuContext) {
context.menuItems = [
{
title: "Settings",
onlyVisibleForSource: "local",
onClick: async () => context.navigate(`/entity/${this.metadata.uid}/settings`)
},
];
res.push({
icon: "settings",
title: "Settings",
onlyVisibleForSource: "local",
onClick: async () => context.navigate(`/entity/${this.metadata.uid}/settings`)
});
if (this.metadata.labels["file"]?.startsWith(storedKubeConfigFolder())) {
context.menuItems.push({
res.push({
icon: "delete",
title: "Delete",
onlyVisibleForSource: "local",
onClick: async () => ClusterStore.getInstance().removeById(this.metadata.uid),
@ -120,62 +124,6 @@ export class KubernetesCluster extends CatalogEntity<CatalogEntityMetadata, Kube
});
}
if (this.status.phase == "connected") {
context.menuItems.push({
title: "Disconnect",
onClick: async () => {
ClusterStore.getInstance().deactivate(this.metadata.uid);
requestMain(clusterDisconnectHandler, this.metadata.uid);
}
});
} else {
context.menuItems.push({
title: "Connect",
onClick: async () => {
context.navigate(`/cluster/${this.metadata.uid}`);
}
});
}
const category = catalogCategoryRegistry.getCategoryForEntity<KubernetesClusterCategory>(this);
if (category) category.emit("contextMenuOpen", this, context);
}
}
export class KubernetesClusterCategory extends CatalogCategory {
public readonly apiVersion = "catalog.k8slens.dev/v1alpha1";
public readonly kind = "CatalogCategory";
public metadata = {
name: "Kubernetes Clusters",
icon: require(`!!raw-loader!./icons/kubernetes.svg`).default // eslint-disable-line
return res;
};
public spec: CatalogCategorySpec = {
group: "entity.k8slens.dev",
versions: [
{
name: "v1alpha1",
entityClass: KubernetesCluster
}
],
names: {
kind: "KubernetesCluster"
}
};
constructor() {
super();
this.on("onCatalogAddMenu", (ctx: CatalogEntityAddMenuContext) => {
ctx.menuItems.push({
icon: "text_snippet",
title: "Add from kubeconfig",
onClick: () => {
ctx.navigate(addClusterURL());
}
});
});
}
}
catalogCategoryRegistry.add(new KubernetesClusterCategory());

View File

@ -18,9 +18,7 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { CatalogCategory, CatalogEntity, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog";
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
import { CatalogEntity, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog";
export interface WebLinkStatus extends CatalogEntityStatus {
phase: "valid" | "invalid";
@ -34,42 +32,7 @@ export class WebLink extends CatalogEntity<CatalogEntityMetadata, WebLinkStatus,
public readonly apiVersion = "entity.k8slens.dev/v1alpha1";
public readonly kind = "WebLink";
async onRun() {
onRun = () => {
window.open(this.spec.url, "_blank");
}
public onSettingsOpen(): void {
return;
}
public onDetailsOpen(): void {
return;
}
public onContextMenuOpen(): void {
return;
}
}
export class WebLinkCategory extends CatalogCategory {
public readonly apiVersion = "catalog.k8slens.dev/v1alpha1";
public readonly kind = "CatalogCategory";
public metadata = {
name: "Web Links",
icon: "link"
};
public spec = {
group: "entity.k8slens.dev",
versions: [
{
name: "v1alpha1",
entityClass: WebLink
}
],
names: {
kind: "WebLink"
}
};
}
catalogCategoryRegistry.add(new WebLinkCategory());

View File

@ -18,60 +18,234 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { action, computed, observable, when } from "mobx";
import { action, computed, observable, toJS } from "mobx";
import { CatalogCategory, CatalogEntityData, CatalogEntityKindData } from "./catalog-entity";
import { navigate } from "../../renderer/navigation";
import { Rest } from "../ipc";
import { Disposer, disposer, ExtendedMap, Singleton, StrictMap } from "../utils";
import {
AddMenuOpenHandler,
CatalogCategorySpec,
CatalogCategoryVersion,
CatalogEntity,
CatalogEntityConstructor,
CatalogEntityData,
CatalogEntityKindData,
CategoryHandler,
ContextMenuOpenHandler,
MatchingCatalogEntityData,
parseApiVersion,
SettingsMenuOpenHandler,
} from "./catalog-entity";
export class CatalogCategoryRegistry {
@observable protected categories: CatalogCategory[] = [];
@action add(category: CatalogCategory) {
this.categories.push(category);
}
type KeysMatching<T, V> = { [K in keyof T]-?: T[K] extends V ? K : never }[keyof T];
type KeysNotMatching<T, V> = { [K in keyof T]-?: T[K] extends V ? never : K }[keyof T];
@action remove(category: CatalogCategory) {
this.categories = this.categories.filter((cat) => cat.apiVersion !== category.apiVersion && cat.kind !== category.kind);
}
export type CategoryHandlers = {
[HandlerName in KeysMatching<CatalogCategory, Set<any>>]?: CatalogCategory[HandlerName] extends Set<infer Handler> ? Handler : never;
};
export type CategoryHandlerNames = keyof CategoryHandlers;
export type CatalogHandler<Name extends CategoryHandlerNames> = CategoryHandlers[Name];
@computed get items() {
return toJS(this.categories);
}
export type EntityContextHandlers = keyof EntityContextGetters;
export type GlobalContextHandlers = keyof GlobalContextGetters;
getForGroupKind<T extends CatalogCategory>(group: string, kind: string) {
return this.categories.find((c) => c.spec.group === group && c.spec.names.kind === kind) as T;
}
type EntityContextGetters = {
[HandlerName in KeysMatching<CategoryHandlers, CategoryHandler<(...args: any) => any>>]: () => Rest<Parameters<CategoryHandlers[HandlerName]>>;
};
getEntityForData(data: CatalogEntityData & CatalogEntityKindData) {
const category = this.getCategoryForEntity(data);
type GlobalContextGetters = {
[HandlerName in KeysNotMatching<CategoryHandlers, CategoryHandler<(...args: any) => any>>]: () => Parameters<CategoryHandlers[HandlerName]>;
};
if (!category) {
return null;
const EntityContexts: EntityContextGetters = {
onContextMenuOpen: () => [{ navigate }],
onSettingsOpen: () => [{ navigate }],
};
const GlobalContexts: GlobalContextGetters = {
onAddMenuOpen: () => [{ navigate }],
};
/**
* Note: this type shouldn't be exported or leaked out of this file.
* The registry should do everything for any consumer of this type.
*/
class CatalogCategory implements CatalogCategorySpec {
onContextMenuOpen = new Set<CategoryHandler<ContextMenuOpenHandler>>();
onSettingsOpen = new Set<CategoryHandler<SettingsMenuOpenHandler>>();
onAddMenuOpen = new Set<AddMenuOpenHandler>();
public readonly id: string;
public readonly apiVersion: string;
public readonly kind: string;
public readonly metadata: {
name: string;
icon: string;
};
public readonly spec: {
group: string;
versions: CatalogCategoryVersion<CatalogEntity>[];
names: {
kind: string;
};
};
constructor(specAndHandlers: CatalogCategorySpec & CategoryHandlers) {
const { apiVersion, kind, metadata, spec, ...handlers } = specAndHandlers;
this.spec = spec;
this.apiVersion = apiVersion;
this.kind = kind;
this.metadata = metadata;
this.id = `${spec.group}/${spec.names.kind}`;
for (const name of Object.keys(handlers)) {
const handlerName = name as CategoryHandlerNames;
if (typeof handlers[handlerName] === "function") {
this[handlerName].add(handlers[handlerName] as any);
}
}
const splitApiVersion = data.apiVersion.split("/");
const version = splitApiVersion[1];
const specVersion = category.spec.versions.find((v) => v.name === version);
if (!specVersion) {
return null;
}
return new specVersion.entityClass(data);
}
getCategoryForEntity<T extends CatalogCategory>(data: CatalogEntityData & CatalogEntityKindData) {
const splitApiVersion = data.apiVersion.split("/");
const group = splitApiVersion[0];
const category = this.categories.find((category) => {
return category.spec.group === group && category.spec.names.kind === data.kind;
});
if (!category) return null;
return category as T;
}
}
export const catalogCategoryRegistry = new CatalogCategoryRegistry();
export class CatalogCategoryRegistry extends Singleton {
/**
* The three levels of keys are: (for category ApiVersions)
* 1. `GROUP`
* 2. `VERSION`
*/
protected categories = observable.set<CatalogCategory>();
/**
* The three levels of keys are: (by entity ApiVersions)
* 1. `GROUP`
* 2. `VERSION`
* 3. `KIND`
*/
@computed protected get entityToCategoryTable(): Map<string, Map<string, Map<string, [CatalogCategory, CatalogEntityConstructor<CatalogEntity>]>>> {
const res = ExtendedMap.newExtendedStrict<string, string, string, [CatalogCategory, CatalogEntityConstructor<CatalogEntity>]>();
for (const category of this.categories.values()) {
const grouping = res.getOrDefault(category.spec.group);
for (const { version, entityClass } of category.spec.versions) {
grouping
.getOrDefault(version)
.strictSet(category.spec.names.kind, [category, entityClass]);
}
}
return res;
}
@computed protected get categoryIdLookup(): Map<string, CatalogCategory> {
const res = new StrictMap<string, CatalogCategory>();
for (const category of this.categories.values()) {
res.strictSet(category.id, category);
}
return res;
}
/**
* Registers (and potentially overrides a previous category)
* @param specAndHandlers The Category spec and initial handlers to register
* @returns the ability to remove this category
*/
@action add(specAndHandlers: CatalogCategorySpec & CategoryHandlers): Disposer {
parseApiVersion(specAndHandlers.apiVersion); // make sure this is valid
const category = new CatalogCategory(specAndHandlers);
this.categories.add(category);
return () => void this.categories.delete(category);
}
@computed get items(): CatalogCategorySpec[] {
return Array.from(this.categoryIdLookup.values());
}
getById(id: string): CatalogCategorySpec | undefined {
return this.categoryIdLookup.get(id);
}
/**
* Gets the `CatalogCategory` once it has been registered
* @param apiVersion the ApiVersion string of the category
* @param kind the kind of entity that is desired
*/
registerHandler(apiVersion: string, kind: string, handlerName: CategoryHandlerNames, handler: CatalogHandler<typeof handlerName>): Disposer {
const { group, version } = parseApiVersion(apiVersion, false);
if (version) {
// only one version to do
return disposer(
when(
() => this.entityToCategoryTable.get(group)?.get(version)?.has(kind),
() => {
const [category] = this.entityToCategoryTable.get(group).get(version).get(kind);
category[handlerName].add(handler as any);
},
),
() => void this.entityToCategoryTable.get(group)?.get(version)?.delete(kind),
);
}
throw new Error("Not providing a version for groups is not supported at this time");
// This would requiring observing future additions to the second level of the map
// and waiting for them to add the kind
// all wrapped up in disposers
}
runEntityHandlersFor(entity: CatalogEntity, handlerName: "onContextMenuOpen"): ReturnType<CategoryHandlers[typeof handlerName]>;
runEntityHandlersFor(entity: CatalogEntity, handlerName: "onSettingsOpen"): ReturnType<CategoryHandlers[typeof handlerName]>;
runEntityHandlersFor(entity: CatalogEntity, handlerName: EntityContextHandlers): ReturnType<CategoryHandlers[typeof handlerName]> {
const category = this.getCategoryForEntity(entity) as CatalogCategory; // safe and what it actually is
const res = (entity[handlerName] as any)?.(...EntityContexts[handlerName]()) ?? [];
console.log(category, res);
for (const handler of category[handlerName].values()) {
res.push((handler as any)(entity, ...EntityContexts[handlerName]()));
}
return res.flat();
}
runGlobalHandlersFor({ spec }: CatalogCategorySpec, handlerName: "onAddMenuOpen"): ReturnType<CategoryHandlers[typeof handlerName]>;
runGlobalHandlersFor({ spec }: CatalogCategorySpec, handlerName: GlobalContextHandlers): ReturnType<CategoryHandlers[typeof handlerName]> {
const category = this.categoryIdLookup.get(`${spec.group}/${spec.names.kind}`);
const res = [];
for (const handler of category[handlerName].values()) {
res.push((handler as any)(...GlobalContexts[handlerName]()));
}
return res.flat();
}
getEntityForData<Entity extends CatalogEntity>(data: MatchingCatalogEntityData<Entity> & CatalogEntityKindData): Entity {
const { group, version } = parseApiVersion(data.apiVersion);
const [, entityClass] = this.entityToCategoryTable.get(group)?.get(version)?.get(data.kind);
const res = new entityClass(data);
if (res.apiVersion !== data.apiVersion || res.kind !== data.kind) {
throw new TypeError(`CatalogEntity class declared for ${group}/${version}:${data.kind} produced ${res.apiVersion}:${res.kind}`);
}
return res as Entity;
}
getCategoryForEntity(data: CatalogEntityData & CatalogEntityKindData): CatalogCategorySpec | undefined {
const { group, version } = parseApiVersion(data.apiVersion);
return this.entityToCategoryTable.get(group)?.get(version)?.get(data.kind)?.[0];
}
}

View File

@ -20,10 +20,10 @@
*/
import { action, computed, observable, IComputedValue, IObservableArray } from "mobx";
import type { CatalogEntity } from "./catalog-entity";
import { iter } from "../utils";
import type { CatalogEntity, CatalogEntityData, CatalogEntityKindData } from "./catalog-entity";
import { cloneJsonObject, iter, Singleton } from "../utils";
export class CatalogEntityRegistry {
export class CatalogEntityRegistry extends Singleton {
protected sources = observable.map<string, IComputedValue<CatalogEntity[]>>([], { deep: true });
@action addObservableSource(id: string, source: IObservableArray<CatalogEntity>) {
@ -38,8 +38,25 @@ export class CatalogEntityRegistry {
this.sources.delete(id);
}
@computed get items(): CatalogEntity[] {
return Array.from(iter.flatMap(this.sources.values(), source => source.get()));
@computed get items(): (CatalogEntityData & CatalogEntityKindData)[] {
// This is done to filter out non-serializable items, namely functions
return Array.from(
iter.flatMap(
this.sources.values(),
source => (
iter.map(
source.get(),
({ apiVersion, kind, metadata, spec, status }) => ({
apiVersion,
kind,
metadata: cloneJsonObject(metadata),
spec: cloneJsonObject(spec),
status: cloneJsonObject(status),
}),
)
),
),
);
}
getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] {
@ -48,5 +65,3 @@ export class CatalogEntityRegistry {
return items as T[];
}
}
export const catalogEntityRegistry = new CatalogEntityRegistry();

View File

@ -18,47 +18,99 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { EventEmitter } from "events";
import { observable } from "mobx";
import URLParse from "url-parse";
export interface ParsedApiVersion {
group: string;
version?: string;
}
const versionSchema = /^\/(?<version>v[1-9][0-9]*((alpha|beta)[1-9][0-9]*)?)$/;
/**
* Attempts to parse an ApiVersion string or a group string
* @param apiVersionOrGroup A string that should be either of the form `<group>/<version>` or `<group>` for any version
* @param strict if true then will throw an error if `<version>` is not provided
* @default strict = true
* @returns A parsed data
*/
export function parseApiVersion(apiVersionOrGroup: string, strict: false): ParsedApiVersion;
export function parseApiVersion(apiVersionOrGroup: string, strict?: true): Required<ParsedApiVersion>;
export function parseApiVersion(apiVersionOrGroup: string, strict?: boolean): ParsedApiVersion {
strict ??= true;
const parsed = new URLParse(`lens://${apiVersionOrGroup}`);
if (
parsed.protocol !== "lens:"
|| parsed.hash
|| parsed.query
|| parsed.auth
|| parsed.port
|| parsed.password
|| parsed.username
) {
throw new TypeError(`invalid apiVersion string: ${apiVersionOrGroup}`);
}
if (!parsed.pathname) {
throw new TypeError(`missing version on apiVersion: ${apiVersionOrGroup}`);
}
const match = parsed.pathname.match(versionSchema);
if (versionSchema && !match && strict) {
throw new TypeError(`invalid version on apiVersion: ${apiVersionOrGroup}`);
}
return {
group: parsed.hostname,
version: match?.groups.version,
};
}
type ExtractEntityMetadataType<Entity> = Entity extends CatalogEntity<infer Metadata> ? Metadata : never;
type ExtractEntityStatusType<Entity> = Entity extends CatalogEntity<any, infer Status> ? Status : never;
type ExtractEntitySpecType<Entity> = Entity extends CatalogEntity<any, any, infer Spec> ? Spec : never;
export type CatalogEntityConstructor<Entity extends CatalogEntity> = (
(new (data: CatalogEntityData<
ExtractEntityMetadataType<Entity>,
ExtractEntityStatusType<Entity>,
ExtractEntitySpecType<Entity>
>) => Entity)
);
export type MatchingCatalogEntityData<Entity extends CatalogEntity> = CatalogEntityData<
ExtractEntityMetadataType<Entity>,
ExtractEntityStatusType<Entity>,
ExtractEntitySpecType<Entity>
>;
export type CatalogEntityConstructor<Entity extends CatalogEntity> = new (data: MatchingCatalogEntityData<Entity>) => Entity;
export interface CatalogCategoryVersion<Entity extends CatalogEntity> {
name: string;
version: string;
entityClass: CatalogEntityConstructor<Entity>;
}
export interface CatalogCategorySpec {
group: string;
versions: CatalogCategoryVersion<CatalogEntity>[];
names: {
kind: string;
};
}
export abstract class CatalogCategory extends EventEmitter {
abstract readonly apiVersion: string;
abstract readonly kind: string;
abstract metadata: {
readonly apiVersion: string;
readonly kind: string;
readonly metadata: {
name: string;
icon: string;
};
abstract spec: CatalogCategorySpec;
public getId(): string {
return `${this.spec.group}/${this.spec.names.kind}`;
}
/**
* It will be a runtime error if any of the instances created through the
* versions don't match the provided `group` and `names.kind` provided here.
*/
readonly spec: {
group: string;
versions: CatalogCategoryVersion<CatalogEntity>[];
names: {
kind: string;
};
};
}
export function getCatalogCategoryId(category: CatalogCategorySpec): string {
return `${category.spec.group}/${category.spec.names.kind}`;
}
export interface CatalogEntityMetadata {
@ -71,18 +123,21 @@ export interface CatalogEntityMetadata {
}
export interface CatalogEntityStatus {
phase: string;
phase?: string;
reason?: string;
message?: string;
active?: boolean;
}
export interface CatalogEntityActionContext {
export interface ActionContext {
navigate: (url: string) => void;
setCommandPaletteContext: (context?: CatalogEntity) => void;
}
export interface CatalogEntityContextMenu {
export type ActionHandler = (ctx: ActionContext) => void;
export interface ContextMenu {
icon: string;
title: string;
onlyVisibleForSource?: string; // show only if empty or if matches with entity source
onClick: () => void | Promise<void>;
@ -91,11 +146,19 @@ export interface CatalogEntityContextMenu {
}
}
export interface CatalogEntityAddMenu extends CatalogEntityContextMenu {
icon: string;
export interface MenuContext {
navigate: (url: string) => void;
}
export interface CatalogEntitySettingsMenu {
export type ContextMenuOpenHandler = (ctx: MenuContext) => ContextMenu[];
export type AddMenuOpenHandler = (ctx: MenuContext) => ContextMenu[];
export type CategoryHandler<EntityHandler extends (...args: any[]) => any> = (entity: CatalogEntity, ...args: Parameters<EntityHandler>) => ReturnType<EntityHandler>;
export interface SettingsContext {
}
export interface SettingsMenu {
group?: string;
title: string;
components: {
@ -103,19 +166,7 @@ export interface CatalogEntitySettingsMenu {
};
}
export interface CatalogEntityContextMenuContext {
navigate: (url: string) => void;
menuItems: CatalogEntityContextMenu[];
}
export interface CatalogEntitySettingsContext {
menuItems: CatalogEntityContextMenu[];
}
export interface CatalogEntityAddMenuContext {
navigate: (url: string) => void;
menuItems: CatalogEntityAddMenu[];
}
export type SettingsMenuOpenHandler = (ctx: SettingsContext) => SettingsMenu[];
export type CatalogEntitySpec = Record<string, any>;
@ -160,8 +211,7 @@ export abstract class CatalogEntity<
return this.metadata.name;
}
public abstract onRun?(context: CatalogEntityActionContext): void | Promise<void>;
public abstract onDetailsOpen(context: CatalogEntityActionContext): void | Promise<void>;
public abstract onContextMenuOpen(context: CatalogEntityContextMenuContext): void | Promise<void>;
public abstract onSettingsOpen(context: CatalogEntitySettingsContext): void | Promise<void>;
public onRun?: ActionHandler;
public onContextMenuOpen?: ContextMenuOpenHandler;
public onSettingsOpen?: SettingsMenuOpenHandler;
}

View File

@ -1,29 +0,0 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { observable } from "mobx";
export type ClusterFrameInfo = {
frameId: number;
processId: number
};
export const clusterFrameMap = observable.map<string, ClusterFrameInfo>();

View File

@ -19,9 +19,8 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
export const clusterActivateHandler = "cluster:activate";
export const clusterSetFrameIdHandler = "cluster:set-frame-id";
export const clusterRefreshHandler = "cluster:refresh";
export const clusterDisconnectHandler = "cluster:disconnect";
export const clusterKubectlApplyAllHandler = "cluster:kubectl-apply-all";
export const clusterKubectlDeleteAllHandler = "cluster:kubectl-delete-all";
export const activate = "cluster:activate";
export const setFrameId = "cluster:set-frame-id";
export const refresh = "cluster:refresh";
export const disconnect = "cluster:disconnect";
export const kubectlApplyAll = "cluster:kubectl-apply-all";

View File

@ -38,7 +38,6 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
private static StateChannel = "cluster:state";
@observable activeCluster: ClusterId;
@observable removedClusters = observable.map<ClusterId, Cluster>();
@observable clusters = observable.map<ClusterId, Cluster>();
private static stateRequestChannel = "cluster:states";
@ -229,7 +228,6 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
protected fromStore({ activeCluster, clusters = [] }: ClusterStoreModel = {}) {
const currentClusters = this.clusters.toJS();
const newClusters = new Map<ClusterId, Cluster>();
const removedClusters = new Map<ClusterId, Cluster>();
// update new clusters
for (const clusterModel of clusters) {
@ -247,16 +245,8 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
}
}
// update removed clusters
currentClusters.forEach(cluster => {
if (!newClusters.has(cluster.id)) {
removedClusters.set(cluster.id, cluster);
}
});
this.setActive(activeCluster);
this.clusters.replace(newClusters);
this.removedClusters.replace(removedClusters);
}
toJSON(): ClusterStoreModel {

View File

@ -0,0 +1,52 @@
import { CatalogCategoryRegistry } from "./catalog";
import { KubernetesCluster, WebLink } from "./catalog-entities";
export function registerDefaultCategories() {
const registry = CatalogCategoryRegistry.getInstance();
registry.add({
apiVersion: "catalog.k8slens.dev/v1alpha1",
kind: "CatalogCategory",
metadata: {
name: "Kubernetes Clusters",
icon: require(`!!raw-loader!./icons/kubernetes.svg`).default // eslint-disable-line
},
spec: {
group: "entity.k8slens.dev",
versions: [
{
version: "v1alpha1",
entityClass: KubernetesCluster
}
],
names: {
kind: "KubernetesCluster"
}
},
onAddMenuOpen: ({ navigate }) => [{
icon: "text_snippet",
title: "Add from kubeconfig",
onClick: () => navigate("/add-cluster"),
}],
});
registry.add({
apiVersion: "catalog.k8slens.dev/v1alpha1",
kind: "CatalogCategory",
metadata: {
name: "Web Links",
icon: "link"
},
spec: {
group: "entity.k8slens.dev",
versions: [
{
version: "v1alpha1",
entityClass: WebLink,
}
],
names: {
kind: "WebLink",
},
},
});
}

View File

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -22,11 +22,10 @@
// Inter-process communications (main <-> renderer)
// https://www.electronjs.org/docs/api/ipc-main
// https://www.electronjs.org/docs/api/ipc-renderer
import Electron, { ipcMain, ipcRenderer, remote } from "electron";
import { ipcMain, ipcRenderer, webContents, remote } from "electron";
import { toJS } from "mobx";
import { ClusterFrameInfo, ClusterManager } from "../../main/cluster-manager";
import logger from "../../main/logger";
import { ClusterFrameInfo, clusterFrameMap } from "../cluster-frames";
import type { Disposer } from "../utils";
const subFramesChannel = "ipc:get-sub-frames";
@ -36,11 +35,11 @@ export async function requestMain(channel: string, ...args: any[]) {
}
function getSubFrames(): ClusterFrameInfo[] {
return toJS(Array.from(clusterFrameMap.values()), { recurseEverything: true });
return ClusterManager.getInstance().getAllFrameInfo();
}
export function broadcastMessage(channel: string, ...args: any[]) {
const views = (webContents || remote?.webContents)?.getAllWebContents();
export async function broadcastMessage(channel: string, ...args: any[]) {
const views = (Electron.webContents || remote?.webContents)?.getAllWebContents();
if (!views) return;

View File

@ -21,7 +21,38 @@
import { action, IEnhancer, IObservableMapInitialValues, ObservableMap } from "mobx";
export class ExtendedMap<K, V> extends Map<K, V> {
export class DuplicateKeyError extends Error {
constructor(public key: any) {
super("Duplicate key in map");
}
}
export class StrictMap<K, V> extends Map<K, V> {
/**
* @throws if `key` already in map
*/
strictSet(key: K, val: V): this {
if (this.has(key)) {
throw new DuplicateKeyError(key);
}
return this.set(key, val);
}
}
export class ExtendedMap<K, V> extends StrictMap<K, V> {
static new<K, MK, MV>(): ExtendedMap<K, Map<MK, MV>> {
return new ExtendedMap<K, Map<MK, MV>>(() => new Map<MK, MV>());
}
static newExtended<K, MK, MV>(getDefault: () => MV): ExtendedMap<K, ExtendedMap<MK, MV>> {
return new ExtendedMap<K, ExtendedMap<MK, MV>>(() => new ExtendedMap<MK, MV>(getDefault));
}
static newExtendedStrict<K, MK, MMK, MMV>(): ExtendedMap<K, ExtendedMap<MK, StrictMap<MMK, MMV>>> {
return new ExtendedMap<K, ExtendedMap<MK, StrictMap<MMK, MMV>>>(() => new ExtendedMap<MK, StrictMap<MMK, MMV>>(() => new StrictMap<MMK, MMV>()));
}
constructor(protected getDefault: () => V, entries?: readonly (readonly [K, V])[] | null) {
super(entries);
}

View File

@ -18,17 +18,63 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import {
CatalogCategorySpec,
CatalogEntity,
CatalogEntityData,
CatalogEntityKindData,
CatalogEntityRegistry as InternalCatalogEntityRegistry,
MatchingCatalogEntityData,
} from "../../common/catalog";
import {
CatalogCategoryRegistry as InternalCatalogCategoryRegistry,
CatalogHandler,
CategoryHandlerNames,
CategoryHandlers,
EntityContextHandlers,
GlobalContextHandlers,
} from "../../common/catalog/catalog-category-registry";
import { Disposer } from "../../common/utils";
import { CatalogEntity, catalogEntityRegistry as registry } from "../../common/catalog";
export { catalogCategoryRegistry as catalogCategories } from "../../common/catalog/catalog-category-registry";
export * from "../../common/catalog-entities";
export class CatalogEntityRegistry {
getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] {
return registry.getItemsForApiKind<T>(apiVersion, kind);
static getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] {
return InternalCatalogEntityRegistry.getInstance().getItemsForApiKind<T>(apiVersion, kind);
}
}
export const catalogEntities = new CatalogEntityRegistry();
export class CatalogCategoryRegistry {
/**
* Registers a new category
* @param category The category to register
* @throws if the apiVersion and kind conflict with a previously registered category
* @returns a disposer to remove the category
*/
static add(category: CatalogCategorySpec): () => void {
return InternalCatalogCategoryRegistry.getInstance().add(category);
}
static registerHandler(apiVersion: string, kind: string, handlerName: CategoryHandlerNames, handler: CatalogHandler<typeof handlerName>): Disposer {
return InternalCatalogCategoryRegistry.getInstance().registerHandler(apiVersion, kind, handlerName, handler);
}
static runEntityHandlersFor(entity: CatalogEntity, handlerName: "onContextMenuOpen"): ReturnType<CategoryHandlers[typeof handlerName]>;
static runEntityHandlersFor(entity: CatalogEntity, handlerName: "onSettingsOpen"): ReturnType<CategoryHandlers[typeof handlerName]>;
static runEntityHandlersFor(entity: CatalogEntity, handlerName: EntityContextHandlers): ReturnType<CategoryHandlers[typeof handlerName]> {
return InternalCatalogCategoryRegistry.getInstance().runEntityHandlersFor(entity, handlerName as any);
}
static runGlobalHandlersFor(categorySpec: CatalogCategorySpec, handlerName: "onAddMenuOpen"): ReturnType<CategoryHandlers[typeof handlerName]>;
static runGlobalHandlersFor(categorySpec: CatalogCategorySpec, handlerName: GlobalContextHandlers): ReturnType<CategoryHandlers[typeof handlerName]> {
return InternalCatalogCategoryRegistry.getInstance().runGlobalHandlersFor(categorySpec, handlerName as any);
}
static getEntityForData<Entity extends CatalogEntity>(data: MatchingCatalogEntityData<Entity> & CatalogEntityKindData): Entity {
return InternalCatalogCategoryRegistry.getInstance().getEntityForData(data);
}
static getCategorySpecForEntity(data: CatalogEntityData & CatalogEntityKindData): CatalogCategorySpec {
return InternalCatalogCategoryRegistry.getInstance().getCategoryForEntity(data);
}
}

View File

@ -19,6 +19,6 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
export { Singleton, openExternal } from "../../common/utils";
export { Singleton, openExternal, disposer, Disposer, ExtendableDisposer } from "../../common/utils";
export { prevDefault, stopPropagation } from "../../renderer/utils/prevDefault";
export { cssNames } from "../../renderer/utils/cssNames";

View File

@ -24,7 +24,7 @@ import { action, observable, reaction } from "mobx";
import { FilesystemProvisionerStore } from "../main/extension-filesystem";
import logger from "../main/logger";
import type { ProtocolHandlerRegistration } from "./registries";
import { disposer } from "../common/utils";
import { Disposer, disposer } from "../common/utils";
export type LensExtensionId = string; // path to manifest (package.json)
export type LensExtensionConstructor = new (...args: ConstructorParameters<typeof LensExtension>) => LensExtension;
@ -106,28 +106,21 @@ export class LensExtension {
}
}
async whenEnabled(handlers: () => Promise<Function[]>) {
const disposers: Function[] = [];
const unregisterHandlers = () => {
disposers.forEach(unregister => unregister());
disposers.length = 0;
};
const cancelReaction = reaction(() => this.isEnabled, async (isEnabled) => {
if (isEnabled) {
const handlerDisposers = await handlers();
async whenEnabled(handlers: () => Promise<Disposer[]>) {
const disposers = disposer();
disposers.push(...handlerDisposers);
} else {
unregisterHandlers();
}
}, {
fireImmediately: true
});
return () => {
unregisterHandlers();
cancelReaction();
};
return disposer(
disposers,
reaction(() => this.isEnabled, async (isEnabled) => {
if (isEnabled) {
disposers.push(...(await handlers()));
} else {
disposers();
}
}, {
fireImmediately: true
})
);
}
protected onActivate(): void {

View File

@ -22,7 +22,7 @@
import { LensExtension } from "./lens-extension";
import { WindowManager } from "../main/window-manager";
import { getExtensionPageUrl } from "./registries/page-registry";
import { CatalogEntity, catalogEntityRegistry } from "../common/catalog";
import { CatalogEntity, CatalogEntityRegistry } from "../common/catalog";
import type { IObservableArray } from "mobx";
import type { MenuRegistration } from "./registries";
@ -41,10 +41,10 @@ export class LensMainExtension extends LensExtension {
}
addCatalogSource(id: string, source: IObservableArray<CatalogEntity>) {
catalogEntityRegistry.addObservableSource(`${this.name}:${id}`, source);
CatalogEntityRegistry.getInstance().addObservableSource(`${this.name}:${id}`, source);
}
removeCatalogSource(id: string) {
catalogEntityRegistry.removeSource(`${this.name}:${id}`);
CatalogEntityRegistry.getInstance().removeSource(`${this.name}:${id}`);
}
}

View File

@ -21,27 +21,24 @@
import { reaction, toJS } from "mobx";
import { broadcastMessage, ipcMainOn } from "../common/ipc";
import type { CatalogEntityRegistry} from "../common/catalog";
import { disposer, Singleton } from "../common/utils";
import { CatalogEntityRegistry } from "../common/catalog";
import "../common/catalog-entities/kubernetes-cluster";
import { disposer } from "../common/utils";
export class CatalogPusher {
static init(catalog: CatalogEntityRegistry) {
new CatalogPusher(catalog).init();
}
private constructor(private catalog: CatalogEntityRegistry) {}
export class CatalogPusher extends Singleton {
init() {
return disposer(
reaction(() => toJS(this.catalog.items, { recurseEverything: true }), (items) => {
reaction(() => toJS(CatalogEntityRegistry.getInstance().items, { recurseEverything: true }), (items) => {
console.log("pushing new items");
broadcastMessage("catalog:items", items);
}, {
fireImmediately: true,
}),
ipcMainOn("catalog:broadcast", () => {
broadcastMessage("catalog:items", toJS(this.catalog.items, { recurseEverything: true }));
})
broadcastMessage("catalog:items", toJS(CatalogEntityRegistry.getInstance().items, { recurseEverything: true }));
}),
);
}
}

View File

@ -20,13 +20,13 @@
*/
import { ObservableMap } from "mobx";
import type { CatalogEntity } from "../../../common/catalog";
import { loadFromOptions } from "../../../common/kube-helpers";
import type { Cluster } from "../../cluster";
import { computeDiff, configToModels } from "../kubeconfig-sync";
import mockFs from "mock-fs";
import fs from "fs";
import { ClusterStore } from "../../../common/cluster-store";
import type { KubernetesCluster } from "../../../common/catalog-entities";
describe("kubeconfig-sync.source tests", () => {
beforeEach(() => {
@ -79,7 +79,7 @@ describe("kubeconfig-sync.source tests", () => {
describe("computeDiff", () => {
it("should leave an empty source empty if there are no entries", () => {
const contents = "";
const rootSource = new ObservableMap<string, [Cluster, CatalogEntity]>();
const rootSource = new ObservableMap<string, KubernetesCluster>();
const filePath = "/bar";
computeDiff(contents, rootSource, filePath);
@ -114,7 +114,7 @@ describe("kubeconfig-sync.source tests", () => {
}],
currentContext: "foobar"
});
const rootSource = new ObservableMap<string, [Cluster, CatalogEntity]>();
const rootSource = new ObservableMap<string, KubernetesCluster>();
const filePath = "/bar";
fs.writeFileSync(filePath, contents);
@ -157,7 +157,7 @@ describe("kubeconfig-sync.source tests", () => {
}],
currentContext: "foobar"
});
const rootSource = new ObservableMap<string, [Cluster, CatalogEntity]>();
const rootSource = new ObservableMap<string, KubernetesCluster>();
const filePath = "/bar";
fs.writeFileSync(filePath, contents);
@ -211,7 +211,7 @@ describe("kubeconfig-sync.source tests", () => {
}],
currentContext: "foobar"
});
const rootSource = new ObservableMap<string, [Cluster, CatalogEntity]>();
const rootSource = new ObservableMap<string, KubernetesCluster>();
const filePath = "/bar";
fs.writeFileSync(filePath, contents);

View File

@ -18,9 +18,6 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { action, observable, IComputedValue, computed, ObservableMap, runInAction } from "mobx";
import { CatalogEntity, catalogEntityRegistry } from "../../common/catalog";
import { watch } from "chokidar";
import fs from "fs";
import fse from "fs-extra";
@ -29,22 +26,32 @@ import { Disposer, ExtendedObservableMap, iter, Singleton, storedKubeConfigFolde
import logger from "../logger";
import type { KubeConfig } from "@kubernetes/client-node";
import { loadConfigFromString, splitConfig, validateKubeConfig } from "../../common/kube-helpers";
import { Cluster } from "../cluster";
import { catalogEntityFromCluster } from "../cluster-manager";
import { UserStore } from "../../common/user-store";
import { ClusterStore } from "../../common/cluster-store";
import type { UpdateClusterModel } from "../../common/cluster-types";
import { createHash } from "crypto";
import { observable, ObservableMap, computed, action, runInAction } from "mobx";
import { CatalogEntityRegistry } from "../../common/catalog";
import { KubernetesCluster } from "../../common/catalog-entities";
import { Cluster } from "../cluster";
const logPrefix = "[KUBECONFIG-SYNC]:";
export class KubeconfigSyncManager extends Singleton {
protected sources = observable.map<string, [IComputedValue<CatalogEntity[]>, Disposer]>();
protected sources = observable.map<string, [ExtendedObservableMap<string, ObservableMap<string, KubernetesCluster>>, Disposer]>();
protected syncing = false;
protected syncListDisposer?: Disposer;
protected static readonly syncName = "lens:kube-sync";
protected items = computed(() => (
Array.from(iter.flatMap(
this.sources.values(),
([sources]) => iter.flatMap(
sources.values(),
source => source.values(),
),
))
));
@action
startSync(): void {
if (this.syncing) {
@ -55,12 +62,7 @@ export class KubeconfigSyncManager extends Singleton {
logger.info(`${logPrefix} starting requested syncs`);
catalogEntityRegistry.addComputedSource(KubeconfigSyncManager.syncName, computed(() => (
Array.from(iter.flatMap(
this.sources.values(),
([entities]) => entities.get()
))
)));
CatalogEntityRegistry.getInstance().addComputedSource(KubeconfigSyncManager.syncName, this.items);
// This must be done so that c&p-ed clusters are visible
this.startNewSync(storedKubeConfigFolder());
@ -89,7 +91,7 @@ export class KubeconfigSyncManager extends Singleton {
this.stopOldSync(filePath);
}
catalogEntityRegistry.removeSource(KubeconfigSyncManager.syncName);
CatalogEntityRegistry.getInstance().removeSource(KubeconfigSyncManager.syncName);
this.syncing = false;
}
@ -142,8 +144,7 @@ export function configToModels(config: KubeConfig, filePath: string): UpdateClus
return validConfigs;
}
type RootSourceValue = [Cluster, CatalogEntity];
type RootSource = ObservableMap<string, RootSourceValue>;
type RootSource = ObservableMap<string, KubernetesCluster>;
// exported for testing
export function computeDiff(contents: string, source: RootSource, filePath: string): void {
@ -154,12 +155,11 @@ export function computeDiff(contents: string, source: RootSource, filePath: stri
logger.debug(`${logPrefix} File now has ${models.size} entries`, { filePath });
for (const [contextName, value] of source) {
for (const [contextName] of source) {
const model = models.get(contextName);
// remove and disconnect clusters that were removed from the config
// remove clusters that were removed from the config
if (!model) {
value[0].disconnect();
source.delete(contextName);
logger.debug(`${logPrefix} Removed old cluster from sync`, { filePath, contextName });
continue;
@ -169,31 +169,32 @@ export function computeDiff(contents: string, source: RootSource, filePath: stri
// Probably should make it so that cluster keeps a copy of the config in its memory and
// diff against that
// or update the model and mark it as not needed to be added
value[0].updateModel(model);
// or mark it as not needed to be added
models.delete(contextName);
logger.debug(`${logPrefix} Updated old cluster from sync`, { filePath, contextName });
}
for (const [contextName, model] of models) {
// add new clusters to the source
try {
const clusterId = createHash("md5").update(`${filePath}:${contextName}`).digest("hex");
const cluster = ClusterStore.getInstance().getById(clusterId) || new Cluster({ ...model, id: clusterId});
if (!cluster.apiUrl) {
throw new Error("Cluster constructor failed, see above error");
source.set(contextName, new KubernetesCluster({
metadata: {
uid: Cluster.getDeteministicId(model),
name: model.contextName,
source: "local",
labels: {
file: filePath,
}
},
spec: {
kubeconfigPath: model.kubeConfigPath,
kubeconfigContext: model.contextName
},
status: {
phase: "disconnected",
}
}));
const entity = catalogEntityFromCluster(cluster);
entity.metadata.labels.file = filePath;
source.set(contextName, [cluster, entity]);
logger.debug(`${logPrefix} Added new cluster from sync`, { filePath, contextName });
} catch (error) {
logger.warn(`${logPrefix} Failed to create cluster from model: ${error}`, { filePath, contextName });
}
logger.debug(`${logPrefix} Added new cluster from sync`, { filePath, contextName });
}
} catch (error) {
logger.warn(`${logPrefix} Failed to compute diff: ${error}`, { filePath });
@ -242,15 +243,14 @@ function diffChangedConfig(filePath: string, source: RootSource): Disposer {
return cleanup;
}
async function watchFileChanges(filePath: string): Promise<[IComputedValue<CatalogEntity[]>, Disposer]> {
async function watchFileChanges(filePath: string): Promise<[ExtendedObservableMap<string, ObservableMap<string, KubernetesCluster>>, Disposer]> {
const stat = await fse.stat(filePath); // traverses symlinks, is a race condition
const watcher = watch(filePath, {
followSymlinks: true,
depth: stat.isDirectory() ? 0 : 1, // DIRs works with 0 but files need 1 (bug: https://github.com/paulmillr/chokidar/issues/1095)
disableGlobbing: true,
});
const rootSource = new ExtendedObservableMap<string, ObservableMap<string, RootSourceValue>>(observable.map);
const derivedSource = computed(() => Array.from(iter.flatMap(rootSource.values(), from => iter.map(from.values(), child => child[1]))));
const rootSource = new ExtendedObservableMap<string, ObservableMap<string, KubernetesCluster>>(observable.map);
const stoppers = new Map<string, Disposer>();
watcher
@ -268,5 +268,5 @@ async function watchFileChanges(filePath: string): Promise<[IComputedValue<Catal
})
.on("error", error => logger.error(`${logPrefix} watching file/folder failed: ${error}`, { filePath }));
return [derivedSource, () => watcher.close()];
return [rootSource, () => watcher.close()];
}

View File

@ -18,129 +18,161 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type http from "http";
import { ipcMain } from "electron";
import { action, autorun, reaction, toJS } from "mobx";
import { ClusterStore } from "../common/cluster-store";
import { getClusterIdFromHost } from "../common/cluster-types";
import type { Cluster } from "./cluster";
import logger from "./logger";
import { action, computed, observable, reaction } from "mobx";
import { CatalogEntityRegistry } from "../common/catalog";
import type { KubernetesCluster, KubernetesClusterStatus } from "../common/catalog-entities/kubernetes-cluster";
import * as ClusterChannels from "../common/cluster-ipc";
import { appEventBus } from "../common/event-bus";
import { iter, noop, Singleton } from "../common/utils";
import { apiKubePrefix } from "../common/vars";
import { Singleton } from "../common/utils";
import { catalogEntityRegistry } from "../common/catalog";
import { KubernetesCluster, KubernetesClusterPrometheusMetrics } from "../common/catalog-entities/kubernetes-cluster";
import { Cluster } from "./cluster";
import logger from "./logger";
import { ResourceApplier } from "./resource-applier";
import type http from "http";
import { ClusterId, getClusterIdFromHost } from "../common/cluster-types";
export type ClusterFrameInfo = {
frameId: number;
processId: number
};
export class ClusterManager extends Singleton {
protected clusterInstances = observable.map<ClusterId, Cluster>();
protected clusterFrameMap = observable.map<string, ClusterFrameInfo>();
constructor() {
super();
reaction(() => toJS(ClusterStore.getInstance().clustersList, { recurseEverything: true }), () => {
this.updateCatalog(ClusterStore.getInstance().clustersList);
}, { fireImmediately: true });
reaction(() => catalogEntityRegistry.getItemsForApiKind<KubernetesCluster>("entity.k8slens.dev/v1alpha1", "KubernetesCluster"), (entities) => {
reaction(() => CatalogEntityRegistry.getInstance().getItemsForApiKind<KubernetesCluster>("entity.k8slens.dev/v1alpha1", "KubernetesCluster"), (entities) => {
this.syncClustersFromCatalog(entities);
});
// auto-stop removed clusters
autorun(() => {
const removedClusters = Array.from(ClusterStore.getInstance().removedClusters.values());
if (removedClusters.length > 0) {
const meta = removedClusters.map(cluster => cluster.getMeta());
logger.info(`[CLUSTER-MANAGER]: removing clusters`, meta);
removedClusters.forEach(cluster => cluster.disconnect());
ClusterStore.getInstance().removedClusters.clear();
}
}, {
delay: 250
});
ipcMain.on("network:offline", () => { this.onNetworkOffline(); });
ipcMain.on("network:online", () => { this.onNetworkOnline(); });
ipcMain.on("network:offline", this.onNetworkOffline);
ipcMain.on("network:online", this.onNetworkOnline);
ipcMain.handle(ClusterChannels.activate, this.handleClusterActivate);
ipcMain.handle(ClusterChannels.setFrameId, this.handleClusteSetFrameId);
ipcMain.handle(ClusterChannels.refresh, this.handleClusterRefresh);
ipcMain.handle(ClusterChannels.disconnect, this.handleClusterDisconnect);
ipcMain.handle(ClusterChannels.kubectlApplyAll, this.handleKubectlApplyAll);
}
@action protected updateCatalog(clusters: Cluster[]) {
for (const cluster of clusters) {
const index = catalogEntityRegistry.items.findIndex((entity) => entity.metadata.uid === cluster.id);
if (index !== -1) {
const entity = catalogEntityRegistry.items[index] as KubernetesCluster;
entity.status.phase = cluster.disconnected ? "disconnected" : "connected";
entity.status.active = !cluster.disconnected;
if (cluster.preferences?.clusterName) {
entity.metadata.name = cluster.preferences.clusterName;
}
entity.spec.metrics ||= { source: "local" };
if (entity.spec.metrics.source === "local") {
const prometheus: KubernetesClusterPrometheusMetrics = entity.spec?.metrics?.prometheus || {};
prometheus.type = cluster.preferences.prometheusProvider?.type;
prometheus.address = cluster.preferences.prometheus;
entity.spec.metrics.prometheus = prometheus;
}
catalogEntityRegistry.items.splice(index, 1, entity);
}
}
/**
* Is a computed mapping between `frameId`'s and their associated `ClusterFrameInfo`
*/
@computed get frameMapById(): Map<number, number> {
return new Map(iter.map(this.clusterFrameMap.values(), info => [info.frameId, info.processId]));
}
@action syncClustersFromCatalog(entities: KubernetesCluster[]) {
for (const entity of entities) {
const cluster = ClusterStore.getInstance().getById(entity.metadata.uid);
const cluster = this.clusterInstances.get(entity.metadata.uid);
if (!cluster) {
ClusterStore.getInstance().addCluster({
this.clusterInstances.set(entity.metadata.uid, new Cluster({
id: entity.metadata.uid,
preferences: {
clusterName: entity.metadata.name
},
kubeConfigPath: entity.spec.kubeconfigPath,
contextName: entity.spec.kubeconfigContext
}));
// This is done so that the push to renderer is updated as necessary
// This also should prevent extensions from trying to set this themselves
// in the future.
Object.defineProperty(entity, "status", {
enumerable: true,
configurable: false,
writable: false,
get(): KubernetesClusterStatus {
return {
phase: cluster.disconnected ? "disconnected" : "connected",
active: !cluster.disconnected,
};
}
});
} else {
cluster.kubeConfigPath = entity.spec.kubeconfigPath;
cluster.contextName = entity.spec.kubeconfigContext;
entity.status = {
phase: cluster.disconnected ? "disconnected" : "connected",
active: !cluster.disconnected
};
}
}
}
protected onNetworkOffline() {
protected onNetworkOffline = () => {
logger.info("[CLUSTER-MANAGER]: network is offline");
ClusterStore.getInstance().clustersList.forEach((cluster) => {
for (const cluster of this.clusterInstances.values()) {
if (!cluster.disconnected) {
cluster.online = false;
cluster.accessible = false;
cluster.refreshConnectionStatus().catch((e) => e);
cluster.refreshConnectionStatus().catch(noop);
}
});
}
};
protected onNetworkOnline = () => {
logger.info("[CLUSTER-MANAGER]: network is online");
for (const cluster of this.clusterInstances.values()) {
if (!cluster.disconnected) {
cluster.refreshConnectionStatus().catch(noop);
}
}
};
protected handleClusterActivate = (event: Electron.IpcMainInvokeEvent, clusterId: ClusterId, force = false) => {
return this.clusterInstances.get(clusterId)?.activate(force);
};
protected handleClusteSetFrameId = ({ frameId, processId }: Electron.IpcMainInvokeEvent, clusterId: ClusterId) => {
const cluster = this.clusterInstances.get(clusterId);
if (!cluster) {
return;
}
this.clusterFrameMap.set(cluster.id, { frameId, processId });
return cluster.pushState();
};
protected handleClusterRefresh = (event: Electron.IpcMainInvokeEvent, clusterId: ClusterId) => {
return this.clusterInstances.get(clusterId)?.refresh({ refreshMetadata: true });
};
protected handleClusterDisconnect = (event: Electron.IpcMainInvokeEvent, clusterId: ClusterId) => {
return this.clusterInstances.get(clusterId)?.disconnect();
};
protected handleKubectlApplyAll = (event: Electron.IpcMainInvokeEvent, clusterId: ClusterId, resources: string[]) => {
appEventBus.emit({ name: "cluster", action: "kubectl-apply-all" });
const cluster = this.clusterInstances.get(clusterId);
if (!cluster) {
throw new Error(`${clusterId} is not a valid ID`);
}
return ResourceApplier.new(cluster).kubectlApplyAll(resources);
};
getFrameInfoByClusterId(clusterId: ClusterId): ClusterFrameInfo {
return this.clusterFrameMap.get(clusterId);
}
protected onNetworkOnline() {
logger.info("[CLUSTER-MANAGER]: network is online");
ClusterStore.getInstance().clustersList.forEach((cluster) => {
if (!cluster.disconnected) {
cluster.refreshConnectionStatus().catch((e) => e);
}
});
getFrameProcessIdById(frameId: number): number {
return this.frameMapById.get(frameId);
}
getAllFrameInfo(): ClusterFrameInfo[] {
return Array.from(this.clusterFrameMap.values());
}
stop() {
ClusterStore.getInstance().clusters.forEach((cluster: Cluster) => {
for (const cluster of this.clusterInstances.values()) {
cluster.disconnect();
});
}
}
getClusterForRequest(req: http.IncomingMessage): Cluster {
@ -150,45 +182,20 @@ export class ClusterManager extends Singleton {
if (req.headers.host.startsWith("127.0.0.1")) {
const clusterId = req.url.split("/")[1];
cluster = ClusterStore.getInstance().getById(clusterId);
cluster = this.clusterInstances.get(clusterId);
if (cluster) {
// we need to swap path prefix so that request is proxied to kube api
req.url = req.url.replace(`/${clusterId}`, apiKubePrefix);
}
} else if (req.headers["x-cluster-id"]) {
cluster = ClusterStore.getInstance().getById(req.headers["x-cluster-id"].toString());
cluster = this.clusterInstances.get(req.headers["x-cluster-id"].toString());
} else {
const clusterId = getClusterIdFromHost(req.headers.host);
cluster = ClusterStore.getInstance().getById(clusterId);
cluster = this.clusterInstances.get(clusterId);
}
return cluster;
}
}
export function catalogEntityFromCluster(cluster: Cluster) {
return new KubernetesCluster(toJS({
apiVersion: "entity.k8slens.dev/v1alpha1",
kind: "KubernetesCluster",
metadata: {
uid: cluster.id,
name: cluster.name,
source: "local",
labels: {
distro: cluster.distribution,
}
},
spec: {
kubeconfigPath: cluster.kubeConfigPath,
kubeconfigContext: cluster.contextName
},
status: {
phase: cluster.disconnected ? "disconnected" : "connected",
reason: "",
message: "",
active: !cluster.disconnected
}
}));
}

View File

@ -18,20 +18,21 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { AuthorizationV1Api, CoreV1Api, HttpError, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node";
import { createHash } from "crypto";
import { ipcMain } from "electron";
import { action, comparer, computed, observable, reaction, toJS, when } from "mobx";
import plimit from "p-limit";
import { broadcastMessage, ClusterListNamespaceForbiddenChannel } from "../common/ipc";
import { ContextHandler } from "./context-handler";
import { AuthorizationV1Api, CoreV1Api, HttpError, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node";
import { Kubectl } from "./kubectl";
import { KubeconfigManager } from "./kubeconfig-manager";
import { loadConfig, validateKubeConfig } from "../common/kube-helpers";
import { apiResourceRecord, apiResources, KubeApiResource, KubeResource } from "../common/rbac";
import logger from "./logger";
import { VersionDetector } from "./cluster-detectors/version-detector";
import { detectorRegistry } from "./cluster-detectors/detector-registry";
import plimit from "p-limit";
import { VersionDetector } from "./cluster-detectors/version-detector";
import { ContextHandler } from "./context-handler";
import { KubeconfigManager } from "./kubeconfig-manager";
import { Kubectl } from "./kubectl";
import logger from "./logger";
import type { ClusterModel, ClusterState, ClusterId, ClusterPreferences, ClusterMetadata, ClusterPrometheusPreferences, UpdateClusterModel, ClusterRefreshOptions } from "../common/cluster-types";
import { ClusterStatus } from "../common/cluster-types";
@ -41,6 +42,10 @@ import { ClusterStatus } from "../common/cluster-types";
* @beta
*/
export class Cluster implements ClusterModel, ClusterState {
public static getDeteministicId(model: UpdateClusterModel): ClusterId {
return createHash("md5").update(`${model.kubeConfigPath}:${model.contextName}`).digest("hex");
}
/** Unique id for a cluster */
public readonly id: ClusterId;
/**

View File

@ -23,6 +23,7 @@
import "../common/system-ca";
import "../common/prometheus-providers";
import "./initilizers";
import * as Mobx from "mobx";
import * as LensExtensions from "../extensions/core-api";
import { app, autoUpdater, ipcMain, dialog, powerMonitor } from "electron";
@ -50,7 +51,6 @@ import { initGetSubFramesHandler } from "../common/ipc";
import { startUpdateChecking } from "./app-updater";
import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
import { CatalogPusher } from "./catalog-pusher";
import { catalogEntityRegistry } from "../common/catalog";
import { HotbarStore } from "../common/hotbar-store";
import { HelmRepoManager } from "./helm/helm-repo-manager";
import { KubeconfigSyncManager } from "./catalog-sources";
@ -59,6 +59,8 @@ import { initIpcMainHandlers } from "./initializers/ipc-handlers";
import { Router } from "./router";
import { initMenu } from "./menu";
import { initTray } from "./tray";
import { CatalogCategoryRegistry, CatalogEntityRegistry } from "../common/catalog";
import { registerDefaultCategories } from "../common/default-categories";
const workingDir = path.join(app.getPath("appData"), appName);
const cleanup = disposer();
@ -127,6 +129,10 @@ app.on("ready", async () => {
registerFileProtocol("static", __static);
CatalogCategoryRegistry.createInstance();
registerDefaultCategories();
CatalogEntityRegistry.createInstance();
const userStore = UserStore.createInstance();
const clusterStore = ClusterStore.createInstance();
const hotbarStore = HotbarStore.createInstance();
@ -197,7 +203,7 @@ app.on("ready", async () => {
}
ipcMain.on(IpcRendererNavigationEvents.LOADED, () => {
CatalogPusher.init(catalogEntityRegistry);
CatalogPusher.createInstance().init();
startUpdateChecking();
LensProtocolRouterMain
.getInstance()

View File

@ -31,7 +31,11 @@ import { appEventBus } from "../common/event-bus";
import { cloneJsonObject } from "../common/utils";
export class ResourceApplier {
constructor(protected cluster: Cluster) {
static new(cluster: Cluster) {
return new ResourceApplier(cluster);
}
protected constructor(protected cluster: Cluster) {
}
async apply(resource: KubernetesObject | any): Promise<any> {

View File

@ -26,10 +26,10 @@ import windowStateKeeper from "electron-window-state";
import { appEventBus } from "../common/event-bus";
import { ipcMainOn } from "../common/ipc";
import { Singleton } from "../common/utils";
import { ClusterFrameInfo, clusterFrameMap } from "../common/cluster-frames";
import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
import logger from "./logger";
import { productName } from "../common/vars";
import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
import { ClusterManager } from "./cluster-manager";
import logger from "./logger";
import { LensProxy } from "./proxy/lens-proxy";
export class WindowManager extends Singleton {
@ -135,34 +135,33 @@ export class WindowManager extends Singleton {
return this.mainWindow;
}
sendToView({ channel, frameInfo, data = [] }: { channel: string, frameInfo?: ClusterFrameInfo, data?: any[] }) {
if (frameInfo) {
this.mainWindow.webContents.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...data);
} else {
this.mainWindow.webContents.send(channel, ...data);
}
}
async navigate(url: string, frameId?: number) {
await this.ensureMainWindow();
const frameInfo = Array.from(clusterFrameMap.values()).find((frameInfo) => frameInfo.frameId === frameId);
const channel = frameInfo
? IpcRendererNavigationEvents.NAVIGATE_IN_CLUSTER
: IpcRendererNavigationEvents.NAVIGATE_IN_APP;
if (frameId === undefined) {
const processId = ClusterManager.getInstance().getFrameProcessIdById(frameId);
this.sendToView({
channel,
frameInfo,
data: [url],
});
this.mainWindow.webContents.sendToFrame(
[processId, frameId],
IpcRendererNavigationEvents.NAVIGATE_IN_APP,
url
);
} else {
this.mainWindow.webContents.send(
IpcRendererNavigationEvents.NAVIGATE_IN_CLUSTER,
url
);
}
}
reload() {
const frameInfo = clusterFrameMap.get(this.activeClusterId);
const frameInfo = ClusterManager.getInstance().getFrameInfoByClusterId(this.activeClusterId);
if (frameInfo) {
this.sendToView({ channel: IpcRendererNavigationEvents.RELOAD_PAGE, frameInfo });
this.mainWindow.webContents.sendToFrame(
[frameInfo.processId, frameInfo.frameId],
IpcRendererNavigationEvents.RELOAD_PAGE,
);
} else {
webContents.getFocusedWebContents()?.reload();
}

View File

@ -21,12 +21,15 @@
import { CatalogEntityRegistry } from "../catalog-entity-registry";
import "../../../common/catalog-entities";
import { catalogCategoryRegistry } from "../../../common/catalog/catalog-category-registry";
describe("CatalogEntityRegistry", () => {
describe("updateItems", () => {
beforeEach(() => {
CatalogEntityRegistry.resetInstance();
});
it("adds new catalog item", () => {
const catalog = new CatalogEntityRegistry(catalogCategoryRegistry);
const catalog = CatalogEntityRegistry.createInstance();
const items = [{
apiVersion: "entity.k8slens.dev/v1alpha1",
kind: "KubernetesCluster",
@ -65,7 +68,7 @@ describe("CatalogEntityRegistry", () => {
});
it("updates existing items", () => {
const catalog = new CatalogEntityRegistry(catalogCategoryRegistry);
const catalog = CatalogEntityRegistry.createInstance();
const items = [{
apiVersion: "entity.k8slens.dev/v1alpha1",
kind: "KubernetesCluster",
@ -93,7 +96,7 @@ describe("CatalogEntityRegistry", () => {
});
it("removes deleted items", () => {
const catalog = new CatalogEntityRegistry(catalogCategoryRegistry);
const catalog = CatalogEntityRegistry.createInstance();
const items = [
{
apiVersion: "entity.k8slens.dev/v1alpha1",

View File

@ -19,4 +19,4 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
export { catalogCategoryRegistry } from "../../common/catalog";
export { CatalogCategoryRegistry } from "../../common/catalog";

View File

@ -18,18 +18,24 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { action, observable } from "mobx";
import { broadcastMessage, ipcRendererOn } from "../../common/ipc";
import { CatalogCategory, CatalogEntity, CatalogEntityData, catalogCategoryRegistry, CatalogCategoryRegistry, CatalogEntityKindData } from "../../common/catalog";
import "../../common/catalog-entities";
export class CatalogEntityRegistry {
import { action, observable } from "mobx";
import {
CatalogCategoryRegistry,
CatalogCategorySpec,
CatalogEntity,
CatalogEntityData,
CatalogEntityKindData,
} from "../../common/catalog";
import { broadcastMessage, ipcRendererOn } from "../../common/ipc";
import { Singleton } from "../utils";
export class CatalogEntityRegistry extends Singleton {
@observable protected _items: CatalogEntity[] = observable.array([], { deep: true });
@observable protected _activeEntity: CatalogEntity;
constructor(private categoryRegistry: CatalogCategoryRegistry) {}
init() {
ipcRendererOn("catalog:items", (ev, items: (CatalogEntityData & CatalogEntityKindData)[]) => {
this.updateItems(items);
@ -38,7 +44,9 @@ export class CatalogEntityRegistry {
}
@action updateItems(items: (CatalogEntityData & CatalogEntityKindData)[]) {
this._items = items.map(data => this.categoryRegistry.getEntityForData(data));
const registry = CatalogCategoryRegistry.getInstance();
this._items = items.map(data => registry.getEntityForData(data));
}
set activeEntity(entity: CatalogEntity) {
@ -46,6 +54,8 @@ export class CatalogEntityRegistry {
}
get activeEntity() {
console.log(this._activeEntity);
return this._activeEntity;
}
@ -63,12 +73,10 @@ export class CatalogEntityRegistry {
return items as T[];
}
getItemsForCategory<T extends CatalogEntity>(category: CatalogCategory): T[] {
const supportedVersions = category.spec.versions.map((v) => `${category.spec.group}/${v.name}`);
getItemsForCategory<T extends CatalogEntity>(category: CatalogCategorySpec): T[] {
const supportedVersions = category.spec.versions.map((v) => `${category.spec.group}/${v.version}`);
const items = this._items.filter((item) => supportedVersions.includes(item.apiVersion) && item.kind === category.spec.names.kind);
return items as T[];
}
}
export const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);

View File

@ -23,19 +23,21 @@ import { navigate } from "../navigation";
import { CommandRegistry } from "../../extensions/registries";
import type { CatalogEntity } from "../../common/catalog";
export { CatalogCategory, CatalogEntity } from "../../common/catalog";
export { CatalogEntity } from "../../common/catalog";
export type {
CatalogCategorySpec,
CatalogEntityData,
CatalogEntityKindData,
CatalogEntityActionContext,
CatalogEntityAddMenuContext,
CatalogEntityAddMenu,
CatalogEntityContextMenu,
CatalogEntityContextMenuContext,
ActionContext,
ActionHandler,
MenuContext,
ContextMenu,
AddMenuOpenHandler,
ContextMenuOpenHandler,
} from "../../common/catalog";
export const catalogEntityRunContext = {
navigate: (url: string) => navigate(url),
navigate,
setCommandPaletteContext: (entity?: CatalogEntity) => {
CommandRegistry.getInstance().activeEntity = entity;
}

View File

@ -43,7 +43,9 @@ import { ThemeStore } from "./theme.store";
import { HelmRepoManager } from "../main/helm/helm-repo-manager";
import { ExtensionInstallationStateStore } from "./components/+extensions/extension-install.store";
import { DefaultProps } from "./mui-base-theme";
import { initCommandRegistry, initEntitySettingsRegistry, initKubeObjectMenuRegistry, initRegistries, initWelcomeMenuRegistry, intiKubeObjectDetailRegistry } from "./initializers";
import * as initializers from "./initializers";
import { CatalogCategoryRegistry, CatalogEntityRegistry } from "../common/catalog";
import { registerDefaultCategories } from "../common/default-categories";
/**
* If this is a development buid, wait a second to attach
@ -75,12 +77,16 @@ export async function bootstrap(App: AppComponent) {
await attachChromeDebugger();
rootElem.classList.toggle("is-mac", isMac);
initRegistries();
initCommandRegistry();
initEntitySettingsRegistry();
initKubeObjectMenuRegistry();
intiKubeObjectDetailRegistry();
initWelcomeMenuRegistry();
initializers.initRegistries();
initializers.initCommandRegistry();
initializers.initEntitySettingsRegistry();
initializers.initKubeObjectMenuRegistry();
initializers.intiKubeObjectDetailRegistry();
initializers.initWelcomeMenuRegistry();
CatalogCategoryRegistry.createInstance();
registerDefaultCategories();
CatalogEntityRegistry.createInstance();
ExtensionLoader.createInstance().init();
ExtensionDiscovery.createInstance().init();

View File

@ -26,32 +26,22 @@ import { Icon } from "../icon";
import { disposeOnUnmount, observer } from "mobx-react";
import { observable, reaction } from "mobx";
import { autobind } from "../../../common/utils";
import type { CatalogCategory, CatalogEntityAddMenuContext, CatalogEntityAddMenu } from "../../api/catalog-entity";
import { EventEmitter } from "events";
import { navigate } from "../../navigation";
import { CatalogCategoryRegistry } from "../../api/catalog-category-registry";
import type { CatalogCategorySpec, ContextMenu } from "../../api/catalog-entity";
export type CatalogAddButtonProps = {
category: CatalogCategory
category: CatalogCategorySpec,
};
@observer
export class CatalogAddButton extends React.Component<CatalogAddButtonProps> {
@observable protected isOpen = false;
protected menuItems = observable.array<CatalogEntityAddMenu>([]);
@observable protected menuItems: ContextMenu[] = [];
componentDidMount() {
disposeOnUnmount(this, [
reaction(() => this.props.category, (category) => {
this.menuItems.clear();
if (category && category instanceof EventEmitter) {
const context: CatalogEntityAddMenuContext = {
navigate: (url: string) => navigate(url),
menuItems: this.menuItems
};
category.emit("onCatalogAddMenu", context);
}
this.menuItems = CatalogCategoryRegistry.getInstance().runGlobalHandlersFor(category, "onAddMenuOpen");
}, { fireImmediately: true })
]);
}

View File

@ -18,13 +18,11 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { action, computed, IReactionDisposer, observable, reaction } from "mobx";
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
import type { CatalogEntity, CatalogEntityActionContext } from "../../api/catalog-entity";
import { computed, IReactionDisposer, observable, reaction } from "mobx";
import type { CatalogEntity, ActionContext, MenuContext, CatalogCategorySpec } from "../../api/catalog-entity";
import { CatalogEntityRegistry } from "../../api/catalog-entity-registry";
import { ItemObject, ItemStore } from "../../item.store";
import { autobind } from "../../utils";
import { CatalogCategory } from "../../../common/catalog";
export class CatalogEntityItem implements ItemObject {
constructor(public entity: CatalogEntity) {}
@ -74,26 +72,27 @@ export class CatalogEntityItem implements ItemObject {
];
}
onRun(ctx: CatalogEntityActionContext) {
onRun(ctx: ActionContext) {
this.entity.onRun(ctx);
}
@action
async onContextMenuOpen(ctx: any) {
onContextMenuOpen(ctx: MenuContext) {
return this.entity.onContextMenuOpen(ctx);
}
}
@autobind()
export class CatalogEntityStore extends ItemStore<CatalogEntityItem> {
@observable activeCategory?: CatalogCategory;
@observable activeCategory?: CatalogCategorySpec;
@computed get entities() {
const registry = CatalogEntityRegistry.getInstance();
if (!this.activeCategory) {
return catalogEntityRegistry.items.map(entity => new CatalogEntityItem(entity));
return registry.items.map(entity => new CatalogEntityItem(entity));
}
return catalogEntityRegistry.getItemsForCategory(this.activeCategory).map(entity => new CatalogEntityItem(entity));
return registry.getItemsForCategory(this.activeCategory).map(entity => new CatalogEntityItem(entity));
}
watch() {

View File

@ -25,41 +25,43 @@ import { disposeOnUnmount, observer } from "mobx-react";
import { ItemListLayout } from "../item-object-list";
import { action, observable, reaction } from "mobx";
import { CatalogEntityItem, CatalogEntityStore } from "./catalog-entity.store";
import { navigate } from "../../navigation";
import { kebabCase } from "lodash";
import { PageLayout } from "../layout/page-layout";
import { MenuItem, MenuActions } from "../menu";
import { CatalogEntityContextMenu, CatalogEntityContextMenuContext, catalogEntityRunContext } from "../../api/catalog-entity";
import { Icon } from "../icon";
import { catalogEntityRunContext } from "../../api/catalog-entity";
import { Badge } from "../badge";
import { HotbarStore } from "../../../common/hotbar-store";
import { autobind } from "../../utils";
import { ConfirmDialog } from "../confirm-dialog";
import { Tab, Tabs } from "../tabs";
import { catalogCategoryRegistry } from "../../../common/catalog";
import { CatalogCategoryRegistry, CatalogCategorySpec, ContextMenu } from "../../../common/catalog";
import { CatalogAddButton } from "./catalog-add-button";
import { navigate } from "../../navigation";
enum sortBy {
name = "name",
source = "source",
status = "status"
}
function getId({ spec }: CatalogCategorySpec): string {
return `${spec.group}/${spec.names.kind}`;
}
@observer
export class Catalog extends React.Component {
@observable private catalogEntityStore?: CatalogEntityStore;
@observable.deep private contextMenu: CatalogEntityContextMenuContext;
@observable private menuItems: ContextMenu[] = [];
@observable activeTab?: string;
async componentDidMount() {
this.contextMenu = {
menuItems: [],
navigate: (url: string) => navigate(url)
};
this.catalogEntityStore = new CatalogEntityStore();
disposeOnUnmount(this, [
this.catalogEntityStore.watch(),
reaction(() => catalogCategoryRegistry.items, (items) => {
reaction(() => CatalogCategoryRegistry.getInstance().items, (items) => {
if (!this.activeTab && items.length > 0) {
this.activeTab = items[0].getId();
this.activeTab = getId(items[0]);
this.catalogEntityStore.activeCategory = items[0];
}
}, { fireImmediately: true })
@ -74,7 +76,7 @@ export class Catalog extends React.Component {
item.onRun(catalogEntityRunContext);
}
onMenuItemClick(menuItem: CatalogEntityContextMenu) {
onMenuItemClick(menuItem: ContextMenu) {
if (menuItem.confirm) {
ConfirmDialog.open({
okButtonProps: {
@ -91,16 +93,15 @@ export class Catalog extends React.Component {
}
}
get categories() {
return catalogCategoryRegistry.items;
}
@action
onTabChange = (tabId: string | null) => {
const activeCategory = this.categories.find(category => category.getId() === tabId);
const activeCategory = CatalogCategoryRegistry.getInstance().getById(tabId);
this.catalogEntityStore.activeCategory = activeCategory;
this.activeTab = activeCategory?.getId();
if (activeCategory) {
this.activeTab = `${activeCategory.spec.group}/${activeCategory.spec.names.kind}`;
}
};
renderNavigation() {
@ -115,14 +116,16 @@ export class Catalog extends React.Component {
data-testid="*-tab"
/>
{
this.categories.map(category => (
<Tab
value={category.getId()}
key={category.getId()}
label={category.metadata.name}
data-testid={`${category.getId()}-tab`}
/>
))
CatalogCategoryRegistry.getInstance()
.items
.map(category => (
<Tab
value={getId(category)}
key={getId(category)}
label={category.metadata.name}
data-testid={`${getId(category)}-tab`}
/>
))
}
</div>
</Tabs>
@ -131,10 +134,13 @@ export class Catalog extends React.Component {
@autobind()
renderItemMenu(item: CatalogEntityItem) {
const menuItems = this.contextMenu.menuItems.filter((menuItem) => !menuItem.onlyVisibleForSource || menuItem.onlyVisibleForSource === item.entity.metadata.source);
const menuItems = this.menuItems.filter((menuItem) => !menuItem.onlyVisibleForSource || menuItem.onlyVisibleForSource === item.entity.metadata.source);
return (
<MenuActions onOpen={() => item.onContextMenuOpen(this.contextMenu)}>
<MenuActions onOpen={() => this.menuItems = item.onContextMenuOpen({ navigate })} onClose={() => this.menuItems = []}>
<MenuItem key="add-to-hotbar" onClick={() => this.addToHotbar(item) }>
<Icon material="add" small interactive={true} title="Add to hotbar"/> Add to Hotbar
</MenuItem>
{
menuItems.map((menuItem, index) => (
<MenuItem key={index} onClick={() => this.onMenuItemClick(menuItem)}>

View File

@ -29,7 +29,7 @@ import { PageLayout } from "../layout/page-layout";
import { navigation } from "../../navigation";
import { Tabs, Tab } from "../tabs";
import type { CatalogEntity } from "../../api/catalog-entity";
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
import { CatalogEntityRegistry } from "../../api/catalog-entity-registry";
import { EntitySettingRegistry } from "../../../extensions/registries";
import type { EntitySettingsRouteParams } from "../../../common/routes";
import { groupBy } from "lodash";
@ -46,7 +46,7 @@ export class EntitySettings extends React.Component<Props> {
}
get entity(): CatalogEntity {
return catalogEntityRegistry.getById(this.entityId);
return CatalogEntityRegistry.getInstance().getById(this.entityId);
}
get menuItems() {

View File

@ -18,55 +18,53 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import React from "react";
import { disposeOnUnmount, observer } from "mobx-react";
import React from "react";
import { Redirect, Route, Router, Switch } from "react-router";
import { history } from "../navigation";
import { Notifications } from "./notifications";
import { NotFound } from "./+404";
import { UserManagement } from "./+user-management/user-management";
import { ConfirmDialog } from "./confirm-dialog";
import { KubeConfigDialog } from "./kubeconfig-dialog/kubeconfig-dialog";
import { Nodes } from "./+nodes";
import { Workloads } from "./+workloads";
import { Namespaces } from "./+namespaces";
import { Network } from "./+network";
import { Storage } from "./+storage";
import { ClusterOverview } from "./+cluster/cluster-overview";
import { Config } from "./+config";
import { Events } from "./+events/events";
import { Apps } from "./+apps";
import { KubeObjectDetails } from "./kube-object/kube-object-details";
import { AddRoleBindingDialog } from "./+user-management-roles-bindings";
import { DeploymentScaleDialog } from "./+workloads-deployments/deployment-scale-dialog";
import { CronJobTriggerDialog } from "./+workloads-cronjobs/cronjob-trigger-dialog";
import { CustomResources } from "./+custom-resources/custom-resources";
import { MainLayout } from "./layout/main-layout";
import { ErrorBoundary } from "./error-boundary";
import { Terminal } from "./dock/terminal";
import whatInput from "what-input";
import { setFrameId } from "../../common/cluster-ipc";
import { getHostedCluster } from "../../common/cluster-store";
import logger from "../../main/logger";
import { webFrame } from "electron";
import { ClusterPageRegistry, getExtensionPageUrl } from "../../extensions/registries/page-registry";
import { ExtensionLoader } from "../../extensions/extension-loader";
import { getHostedClusterId } from "../../common/cluster-types";
import { appEventBus } from "../../common/event-bus";
import { requestMain } from "../../common/ipc";
import whatInput from "what-input";
import { clusterSetFrameIdHandler } from "../../common/cluster-ipc";
import { ExtensionLoader } from "../../extensions/extension-loader";
import { ClusterPageMenuRegistration, ClusterPageMenuRegistry } from "../../extensions/registries";
import { TabLayout, TabLayoutRoute } from "./layout/tab-layout";
import { StatefulSetScaleDialog } from "./+workloads-statefulsets/statefulset-scale-dialog";
import { KubeWatchApi } from "../api/kube-watch-api";
import { ReplicaSetScaleDialog } from "./+workloads-replicasets/replicaset-scale-dialog";
import { CommandContainer } from "./command-palette/command-container";
import * as routes from "../../common/routes";
import { getHostedClusterId } from "../../common/cluster-types";
import { initApiManagerStores } from "../initializers/api-manager-stores";
import { ApiManager } from "../api/api-manager";
import { ClusterPageRegistry, getExtensionPageUrl } from "../../extensions/registries/page-registry";
import type { Cluster } from "../../main/cluster";
import { eventApi, namespacesApi, nodesApi, podsApi } from "../api/endpoints";
import logger from "../../main/logger";
import { ApiManager } from "../api/api-manager";
import { podsApi, nodesApi, eventApi, namespacesApi } from "../api/endpoints";
import { KubeWatchApi } from "../api/kube-watch-api";
import { initApiManagerStores } from "../initializers/api-manager-stores";
import { history } from "../navigation";
import { NotFound } from "./+404";
import { Apps } from "./+apps";
import { ReleaseStore } from "./+apps-releases/release.store";
import { ClusterOverview } from "./+cluster/cluster-overview";
import { Config } from "./+config";
import { CustomResources } from "./+custom-resources/custom-resources";
import { Events } from "./+events/events";
import { Namespaces } from "./+namespaces";
import { Network } from "./+network";
import { Nodes } from "./+nodes";
import { Storage } from "./+storage";
import { AddRoleBindingDialog } from "./+user-management-roles-bindings";
import { UserManagement } from "./+user-management/user-management";
import { Workloads } from "./+workloads";
import { CronJobTriggerDialog } from "./+workloads-cronjobs/cronjob-trigger-dialog";
import { DeploymentScaleDialog } from "./+workloads-deployments/deployment-scale-dialog";
import { ReplicaSetScaleDialog } from "./+workloads-replicasets/replicaset-scale-dialog";
import { StatefulSetScaleDialog } from "./+workloads-statefulsets/statefulset-scale-dialog";
import { CommandContainer } from "./command-palette/command-container";
import { ConfirmDialog } from "./confirm-dialog";
import { Terminal } from "./dock/terminal";
import { ErrorBoundary } from "./error-boundary";
import { KubeObjectDetails } from "./kube-object/kube-object-details";
import { KubeConfigDialog } from "./kubeconfig-dialog/kubeconfig-dialog";
import { MainLayout } from "./layout/main-layout";
import { TabLayout, TabLayoutRoute } from "./layout/tab-layout";
import { Notifications } from "./notifications";
import * as routes from "../../common/routes";
@observer
export class ClusterFrame extends React.Component {
@ -74,12 +72,12 @@ export class ClusterFrame extends React.Component {
static cluster: Cluster;
static async init() {
const frameId = webFrame.routingId;
const frameId = Electron.webFrame.routingId;
const clusterId = getHostedClusterId();
logger.info(`[APP]: Init dashboard, clusterId=${clusterId}, frameId=${frameId}`);
await Terminal.preloadFonts();
await requestMain(clusterSetFrameIdHandler, clusterId);
await requestMain(setFrameId, clusterId);
this.cluster = getHostedCluster();

View File

@ -18,14 +18,14 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import "./badge.scss";
import React from "react";
import { cssNames } from "../../utils/cssNames";
import { TooltipDecoratorProps, withTooltip } from "../tooltip";
export interface BadgeProps extends React.HTMLAttributes<any>, TooltipDecoratorProps {
export interface BadgeProps extends React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, TooltipDecoratorProps {
small?: boolean;
flat?: boolean;
label?: React.ReactNode;
@ -36,11 +36,11 @@ export class Badge extends React.Component<BadgeProps> {
render() {
const { className, label, small, flat, children, ...elemProps } = this.props;
return <>
return (
<span className={cssNames("Badge", { small, flat }, className)} {...elemProps}>
{label}
{children}
</span>
</>;
);
}
}

View File

@ -20,7 +20,9 @@
*/
import "./button.scss";
import React, { ButtonHTMLAttributes } from "react";
import { cssNames } from "../../utils";
import { TooltipDecoratorProps, withTooltip } from "../tooltip";

View File

@ -18,12 +18,8 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { KubeAuthProxyLog } from "../../../main/kube-auth-proxy";
import "./cluster-status.scss";
import React from "react";
import { observer } from "mobx-react";
import { ipcRenderer } from "electron";
import { computed, observable } from "mobx";
import { ipcRendererOn, requestMain } from "../../../common/ipc";
@ -34,7 +30,10 @@ import type { Cluster } from "../../../main/cluster";
import { ClusterStore } from "../../../common/cluster-store";
import type { ClusterId } from "../../../common/cluster-types";
import { CubeSpinner } from "../spinner";
import { clusterActivateHandler } from "../../../common/cluster-ipc";
import type { KubeAuthProxyLog } from "../../../main/kube-auth-proxy";
import { observer } from "mobx-react";
import React from "react-transition-group/node_modules/@types/react";
import { activate } from "../../../common/cluster-ipc";
interface Props {
className?: IClassName;
@ -68,7 +67,7 @@ export class ClusterStatus extends React.Component<Props> {
}
activateCluster = async (force = false) => {
await requestMain(clusterActivateHandler, this.props.clusterId, force);
await requestMain(activate, this.props.clusterId, force);
};
reconnect = async () => {

View File

@ -20,6 +20,7 @@
*/
import "./cluster-view.scss";
import React from "react";
import { reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
@ -29,10 +30,10 @@ import { hasLoadedView, initView, lensViews, refreshViews } from "./lens-views";
import type { Cluster } from "../../../main/cluster";
import { ClusterStore } from "../../../common/cluster-store";
import { requestMain } from "../../../common/ipc";
import { clusterActivateHandler } from "../../../common/cluster-ipc";
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
import { navigate } from "../../navigation";
import { catalogURL, ClusterViewRouteParams } from "../../../common/routes";
import { activate } from "../../../common/cluster-ipc";
import { CatalogEntityRegistry } from "../../api/catalog-entity-registry";
interface Props extends RouteComponentProps<ClusterViewRouteParams> {
}
@ -69,20 +70,20 @@ export class ClusterView extends React.Component<Props> {
showCluster(clusterId: string) {
initView(clusterId);
requestMain(clusterActivateHandler, this.clusterId, false);
requestMain(activate, this.clusterId, false);
const entity = catalogEntityRegistry.getById(this.clusterId);
const entity = CatalogEntityRegistry.getInstance().getById(this.clusterId);
if (entity) {
catalogEntityRegistry.activeEntity = entity;
CatalogEntityRegistry.getInstance().activeEntity = entity;
}
}
hideCluster() {
refreshViews();
if (catalogEntityRegistry.activeEntity?.metadata?.uid === this.clusterId) {
catalogEntityRegistry.activeEntity = null;
if (CatalogEntityRegistry.getInstance().activeEntity?.metadata?.uid === this.clusterId) {
CatalogEntityRegistry.getInstance().activeEntity = null;
}
}

View File

@ -25,13 +25,12 @@ import React, { DOMAttributes, useState } from "react";
import { Avatar } from "@material-ui/core";
import randomColor from "randomcolor";
import GraphemeSplitter from "grapheme-splitter";
import type { CatalogEntityContextMenu } from "../../../common/catalog";
import { cssNames, IClassName, iter } from "../../utils";
import { ConfirmDialog } from "../confirm-dialog";
import { Menu, MenuItem } from "../menu";
import { MaterialTooltip } from "../+catalog/material-tooltip/material-tooltip";
import { observer } from "mobx-react";
import type { ContextMenu } from "../../api/catalog-entity";
interface Props extends DOMAttributes<HTMLElement> {
uid: string;
@ -40,7 +39,7 @@ interface Props extends DOMAttributes<HTMLElement> {
onMenuOpen?: () => void;
className?: IClassName;
active?: boolean;
menuItems?: CatalogEntityContextMenu[];
menuItems?: ContextMenu[];
disabled?: boolean;
}
@ -50,7 +49,7 @@ function generateAvatarStyle(seed: string): React.CSSProperties {
};
}
function onMenuItemClick(menuItem: CatalogEntityContextMenu) {
function onMenuItemClick(menuItem: ContextMenu) {
if (menuItem.confirm) {
ConfirmDialog.open({
okButtonProps: {

View File

@ -25,7 +25,7 @@ import React from "react";
import { observer } from "mobx-react";
import { HotbarEntityIcon } from "./hotbar-entity-icon";
import { cssNames, IClassName } from "../../utils";
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
import { CatalogEntityRegistry } from "../../api/catalog-entity-registry";
import { defaultHotbarCells, HotbarItem, HotbarStore } from "../../../common/hotbar-store";
import { CatalogEntity, catalogEntityRunContext } from "../../api/catalog-entity";
import { DragDropContext, Draggable, Droppable, DropResult } from "react-beautiful-dnd";
@ -44,6 +44,10 @@ export class HotbarMenu extends React.Component<Props> {
return HotbarStore.getInstance().getActive();
}
isActive(item: CatalogEntity) {
return CatalogEntityRegistry.getInstance().activeEntity?.metadata?.uid == item.getId();
}
getEntity(item: HotbarItem) {
const hotbar = HotbarStore.getInstance().getActive();
@ -51,7 +55,7 @@ export class HotbarMenu extends React.Component<Props> {
return null;
}
return item ? catalogEntityRegistry.items.find((entity) => entity.metadata.uid === item.entity.uid) : null;
return item ? CatalogEntityRegistry.getInstance().items.find((entity) => entity.metadata.uid === item.entity.uid) : null;
}
onDragEnd(result: DropResult) {
@ -91,7 +95,7 @@ export class HotbarMenu extends React.Component<Props> {
@computed get items() {
const items = this.hotbar.items;
const activeEntity = catalogEntityRegistry.activeEntity;
const activeEntity = CatalogEntityRegistry.getInstance().activeEntity;
if (!activeEntity) return items;

View File

@ -22,8 +22,8 @@
import "./menu-actions.scss";
import React, { isValidElement } from "react";
import { observable } from "mobx";
import { observer } from "mobx-react";
import { observable, reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import { autobind, cssNames } from "../../utils";
import { ConfirmDialog } from "../confirm-dialog";
import { Icon, IconProps } from "../icon";
@ -40,6 +40,7 @@ export interface MenuActionsProps extends Partial<MenuProps> {
updateAction?(): void;
removeAction?(): void;
onOpen?(): void;
onClose?(): void;
}
@observer
@ -54,6 +55,12 @@ export class MenuActions extends React.Component<MenuActionsProps> {
@observable isOpen = !!this.props.toolbar;
componentDidMount() {
disposeOnUnmount(this, [
reaction(() => !this.isOpen, () => this.props.onClose?.()),
]);
}
toggle = () => {
if (this.props.toolbar) return;
this.isOpen = !this.isOpen;

View File

@ -35,14 +35,14 @@ import { LensProtocolRouterRenderer, bindProtocolAddRouteHandlers } from "./prot
import { registerIpcHandlers } from "./ipc";
import { ipcRenderer } from "electron";
import { IpcRendererNavigationEvents } from "./navigation/events";
import { catalogEntityRegistry } from "./api/catalog-entity-registry";
import { CommandRegistry } from "../extensions/registries";
import { CatalogEntityRegistry } from "./api/catalog-entity-registry";
import { reaction } from "mobx";
@observer
export class LensApp extends React.Component {
static async init() {
catalogEntityRegistry.init();
CatalogEntityRegistry.createInstance().init();
ExtensionLoader.getInstance().loadOnClusterManagerRenderer();
LensProtocolRouterRenderer.createInstance().init();
bindProtocolAddRouteHandlers();
@ -55,7 +55,7 @@ export class LensApp extends React.Component {
}
componentDidMount() {
reaction(() => catalogEntityRegistry.items, (items) => {
reaction(() => CatalogEntityRegistry.getInstance().items, (items) => {
const reg = CommandRegistry.getInstance();
if (reg.activeEntity && items.includes(reg.activeEntity)) {

View File

@ -22,7 +22,7 @@
import { attemptInstallByInfo } from "../components/+extensions";
import { LensProtocolRouterRenderer } from "./router";
import { navigate } from "../navigation/helpers";
import { catalogEntityRegistry } from "../api/catalog-entity-registry";
import { CatalogEntityRegistry } from "../api/catalog-entity-registry";
import { ClusterStore } from "../../common/cluster-store";
import { EXTENSION_NAME_MATCH, EXTENSION_PUBLISHER_MATCH, LensProtocolRouter } from "../../common/protocol-handler";
import * as routes from "../../common/routes";
@ -43,7 +43,7 @@ export function bindProtocolAddRouteHandlers() {
navigate(routes.addClusterURL());
})
.addInternalHandler("/entity/:entityId/settings", ({ pathname: { entityId } }) => {
const entity = catalogEntityRegistry.getById(entityId);
const entity = CatalogEntityRegistry.getInstance().getById(entityId);
if (entity) {
navigate(routes.entitySettingsURL({ params: { entityId } }));

View File

@ -51,6 +51,9 @@ export function createStorage<T>(key: string, defaultValue: T, observableOptions
// bind auto-saving
reaction(() => storage.toJSON(), saveFile, { delay: 250 });
// We don't clean up the cluster because it might come back in the future
// as cluster IDs are now deterministic based on "path" + "contextName"
}
async function saveFile(json = {}) {

View File

@ -117,6 +117,9 @@ export default function generateExtensionTypes(): webpack.Configuration {
extensions: [".ts", ".tsx", ".js"]
},
plugins: [
new CircularDependencyPlugin({
exclude: /node_modules/,
}),
// In ts-loader's README they said to output a built .d.ts file,
// you can set "declaration": true in tsconfig.extensions.json,
// and use the DeclarationBundlerPlugin in your webpack config... but

View File

@ -1644,20 +1644,13 @@
dependencies:
"@types/react" "*"
"@types/react@*":
version "16.9.35"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.35.tgz#a0830d172e8aadd9bd41709ba2281a3124bbd368"
integrity sha512-q0n0SsWcGc8nDqH2GJfWQWUOmZSJhXV64CjVN5SvcNti3TdEaA3AH0D8DwNmMdzjMAC/78tB8nAZIlV8yTz+zQ==
dependencies:
"@types/prop-types" "*"
csstype "^2.2.0"
"@types/react@^17.0.0":
version "17.0.0"
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.0.tgz#5af3eb7fad2807092f0046a1302b7823e27919b8"
integrity sha512-aj/L7RIMsRlWML3YB6KZiXB3fV2t41+5RBGYF8z+tAKU43Px8C3cYUZsDvf1/+Bm4FK21QWBrDutu8ZJ/70qOw==
"@types/react@*", "@types/react@^17.0.0":
version "17.0.5"
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.5.tgz#3d887570c4489011f75a3fc8f965bf87d09a1bea"
integrity sha512-bj4biDB9ZJmGAYTWSKJly6bMr4BLUiBrx9ujiJEoP9XIDY9CTaPGxE5QWN/1WjpPLzYF7/jRNnV2nNxNe970sw==
dependencies:
"@types/prop-types" "*"
"@types/scheduler" "*"
csstype "^3.0.2"
"@types/readable-stream@^2.3.9":
@ -1695,6 +1688,11 @@
resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d"
integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==
"@types/scheduler@*":
version "0.16.1"
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275"
integrity sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA==
"@types/semver@^7.2.0":
version "7.2.0"
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.2.0.tgz#0d72066965e910531e1db4621c15d0ca36b8d83b"
@ -4355,7 +4353,7 @@ cssstyle@^2.2.0:
dependencies:
cssom "~0.3.6"
csstype@^2.2.0, csstype@^2.5.2, csstype@^2.5.7, csstype@^2.6.5, csstype@^2.6.7:
csstype@^2.5.2, csstype@^2.5.7, csstype@^2.6.5, csstype@^2.6.7:
version "2.6.10"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.10.tgz#e63af50e66d7c266edb6b32909cfd0aabe03928b"
integrity sha512-D34BqZU4cIlMCY93rZHbrq9pjTAQJ3U8S8rfBqjwHxkGPThWFjzZDQpgMJY0QViLxth6ZKYiwFBo14RdN44U/w==