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 * 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. * 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 React from "react";
import { LensRendererExtension, Catalog } from "@k8slens/extensions";
import { MetricsSettings } from "./src/metrics-settings"; import { MetricsSettings } from "./src/metrics-settings";
export default class ClusterMetricsFeatureExtension extends LensRendererExtension { export default class ClusterMetricsFeatureExtension extends LensRendererExtension {
entitySettings = [ 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 * 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. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import path from "path";
import Config from "conf"; import Config from "conf";
import type { Options as ConfOptions } from "conf/dist/source/types"; import type { Options as ConfOptions } from "conf/dist/source/types";
import { app, ipcMain, ipcRenderer, remote } from "electron"; import { app, ipcMain, ipcRenderer, remote } from "electron";
import isEqual from "lodash/isEqual";
import { IReactionOptions, observable, reaction, runInAction, when } from "mobx"; import { IReactionOptions, observable, reaction, runInAction, when } from "mobx";
import { Singleton, getAppVersion } from "./utils"; import path from "path";
import logger from "../main/logger"; import logger from "../main/logger";
import { broadcastMessage, ipcMainOn, ipcRendererOn } from "./ipc"; 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> { export interface BaseStoreParams<T = any> extends ConfOptions<T> {
autoLoad?: boolean; autoLoad?: boolean;

View File

@ -18,16 +18,15 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * 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. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { app } from "electron";
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry"; import { CatalogEntity, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog";
import { CatalogEntity, CatalogEntityActionContext, CatalogEntityAddMenuContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus, CatalogCategory, CatalogCategorySpec } from "../catalog"; import type { ActionContext, ContextMenu, MenuContext } from "../catalog/catalog-entity";
import { clusterActivateHandler, clusterDisconnectHandler } from "../cluster-ipc"; import * as clusterIpc from "../cluster-ipc";
import { ClusterStore } from "../cluster-store"; import { ClusterStore } from "../cluster-store";
import { requestMain } from "../ipc"; import { requestMain } from "../ipc";
import { productName } from "../vars";
import { addClusterURL } from "../routes";
import { storedKubeConfigFolder } from "../utils"; import { storedKubeConfigFolder } from "../utils";
import { app } from "electron"; import { productName } from "../vars";
export type KubernetesClusterPrometheusMetrics = { export type KubernetesClusterPrometheusMetrics = {
address?: { address?: {
@ -49,7 +48,7 @@ export type KubernetesClusterSpec = {
}; };
export interface KubernetesClusterStatus extends CatalogEntityStatus { export interface KubernetesClusterStatus extends CatalogEntityStatus {
phase: "connected" | "disconnected"; phase?: "connected" | "disconnected";
} }
export class KubernetesCluster extends CatalogEntity<CatalogEntityMetadata, KubernetesClusterStatus, KubernetesClusterSpec> { export class KubernetesCluster extends CatalogEntity<CatalogEntityMetadata, KubernetesClusterStatus, KubernetesClusterSpec> {
@ -67,7 +66,7 @@ export class KubernetesCluster extends CatalogEntity<CatalogEntityMetadata, Kube
return; return;
} }
await requestMain(clusterActivateHandler, this.metadata.uid, false); await requestMain(clusterIpc.activate, this.metadata.uid, false);
return; return;
} }
@ -83,34 +82,39 @@ export class KubernetesCluster extends CatalogEntity<CatalogEntityMetadata, Kube
return; return;
} }
await requestMain(clusterDisconnectHandler, this.metadata.uid, false); await requestMain(clusterIpc.disconnect, this.metadata.uid, false);
return; return;
} }
async onRun(context: CatalogEntityActionContext) { onRun = (context: ActionContext) => {
context.navigate(`/cluster/${this.metadata.uid}`); 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) { res.push({
context.menuItems = [ icon: "settings",
{ title: "Settings",
title: "Settings", onlyVisibleForSource: "local",
onlyVisibleForSource: "local", onClick: async () => context.navigate(`/entity/${this.metadata.uid}/settings`)
onClick: async () => context.navigate(`/entity/${this.metadata.uid}/settings`) });
},
];
if (this.metadata.labels["file"]?.startsWith(storedKubeConfigFolder())) { if (this.metadata.labels["file"]?.startsWith(storedKubeConfigFolder())) {
context.menuItems.push({ res.push({
icon: "delete",
title: "Delete", title: "Delete",
onlyVisibleForSource: "local", onlyVisibleForSource: "local",
onClick: async () => ClusterStore.getInstance().removeById(this.metadata.uid), onClick: async () => ClusterStore.getInstance().removeById(this.metadata.uid),
@ -120,62 +124,6 @@ export class KubernetesCluster extends CatalogEntity<CatalogEntityMetadata, Kube
}); });
} }
if (this.status.phase == "connected") { return res;
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
}; };
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 * 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. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { CatalogEntity, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog";
import { CatalogCategory, CatalogEntity, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog";
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
export interface WebLinkStatus extends CatalogEntityStatus { export interface WebLinkStatus extends CatalogEntityStatus {
phase: "valid" | "invalid"; phase: "valid" | "invalid";
@ -34,42 +32,7 @@ export class WebLink extends CatalogEntity<CatalogEntityMetadata, WebLinkStatus,
public readonly apiVersion = "entity.k8slens.dev/v1alpha1"; public readonly apiVersion = "entity.k8slens.dev/v1alpha1";
public readonly kind = "WebLink"; public readonly kind = "WebLink";
async onRun() { onRun = () => {
window.open(this.spec.url, "_blank"); 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 * 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. * 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 { navigate } from "../../renderer/navigation";
import { CatalogCategory, CatalogEntityData, CatalogEntityKindData } from "./catalog-entity"; 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) { type KeysMatching<T, V> = { [K in keyof T]-?: T[K] extends V ? K : never }[keyof T];
this.categories.push(category); type KeysNotMatching<T, V> = { [K in keyof T]-?: T[K] extends V ? never : K }[keyof T];
}
@action remove(category: CatalogCategory) { export type CategoryHandlers = {
this.categories = this.categories.filter((cat) => cat.apiVersion !== category.apiVersion && cat.kind !== category.kind); [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() { export type EntityContextHandlers = keyof EntityContextGetters;
return toJS(this.categories); export type GlobalContextHandlers = keyof GlobalContextGetters;
}
getForGroupKind<T extends CatalogCategory>(group: string, kind: string) { type EntityContextGetters = {
return this.categories.find((c) => c.spec.group === group && c.spec.names.kind === kind) as T; [HandlerName in KeysMatching<CategoryHandlers, CategoryHandler<(...args: any) => any>>]: () => Rest<Parameters<CategoryHandlers[HandlerName]>>;
} };
getEntityForData(data: CatalogEntityData & CatalogEntityKindData) { type GlobalContextGetters = {
const category = this.getCategoryForEntity(data); [HandlerName in KeysNotMatching<CategoryHandlers, CategoryHandler<(...args: any) => any>>]: () => Parameters<CategoryHandlers[HandlerName]>;
};
if (!category) { const EntityContexts: EntityContextGetters = {
return null; 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 { action, computed, observable, IComputedValue, IObservableArray } from "mobx";
import type { CatalogEntity } from "./catalog-entity"; import type { CatalogEntity, CatalogEntityData, CatalogEntityKindData } from "./catalog-entity";
import { iter } from "../utils"; import { cloneJsonObject, iter, Singleton } from "../utils";
export class CatalogEntityRegistry { export class CatalogEntityRegistry extends Singleton {
protected sources = observable.map<string, IComputedValue<CatalogEntity[]>>([], { deep: true }); protected sources = observable.map<string, IComputedValue<CatalogEntity[]>>([], { deep: true });
@action addObservableSource(id: string, source: IObservableArray<CatalogEntity>) { @action addObservableSource(id: string, source: IObservableArray<CatalogEntity>) {
@ -38,8 +38,25 @@ export class CatalogEntityRegistry {
this.sources.delete(id); this.sources.delete(id);
} }
@computed get items(): CatalogEntity[] { @computed get items(): (CatalogEntityData & CatalogEntityKindData)[] {
return Array.from(iter.flatMap(this.sources.values(), source => source.get())); // 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[] { getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] {
@ -48,5 +65,3 @@ export class CatalogEntityRegistry {
return items as T[]; 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 * 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. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { EventEmitter } from "events";
import { observable } from "mobx"; 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 ExtractEntityMetadataType<Entity> = Entity extends CatalogEntity<infer Metadata> ? Metadata : never;
type ExtractEntityStatusType<Entity> = Entity extends CatalogEntity<any, infer Status> ? Status : never; type ExtractEntityStatusType<Entity> = Entity extends CatalogEntity<any, infer Status> ? Status : never;
type ExtractEntitySpecType<Entity> = Entity extends CatalogEntity<any, any, infer Spec> ? Spec : never; type ExtractEntitySpecType<Entity> = Entity extends CatalogEntity<any, any, infer Spec> ? Spec : never;
export type CatalogEntityConstructor<Entity extends CatalogEntity> = ( export type MatchingCatalogEntityData<Entity extends CatalogEntity> = CatalogEntityData<
(new (data: CatalogEntityData< ExtractEntityMetadataType<Entity>,
ExtractEntityMetadataType<Entity>, ExtractEntityStatusType<Entity>,
ExtractEntityStatusType<Entity>, ExtractEntitySpecType<Entity>
ExtractEntitySpecType<Entity> >;
>) => Entity)
); export type CatalogEntityConstructor<Entity extends CatalogEntity> = new (data: MatchingCatalogEntityData<Entity>) => Entity;
export interface CatalogCategoryVersion<Entity extends CatalogEntity> { export interface CatalogCategoryVersion<Entity extends CatalogEntity> {
name: string; version: string;
entityClass: CatalogEntityConstructor<Entity>; entityClass: CatalogEntityConstructor<Entity>;
} }
export interface CatalogCategorySpec { export interface CatalogCategorySpec {
group: string; readonly apiVersion: string;
versions: CatalogCategoryVersion<CatalogEntity>[]; readonly kind: string;
names: { readonly metadata: {
kind: string;
};
}
export abstract class CatalogCategory extends EventEmitter {
abstract readonly apiVersion: string;
abstract readonly kind: string;
abstract metadata: {
name: string; name: string;
icon: 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 { export interface CatalogEntityMetadata {
@ -71,18 +123,21 @@ export interface CatalogEntityMetadata {
} }
export interface CatalogEntityStatus { export interface CatalogEntityStatus {
phase: string; phase?: string;
reason?: string; reason?: string;
message?: string; message?: string;
active?: boolean; active?: boolean;
} }
export interface CatalogEntityActionContext { export interface ActionContext {
navigate: (url: string) => void; navigate: (url: string) => void;
setCommandPaletteContext: (context?: CatalogEntity) => void; setCommandPaletteContext: (context?: CatalogEntity) => void;
} }
export interface CatalogEntityContextMenu { export type ActionHandler = (ctx: ActionContext) => void;
export interface ContextMenu {
icon: string;
title: string; title: string;
onlyVisibleForSource?: string; // show only if empty or if matches with entity source onlyVisibleForSource?: string; // show only if empty or if matches with entity source
onClick: () => void | Promise<void>; onClick: () => void | Promise<void>;
@ -91,11 +146,19 @@ export interface CatalogEntityContextMenu {
} }
} }
export interface CatalogEntityAddMenu extends CatalogEntityContextMenu { export interface MenuContext {
icon: string; 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; group?: string;
title: string; title: string;
components: { components: {
@ -103,19 +166,7 @@ export interface CatalogEntitySettingsMenu {
}; };
} }
export interface CatalogEntityContextMenuContext { export type SettingsMenuOpenHandler = (ctx: SettingsContext) => SettingsMenu[];
navigate: (url: string) => void;
menuItems: CatalogEntityContextMenu[];
}
export interface CatalogEntitySettingsContext {
menuItems: CatalogEntityContextMenu[];
}
export interface CatalogEntityAddMenuContext {
navigate: (url: string) => void;
menuItems: CatalogEntityAddMenu[];
}
export type CatalogEntitySpec = Record<string, any>; export type CatalogEntitySpec = Record<string, any>;
@ -160,8 +211,7 @@ export abstract class CatalogEntity<
return this.metadata.name; return this.metadata.name;
} }
public abstract onRun?(context: CatalogEntityActionContext): void | Promise<void>; public onRun?: ActionHandler;
public abstract onDetailsOpen(context: CatalogEntityActionContext): void | Promise<void>; public onContextMenuOpen?: ContextMenuOpenHandler;
public abstract onContextMenuOpen(context: CatalogEntityContextMenuContext): void | Promise<void>; public onSettingsOpen?: SettingsMenuOpenHandler;
public abstract onSettingsOpen(context: CatalogEntitySettingsContext): void | Promise<void>;
} }

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. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
export const clusterActivateHandler = "cluster:activate"; export const activate = "cluster:activate";
export const clusterSetFrameIdHandler = "cluster:set-frame-id"; export const setFrameId = "cluster:set-frame-id";
export const clusterRefreshHandler = "cluster:refresh"; export const refresh = "cluster:refresh";
export const clusterDisconnectHandler = "cluster:disconnect"; export const disconnect = "cluster:disconnect";
export const clusterKubectlApplyAllHandler = "cluster:kubectl-apply-all"; export const kubectlApplyAll = "cluster:kubectl-apply-all";
export const clusterKubectlDeleteAllHandler = "cluster:kubectl-delete-all";

View File

@ -38,7 +38,6 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
private static StateChannel = "cluster:state"; private static StateChannel = "cluster:state";
@observable activeCluster: ClusterId; @observable activeCluster: ClusterId;
@observable removedClusters = observable.map<ClusterId, Cluster>();
@observable clusters = observable.map<ClusterId, Cluster>(); @observable clusters = observable.map<ClusterId, Cluster>();
private static stateRequestChannel = "cluster:states"; private static stateRequestChannel = "cluster:states";
@ -229,7 +228,6 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
protected fromStore({ activeCluster, clusters = [] }: ClusterStoreModel = {}) { protected fromStore({ activeCluster, clusters = [] }: ClusterStoreModel = {}) {
const currentClusters = this.clusters.toJS(); const currentClusters = this.clusters.toJS();
const newClusters = new Map<ClusterId, Cluster>(); const newClusters = new Map<ClusterId, Cluster>();
const removedClusters = new Map<ClusterId, Cluster>();
// update new clusters // update new clusters
for (const clusterModel of 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.setActive(activeCluster);
this.clusters.replace(newClusters); this.clusters.replace(newClusters);
this.removedClusters.replace(removedClusters);
} }
toJSON(): ClusterStoreModel { 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) // Inter-process communications (main <-> renderer)
// https://www.electronjs.org/docs/api/ipc-main // https://www.electronjs.org/docs/api/ipc-main
// https://www.electronjs.org/docs/api/ipc-renderer // https://www.electronjs.org/docs/api/ipc-renderer
import Electron, { ipcMain, ipcRenderer, remote } from "electron";
import { ipcMain, ipcRenderer, webContents, remote } from "electron"; import { ClusterFrameInfo, ClusterManager } from "../../main/cluster-manager";
import { toJS } from "mobx";
import logger from "../../main/logger"; import logger from "../../main/logger";
import { ClusterFrameInfo, clusterFrameMap } from "../cluster-frames";
import type { Disposer } from "../utils"; import type { Disposer } from "../utils";
const subFramesChannel = "ipc:get-sub-frames"; const subFramesChannel = "ipc:get-sub-frames";
@ -36,11 +35,11 @@ export async function requestMain(channel: string, ...args: any[]) {
} }
function getSubFrames(): ClusterFrameInfo[] { function getSubFrames(): ClusterFrameInfo[] {
return toJS(Array.from(clusterFrameMap.values()), { recurseEverything: true }); return ClusterManager.getInstance().getAllFrameInfo();
} }
export function broadcastMessage(channel: string, ...args: any[]) { export async function broadcastMessage(channel: string, ...args: any[]) {
const views = (webContents || remote?.webContents)?.getAllWebContents(); const views = (Electron.webContents || remote?.webContents)?.getAllWebContents();
if (!views) return; if (!views) return;

View File

@ -21,7 +21,38 @@
import { action, IEnhancer, IObservableMapInitialValues, ObservableMap } from "mobx"; 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) { constructor(protected getDefault: () => V, entries?: readonly (readonly [K, V])[] | null) {
super(entries); super(entries);
} }

View File

@ -18,17 +18,63 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * 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. * 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 * from "../../common/catalog-entities";
export class CatalogEntityRegistry { export class CatalogEntityRegistry {
getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] { static getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] {
return registry.getItemsForApiKind<T>(apiVersion, kind); 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. * 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 { prevDefault, stopPropagation } from "../../renderer/utils/prevDefault";
export { cssNames } from "../../renderer/utils/cssNames"; 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 { FilesystemProvisionerStore } from "../main/extension-filesystem";
import logger from "../main/logger"; import logger from "../main/logger";
import type { ProtocolHandlerRegistration } from "./registries"; 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 LensExtensionId = string; // path to manifest (package.json)
export type LensExtensionConstructor = new (...args: ConstructorParameters<typeof LensExtension>) => LensExtension; export type LensExtensionConstructor = new (...args: ConstructorParameters<typeof LensExtension>) => LensExtension;
@ -106,28 +106,21 @@ export class LensExtension {
} }
} }
async whenEnabled(handlers: () => Promise<Function[]>) { async whenEnabled(handlers: () => Promise<Disposer[]>) {
const disposers: Function[] = []; const disposers = disposer();
const unregisterHandlers = () => {
disposers.forEach(unregister => unregister());
disposers.length = 0;
};
const cancelReaction = reaction(() => this.isEnabled, async (isEnabled) => {
if (isEnabled) {
const handlerDisposers = await handlers();
disposers.push(...handlerDisposers); return disposer(
} else { disposers,
unregisterHandlers(); reaction(() => this.isEnabled, async (isEnabled) => {
} if (isEnabled) {
}, { disposers.push(...(await handlers()));
fireImmediately: true } else {
}); disposers();
}
return () => { }, {
unregisterHandlers(); fireImmediately: true
cancelReaction(); })
}; );
} }
protected onActivate(): void { protected onActivate(): void {

View File

@ -22,7 +22,7 @@
import { LensExtension } from "./lens-extension"; import { LensExtension } from "./lens-extension";
import { WindowManager } from "../main/window-manager"; import { WindowManager } from "../main/window-manager";
import { getExtensionPageUrl } from "./registries/page-registry"; 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 { IObservableArray } from "mobx";
import type { MenuRegistration } from "./registries"; import type { MenuRegistration } from "./registries";
@ -41,10 +41,10 @@ export class LensMainExtension extends LensExtension {
} }
addCatalogSource(id: string, source: IObservableArray<CatalogEntity>) { addCatalogSource(id: string, source: IObservableArray<CatalogEntity>) {
catalogEntityRegistry.addObservableSource(`${this.name}:${id}`, source); CatalogEntityRegistry.getInstance().addObservableSource(`${this.name}:${id}`, source);
} }
removeCatalogSource(id: string) { 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 { reaction, toJS } from "mobx";
import { broadcastMessage, ipcMainOn } from "../common/ipc"; 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 "../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() { init() {
return disposer( 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); broadcastMessage("catalog:items", items);
}, { }, {
fireImmediately: true, fireImmediately: true,
}), }),
ipcMainOn("catalog:broadcast", () => { 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 { ObservableMap } from "mobx";
import type { CatalogEntity } from "../../../common/catalog";
import { loadFromOptions } from "../../../common/kube-helpers"; import { loadFromOptions } from "../../../common/kube-helpers";
import type { Cluster } from "../../cluster"; import type { Cluster } from "../../cluster";
import { computeDiff, configToModels } from "../kubeconfig-sync"; import { computeDiff, configToModels } from "../kubeconfig-sync";
import mockFs from "mock-fs"; import mockFs from "mock-fs";
import fs from "fs"; import fs from "fs";
import { ClusterStore } from "../../../common/cluster-store"; import { ClusterStore } from "../../../common/cluster-store";
import type { KubernetesCluster } from "../../../common/catalog-entities";
describe("kubeconfig-sync.source tests", () => { describe("kubeconfig-sync.source tests", () => {
beforeEach(() => { beforeEach(() => {
@ -79,7 +79,7 @@ describe("kubeconfig-sync.source tests", () => {
describe("computeDiff", () => { describe("computeDiff", () => {
it("should leave an empty source empty if there are no entries", () => { it("should leave an empty source empty if there are no entries", () => {
const contents = ""; const contents = "";
const rootSource = new ObservableMap<string, [Cluster, CatalogEntity]>(); const rootSource = new ObservableMap<string, KubernetesCluster>();
const filePath = "/bar"; const filePath = "/bar";
computeDiff(contents, rootSource, filePath); computeDiff(contents, rootSource, filePath);
@ -114,7 +114,7 @@ describe("kubeconfig-sync.source tests", () => {
}], }],
currentContext: "foobar" currentContext: "foobar"
}); });
const rootSource = new ObservableMap<string, [Cluster, CatalogEntity]>(); const rootSource = new ObservableMap<string, KubernetesCluster>();
const filePath = "/bar"; const filePath = "/bar";
fs.writeFileSync(filePath, contents); fs.writeFileSync(filePath, contents);
@ -157,7 +157,7 @@ describe("kubeconfig-sync.source tests", () => {
}], }],
currentContext: "foobar" currentContext: "foobar"
}); });
const rootSource = new ObservableMap<string, [Cluster, CatalogEntity]>(); const rootSource = new ObservableMap<string, KubernetesCluster>();
const filePath = "/bar"; const filePath = "/bar";
fs.writeFileSync(filePath, contents); fs.writeFileSync(filePath, contents);
@ -211,7 +211,7 @@ describe("kubeconfig-sync.source tests", () => {
}], }],
currentContext: "foobar" currentContext: "foobar"
}); });
const rootSource = new ObservableMap<string, [Cluster, CatalogEntity]>(); const rootSource = new ObservableMap<string, KubernetesCluster>();
const filePath = "/bar"; const filePath = "/bar";
fs.writeFileSync(filePath, contents); fs.writeFileSync(filePath, contents);

View File

@ -18,9 +18,6 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * 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. * 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 { watch } from "chokidar";
import fs from "fs"; import fs from "fs";
import fse from "fs-extra"; import fse from "fs-extra";
@ -29,22 +26,32 @@ import { Disposer, ExtendedObservableMap, iter, Singleton, storedKubeConfigFolde
import logger from "../logger"; import logger from "../logger";
import type { KubeConfig } from "@kubernetes/client-node"; import type { KubeConfig } from "@kubernetes/client-node";
import { loadConfigFromString, splitConfig, validateKubeConfig } from "../../common/kube-helpers"; import { loadConfigFromString, splitConfig, validateKubeConfig } from "../../common/kube-helpers";
import { Cluster } from "../cluster";
import { catalogEntityFromCluster } from "../cluster-manager";
import { UserStore } from "../../common/user-store"; import { UserStore } from "../../common/user-store";
import { ClusterStore } from "../../common/cluster-store";
import type { UpdateClusterModel } from "../../common/cluster-types"; 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]:"; const logPrefix = "[KUBECONFIG-SYNC]:";
export class KubeconfigSyncManager extends Singleton { 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 syncing = false;
protected syncListDisposer?: Disposer; protected syncListDisposer?: Disposer;
protected static readonly syncName = "lens:kube-sync"; 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 @action
startSync(): void { startSync(): void {
if (this.syncing) { if (this.syncing) {
@ -55,12 +62,7 @@ export class KubeconfigSyncManager extends Singleton {
logger.info(`${logPrefix} starting requested syncs`); logger.info(`${logPrefix} starting requested syncs`);
catalogEntityRegistry.addComputedSource(KubeconfigSyncManager.syncName, computed(() => ( CatalogEntityRegistry.getInstance().addComputedSource(KubeconfigSyncManager.syncName, this.items);
Array.from(iter.flatMap(
this.sources.values(),
([entities]) => entities.get()
))
)));
// This must be done so that c&p-ed clusters are visible // This must be done so that c&p-ed clusters are visible
this.startNewSync(storedKubeConfigFolder()); this.startNewSync(storedKubeConfigFolder());
@ -89,7 +91,7 @@ export class KubeconfigSyncManager extends Singleton {
this.stopOldSync(filePath); this.stopOldSync(filePath);
} }
catalogEntityRegistry.removeSource(KubeconfigSyncManager.syncName); CatalogEntityRegistry.getInstance().removeSource(KubeconfigSyncManager.syncName);
this.syncing = false; this.syncing = false;
} }
@ -142,8 +144,7 @@ export function configToModels(config: KubeConfig, filePath: string): UpdateClus
return validConfigs; return validConfigs;
} }
type RootSourceValue = [Cluster, CatalogEntity]; type RootSource = ObservableMap<string, KubernetesCluster>;
type RootSource = ObservableMap<string, RootSourceValue>;
// exported for testing // exported for testing
export function computeDiff(contents: string, source: RootSource, filePath: string): void { 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 }); 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); 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) { if (!model) {
value[0].disconnect();
source.delete(contextName); source.delete(contextName);
logger.debug(`${logPrefix} Removed old cluster from sync`, { filePath, contextName }); logger.debug(`${logPrefix} Removed old cluster from sync`, { filePath, contextName });
continue; 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 // Probably should make it so that cluster keeps a copy of the config in its memory and
// diff against that // diff against that
// or update the model and mark it as not needed to be added // or mark it as not needed to be added
value[0].updateModel(model);
models.delete(contextName); models.delete(contextName);
logger.debug(`${logPrefix} Updated old cluster from sync`, { filePath, contextName }); logger.debug(`${logPrefix} Updated old cluster from sync`, { filePath, contextName });
} }
for (const [contextName, model] of models) { for (const [contextName, model] of models) {
// add new clusters to the source // add new clusters to the source
try { source.set(contextName, new KubernetesCluster({
const clusterId = createHash("md5").update(`${filePath}:${contextName}`).digest("hex"); metadata: {
const cluster = ClusterStore.getInstance().getById(clusterId) || new Cluster({ ...model, id: clusterId}); uid: Cluster.getDeteministicId(model),
name: model.contextName,
if (!cluster.apiUrl) { source: "local",
throw new Error("Cluster constructor failed, see above error"); labels: {
file: filePath,
}
},
spec: {
kubeconfigPath: model.kubeConfigPath,
kubeconfigContext: model.contextName
},
status: {
phase: "disconnected",
} }
}));
const entity = catalogEntityFromCluster(cluster); logger.debug(`${logPrefix} Added new cluster from sync`, { filePath, contextName });
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 });
}
} }
} catch (error) { } catch (error) {
logger.warn(`${logPrefix} Failed to compute diff: ${error}`, { filePath }); logger.warn(`${logPrefix} Failed to compute diff: ${error}`, { filePath });
@ -242,15 +243,14 @@ function diffChangedConfig(filePath: string, source: RootSource): Disposer {
return cleanup; 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 stat = await fse.stat(filePath); // traverses symlinks, is a race condition
const watcher = watch(filePath, { const watcher = watch(filePath, {
followSymlinks: true, followSymlinks: true,
depth: stat.isDirectory() ? 0 : 1, // DIRs works with 0 but files need 1 (bug: https://github.com/paulmillr/chokidar/issues/1095) depth: stat.isDirectory() ? 0 : 1, // DIRs works with 0 but files need 1 (bug: https://github.com/paulmillr/chokidar/issues/1095)
disableGlobbing: true, disableGlobbing: true,
}); });
const rootSource = new ExtendedObservableMap<string, ObservableMap<string, RootSourceValue>>(observable.map); const rootSource = new ExtendedObservableMap<string, ObservableMap<string, KubernetesCluster>>(observable.map);
const derivedSource = computed(() => Array.from(iter.flatMap(rootSource.values(), from => iter.map(from.values(), child => child[1]))));
const stoppers = new Map<string, Disposer>(); const stoppers = new Map<string, Disposer>();
watcher 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 })); .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 * 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. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import type http from "http";
import { ipcMain } from "electron"; import { ipcMain } from "electron";
import { action, autorun, reaction, toJS } from "mobx"; import { action, computed, observable, reaction } from "mobx";
import { ClusterStore } from "../common/cluster-store"; import { CatalogEntityRegistry } from "../common/catalog";
import { getClusterIdFromHost } from "../common/cluster-types"; import type { KubernetesCluster, KubernetesClusterStatus } from "../common/catalog-entities/kubernetes-cluster";
import type { Cluster } from "./cluster"; import * as ClusterChannels from "../common/cluster-ipc";
import logger from "./logger"; import { appEventBus } from "../common/event-bus";
import { iter, noop, Singleton } from "../common/utils";
import { apiKubePrefix } from "../common/vars"; import { apiKubePrefix } from "../common/vars";
import { Singleton } from "../common/utils"; import { Cluster } from "./cluster";
import { catalogEntityRegistry } from "../common/catalog"; import logger from "./logger";
import { KubernetesCluster, KubernetesClusterPrometheusMetrics } from "../common/catalog-entities/kubernetes-cluster"; 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 { export class ClusterManager extends Singleton {
protected clusterInstances = observable.map<ClusterId, Cluster>();
protected clusterFrameMap = observable.map<string, ClusterFrameInfo>();
constructor() { constructor() {
super(); super();
reaction(() => toJS(ClusterStore.getInstance().clustersList, { recurseEverything: true }), () => { reaction(() => CatalogEntityRegistry.getInstance().getItemsForApiKind<KubernetesCluster>("entity.k8slens.dev/v1alpha1", "KubernetesCluster"), (entities) => {
this.updateCatalog(ClusterStore.getInstance().clustersList);
}, { fireImmediately: true });
reaction(() => catalogEntityRegistry.getItemsForApiKind<KubernetesCluster>("entity.k8slens.dev/v1alpha1", "KubernetesCluster"), (entities) => {
this.syncClustersFromCatalog(entities); this.syncClustersFromCatalog(entities);
}); });
ipcMain.on("network:offline", this.onNetworkOffline);
// auto-stop removed clusters ipcMain.on("network:online", this.onNetworkOnline);
autorun(() => { ipcMain.handle(ClusterChannels.activate, this.handleClusterActivate);
const removedClusters = Array.from(ClusterStore.getInstance().removedClusters.values()); ipcMain.handle(ClusterChannels.setFrameId, this.handleClusteSetFrameId);
ipcMain.handle(ClusterChannels.refresh, this.handleClusterRefresh);
if (removedClusters.length > 0) { ipcMain.handle(ClusterChannels.disconnect, this.handleClusterDisconnect);
const meta = removedClusters.map(cluster => cluster.getMeta()); ipcMain.handle(ClusterChannels.kubectlApplyAll, this.handleKubectlApplyAll);
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(); });
} }
@action protected updateCatalog(clusters: Cluster[]) { /**
for (const cluster of clusters) { * Is a computed mapping between `frameId`'s and their associated `ClusterFrameInfo`
const index = catalogEntityRegistry.items.findIndex((entity) => entity.metadata.uid === cluster.id); */
@computed get frameMapById(): Map<number, number> {
if (index !== -1) { return new Map(iter.map(this.clusterFrameMap.values(), info => [info.frameId, info.processId]));
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);
}
}
} }
@action syncClustersFromCatalog(entities: KubernetesCluster[]) { @action syncClustersFromCatalog(entities: KubernetesCluster[]) {
for (const entity of entities) { for (const entity of entities) {
const cluster = ClusterStore.getInstance().getById(entity.metadata.uid); const cluster = this.clusterInstances.get(entity.metadata.uid);
if (!cluster) { if (!cluster) {
ClusterStore.getInstance().addCluster({ this.clusterInstances.set(entity.metadata.uid, new Cluster({
id: entity.metadata.uid, id: entity.metadata.uid,
preferences: { preferences: {
clusterName: entity.metadata.name clusterName: entity.metadata.name
}, },
kubeConfigPath: entity.spec.kubeconfigPath, kubeConfigPath: entity.spec.kubeconfigPath,
contextName: entity.spec.kubeconfigContext 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 { } else {
cluster.kubeConfigPath = entity.spec.kubeconfigPath; cluster.kubeConfigPath = entity.spec.kubeconfigPath;
cluster.contextName = entity.spec.kubeconfigContext; 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"); logger.info("[CLUSTER-MANAGER]: network is offline");
ClusterStore.getInstance().clustersList.forEach((cluster) => {
for (const cluster of this.clusterInstances.values()) {
if (!cluster.disconnected) { if (!cluster.disconnected) {
cluster.online = false; cluster.online = false;
cluster.accessible = 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() { getFrameProcessIdById(frameId: number): number {
logger.info("[CLUSTER-MANAGER]: network is online"); return this.frameMapById.get(frameId);
ClusterStore.getInstance().clustersList.forEach((cluster) => { }
if (!cluster.disconnected) {
cluster.refreshConnectionStatus().catch((e) => e); getAllFrameInfo(): ClusterFrameInfo[] {
} return Array.from(this.clusterFrameMap.values());
});
} }
stop() { stop() {
ClusterStore.getInstance().clusters.forEach((cluster: Cluster) => { for (const cluster of this.clusterInstances.values()) {
cluster.disconnect(); cluster.disconnect();
}); }
} }
getClusterForRequest(req: http.IncomingMessage): Cluster { getClusterForRequest(req: http.IncomingMessage): Cluster {
@ -150,45 +182,20 @@ export class ClusterManager extends Singleton {
if (req.headers.host.startsWith("127.0.0.1")) { if (req.headers.host.startsWith("127.0.0.1")) {
const clusterId = req.url.split("/")[1]; const clusterId = req.url.split("/")[1];
cluster = ClusterStore.getInstance().getById(clusterId); cluster = this.clusterInstances.get(clusterId);
if (cluster) { if (cluster) {
// we need to swap path prefix so that request is proxied to kube api // we need to swap path prefix so that request is proxied to kube api
req.url = req.url.replace(`/${clusterId}`, apiKubePrefix); req.url = req.url.replace(`/${clusterId}`, apiKubePrefix);
} }
} else if (req.headers["x-cluster-id"]) { } 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 { } else {
const clusterId = getClusterIdFromHost(req.headers.host); const clusterId = getClusterIdFromHost(req.headers.host);
cluster = ClusterStore.getInstance().getById(clusterId); cluster = this.clusterInstances.get(clusterId);
} }
return cluster; 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 * 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. * 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 { ipcMain } from "electron";
import { action, comparer, computed, observable, reaction, toJS, when } from "mobx"; import { action, comparer, computed, observable, reaction, toJS, when } from "mobx";
import plimit from "p-limit";
import { broadcastMessage, ClusterListNamespaceForbiddenChannel } from "../common/ipc"; 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 { loadConfig, validateKubeConfig } from "../common/kube-helpers";
import { apiResourceRecord, apiResources, KubeApiResource, KubeResource } from "../common/rbac"; 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 { 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 type { ClusterModel, ClusterState, ClusterId, ClusterPreferences, ClusterMetadata, ClusterPrometheusPreferences, UpdateClusterModel, ClusterRefreshOptions } from "../common/cluster-types";
import { ClusterStatus } from "../common/cluster-types"; import { ClusterStatus } from "../common/cluster-types";
@ -41,6 +42,10 @@ import { ClusterStatus } from "../common/cluster-types";
* @beta * @beta
*/ */
export class Cluster implements ClusterModel, ClusterState { 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 */ /** Unique id for a cluster */
public readonly id: ClusterId; public readonly id: ClusterId;
/** /**

View File

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

View File

@ -31,7 +31,11 @@ import { appEventBus } from "../common/event-bus";
import { cloneJsonObject } from "../common/utils"; import { cloneJsonObject } from "../common/utils";
export class ResourceApplier { 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> { 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 { appEventBus } from "../common/event-bus";
import { ipcMainOn } from "../common/ipc"; import { ipcMainOn } from "../common/ipc";
import { Singleton } from "../common/utils"; 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 { 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"; import { LensProxy } from "./proxy/lens-proxy";
export class WindowManager extends Singleton { export class WindowManager extends Singleton {
@ -135,34 +135,33 @@ export class WindowManager extends Singleton {
return this.mainWindow; 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) { async navigate(url: string, frameId?: number) {
await this.ensureMainWindow(); await this.ensureMainWindow();
const frameInfo = Array.from(clusterFrameMap.values()).find((frameInfo) => frameInfo.frameId === frameId); if (frameId === undefined) {
const channel = frameInfo const processId = ClusterManager.getInstance().getFrameProcessIdById(frameId);
? IpcRendererNavigationEvents.NAVIGATE_IN_CLUSTER
: IpcRendererNavigationEvents.NAVIGATE_IN_APP;
this.sendToView({ this.mainWindow.webContents.sendToFrame(
channel, [processId, frameId],
frameInfo, IpcRendererNavigationEvents.NAVIGATE_IN_APP,
data: [url], url
}); );
} else {
this.mainWindow.webContents.send(
IpcRendererNavigationEvents.NAVIGATE_IN_CLUSTER,
url
);
}
} }
reload() { reload() {
const frameInfo = clusterFrameMap.get(this.activeClusterId); const frameInfo = ClusterManager.getInstance().getFrameInfoByClusterId(this.activeClusterId);
if (frameInfo) { if (frameInfo) {
this.sendToView({ channel: IpcRendererNavigationEvents.RELOAD_PAGE, frameInfo }); this.mainWindow.webContents.sendToFrame(
[frameInfo.processId, frameInfo.frameId],
IpcRendererNavigationEvents.RELOAD_PAGE,
);
} else { } else {
webContents.getFocusedWebContents()?.reload(); webContents.getFocusedWebContents()?.reload();
} }

View File

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

View File

@ -19,4 +19,4 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * 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 * 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. * 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"; 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 _items: CatalogEntity[] = observable.array([], { deep: true });
@observable protected _activeEntity: CatalogEntity; @observable protected _activeEntity: CatalogEntity;
constructor(private categoryRegistry: CatalogCategoryRegistry) {}
init() { init() {
ipcRendererOn("catalog:items", (ev, items: (CatalogEntityData & CatalogEntityKindData)[]) => { ipcRendererOn("catalog:items", (ev, items: (CatalogEntityData & CatalogEntityKindData)[]) => {
this.updateItems(items); this.updateItems(items);
@ -38,7 +44,9 @@ export class CatalogEntityRegistry {
} }
@action updateItems(items: (CatalogEntityData & CatalogEntityKindData)[]) { @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) { set activeEntity(entity: CatalogEntity) {
@ -46,6 +54,8 @@ export class CatalogEntityRegistry {
} }
get activeEntity() { get activeEntity() {
console.log(this._activeEntity);
return this._activeEntity; return this._activeEntity;
} }
@ -63,12 +73,10 @@ export class CatalogEntityRegistry {
return items as T[]; return items as T[];
} }
getItemsForCategory<T extends CatalogEntity>(category: CatalogCategory): T[] { getItemsForCategory<T extends CatalogEntity>(category: CatalogCategorySpec): T[] {
const supportedVersions = category.spec.versions.map((v) => `${category.spec.group}/${v.name}`); 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); const items = this._items.filter((item) => supportedVersions.includes(item.apiVersion) && item.kind === category.spec.names.kind);
return items as T[]; 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 { CommandRegistry } from "../../extensions/registries";
import type { CatalogEntity } from "../../common/catalog"; import type { CatalogEntity } from "../../common/catalog";
export { CatalogCategory, CatalogEntity } from "../../common/catalog"; export { CatalogEntity } from "../../common/catalog";
export type { export type {
CatalogCategorySpec,
CatalogEntityData, CatalogEntityData,
CatalogEntityKindData, CatalogEntityKindData,
CatalogEntityActionContext, ActionContext,
CatalogEntityAddMenuContext, ActionHandler,
CatalogEntityAddMenu, MenuContext,
CatalogEntityContextMenu, ContextMenu,
CatalogEntityContextMenuContext, AddMenuOpenHandler,
ContextMenuOpenHandler,
} from "../../common/catalog"; } from "../../common/catalog";
export const catalogEntityRunContext = { export const catalogEntityRunContext = {
navigate: (url: string) => navigate(url), navigate,
setCommandPaletteContext: (entity?: CatalogEntity) => { setCommandPaletteContext: (entity?: CatalogEntity) => {
CommandRegistry.getInstance().activeEntity = entity; CommandRegistry.getInstance().activeEntity = entity;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,55 +18,53 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * 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. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import React from "react";
import { disposeOnUnmount, observer } from "mobx-react"; import { disposeOnUnmount, observer } from "mobx-react";
import React from "react";
import { Redirect, Route, Router, Switch } from "react-router"; import { Redirect, Route, Router, Switch } from "react-router";
import { history } from "../navigation"; import whatInput from "what-input";
import { Notifications } from "./notifications"; import { setFrameId } from "../../common/cluster-ipc";
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 { getHostedCluster } from "../../common/cluster-store"; import { getHostedCluster } from "../../common/cluster-store";
import logger from "../../main/logger"; import { getHostedClusterId } from "../../common/cluster-types";
import { webFrame } from "electron";
import { ClusterPageRegistry, getExtensionPageUrl } from "../../extensions/registries/page-registry";
import { ExtensionLoader } from "../../extensions/extension-loader";
import { appEventBus } from "../../common/event-bus"; import { appEventBus } from "../../common/event-bus";
import { requestMain } from "../../common/ipc"; import { requestMain } from "../../common/ipc";
import whatInput from "what-input"; import { ExtensionLoader } from "../../extensions/extension-loader";
import { clusterSetFrameIdHandler } from "../../common/cluster-ipc";
import { ClusterPageMenuRegistration, ClusterPageMenuRegistry } from "../../extensions/registries"; import { ClusterPageMenuRegistration, ClusterPageMenuRegistry } from "../../extensions/registries";
import { TabLayout, TabLayoutRoute } from "./layout/tab-layout"; import { ClusterPageRegistry, getExtensionPageUrl } from "../../extensions/registries/page-registry";
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 type { Cluster } from "../../main/cluster"; 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 { 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 @observer
export class ClusterFrame extends React.Component { export class ClusterFrame extends React.Component {
@ -74,12 +72,12 @@ export class ClusterFrame extends React.Component {
static cluster: Cluster; static cluster: Cluster;
static async init() { static async init() {
const frameId = webFrame.routingId; const frameId = Electron.webFrame.routingId;
const clusterId = getHostedClusterId(); const clusterId = getHostedClusterId();
logger.info(`[APP]: Init dashboard, clusterId=${clusterId}, frameId=${frameId}`); logger.info(`[APP]: Init dashboard, clusterId=${clusterId}, frameId=${frameId}`);
await Terminal.preloadFonts(); await Terminal.preloadFonts();
await requestMain(clusterSetFrameIdHandler, clusterId); await requestMain(setFrameId, clusterId);
this.cluster = getHostedCluster(); this.cluster = getHostedCluster();

View File

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

View File

@ -20,7 +20,9 @@
*/ */
import "./button.scss"; import "./button.scss";
import React, { ButtonHTMLAttributes } from "react"; import React, { ButtonHTMLAttributes } from "react";
import { cssNames } from "../../utils"; import { cssNames } from "../../utils";
import { TooltipDecoratorProps, withTooltip } from "../tooltip"; 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 * 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. * 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 "./cluster-status.scss";
import React from "react";
import { observer } from "mobx-react";
import { ipcRenderer } from "electron"; import { ipcRenderer } from "electron";
import { computed, observable } from "mobx"; import { computed, observable } from "mobx";
import { ipcRendererOn, requestMain } from "../../../common/ipc"; import { ipcRendererOn, requestMain } from "../../../common/ipc";
@ -34,7 +30,10 @@ import type { Cluster } from "../../../main/cluster";
import { ClusterStore } from "../../../common/cluster-store"; import { ClusterStore } from "../../../common/cluster-store";
import type { ClusterId } from "../../../common/cluster-types"; import type { ClusterId } from "../../../common/cluster-types";
import { CubeSpinner } from "../spinner"; 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 { interface Props {
className?: IClassName; className?: IClassName;
@ -68,7 +67,7 @@ export class ClusterStatus extends React.Component<Props> {
} }
activateCluster = async (force = false) => { activateCluster = async (force = false) => {
await requestMain(clusterActivateHandler, this.props.clusterId, force); await requestMain(activate, this.props.clusterId, force);
}; };
reconnect = async () => { reconnect = async () => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -51,6 +51,9 @@ export function createStorage<T>(key: string, defaultValue: T, observableOptions
// bind auto-saving // bind auto-saving
reaction(() => storage.toJSON(), saveFile, { delay: 250 }); 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 = {}) { async function saveFile(json = {}) {

View File

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

View File

@ -1644,20 +1644,13 @@
dependencies: dependencies:
"@types/react" "*" "@types/react" "*"
"@types/react@*": "@types/react@*", "@types/react@^17.0.0":
version "16.9.35" version "17.0.5"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.35.tgz#a0830d172e8aadd9bd41709ba2281a3124bbd368" resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.5.tgz#3d887570c4489011f75a3fc8f965bf87d09a1bea"
integrity sha512-q0n0SsWcGc8nDqH2GJfWQWUOmZSJhXV64CjVN5SvcNti3TdEaA3AH0D8DwNmMdzjMAC/78tB8nAZIlV8yTz+zQ== integrity sha512-bj4biDB9ZJmGAYTWSKJly6bMr4BLUiBrx9ujiJEoP9XIDY9CTaPGxE5QWN/1WjpPLzYF7/jRNnV2nNxNe970sw==
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==
dependencies: dependencies:
"@types/prop-types" "*" "@types/prop-types" "*"
"@types/scheduler" "*"
csstype "^3.0.2" csstype "^3.0.2"
"@types/readable-stream@^2.3.9": "@types/readable-stream@^2.3.9":
@ -1695,6 +1688,11 @@
resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d"
integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== 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": "@types/semver@^7.2.0":
version "7.2.0" version "7.2.0"
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.2.0.tgz#0d72066965e910531e1db4621c15d0ca36b8d83b" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.2.0.tgz#0d72066965e910531e1db4621c15d0ca36b8d83b"
@ -4355,7 +4353,7 @@ cssstyle@^2.2.0:
dependencies: dependencies:
cssom "~0.3.6" 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" version "2.6.10"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.10.tgz#e63af50e66d7c266edb6b32909cfd0aabe03928b" resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.10.tgz#e63af50e66d7c266edb6b32909cfd0aabe03928b"
integrity sha512-D34BqZU4cIlMCY93rZHbrq9pjTAQJ3U8S8rfBqjwHxkGPThWFjzZDQpgMJY0QViLxth6ZKYiwFBo14RdN44U/w== integrity sha512-D34BqZU4cIlMCY93rZHbrq9pjTAQJ3U8S8rfBqjwHxkGPThWFjzZDQpgMJY0QViLxth6ZKYiwFBo14RdN44U/w==