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:
parent
33422ce975
commit
111356521a
@ -18,11 +18,12 @@
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { Catalog, Interface, LensRendererExtension } from "@k8slens/extensions";
|
||||
|
||||
import React from "react";
|
||||
import { LensRendererExtension, Catalog } from "@k8slens/extensions";
|
||||
import { MetricsSettings } from "./src/metrics-settings";
|
||||
|
||||
|
||||
export default class ClusterMetricsFeatureExtension extends LensRendererExtension {
|
||||
entitySettings = [
|
||||
{
|
||||
@ -39,4 +40,67 @@ export default class ClusterMetricsFeatureExtension extends LensRendererExtensio
|
||||
}
|
||||
}
|
||||
];
|
||||
onActivate() {
|
||||
this.disposers.push(
|
||||
Catalog.CatalogCategoryRegistry.registerHandler(
|
||||
"entity.k8slens.dev/v1alpha1",
|
||||
"KubernetesCluster",
|
||||
"onContextMenuOpen",
|
||||
this.clusterContextMenuOpen,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
clusterContextMenuOpen = (cluster: Catalog.KubernetesCluster): Interface.ContextMenu[] => {
|
||||
if (!cluster.status.active) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const metricsFeature = new MetricsFeature();
|
||||
|
||||
return [
|
||||
{
|
||||
icon: "refresh",
|
||||
title: "Upgrade Lens Metrics stack",
|
||||
onClick: async () => {
|
||||
metricsFeature.upgrade(cluster);
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
// const metricsFeature = new MetricsFeature();
|
||||
|
||||
// await metricsFeature.updateStatus(cluster);
|
||||
|
||||
// if (metricsFeature.status.installed) {
|
||||
// if (metricsFeature.status.canUpgrade) {
|
||||
// ctx.menuItems.unshift({
|
||||
// icon: "refresh",
|
||||
// title: "Upgrade Lens Metrics stack",
|
||||
// onClick: async () => {
|
||||
// metricsFeature.upgrade(cluster);
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// ctx.menuItems.unshift({
|
||||
// icon: "toggle_off",
|
||||
// title: "Uninstall Lens Metrics stack",
|
||||
// onClick: async () => {
|
||||
// await metricsFeature.uninstall(cluster);
|
||||
|
||||
// Component.Notifications.info(`Lens Metrics has been removed from ${cluster.metadata.name}`, { timeout: 10_000 });
|
||||
// }
|
||||
// });
|
||||
// } else {
|
||||
// ctx.menuItems.unshift({
|
||||
// icon: "toggle_on",
|
||||
// title: "Install Lens Metrics stack",
|
||||
// onClick: async () => {
|
||||
// metricsFeature.install(cluster);
|
||||
|
||||
// Component.Notifications.info(`Lens Metrics is now installed to ${cluster.metadata.name}`, { timeout: 10_000 });
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
};
|
||||
}
|
||||
|
||||
@ -18,16 +18,17 @@
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import path from "path";
|
||||
import Config from "conf";
|
||||
import type { Options as ConfOptions } from "conf/dist/source/types";
|
||||
import { app, ipcMain, ipcRenderer, remote } from "electron";
|
||||
import isEqual from "lodash/isEqual";
|
||||
import { IReactionOptions, observable, reaction, runInAction, when } from "mobx";
|
||||
import { Singleton, getAppVersion } from "./utils";
|
||||
import path from "path";
|
||||
|
||||
import logger from "../main/logger";
|
||||
import { broadcastMessage, ipcMainOn, ipcRendererOn } from "./ipc";
|
||||
import isEqual from "lodash/isEqual";
|
||||
import { getAppVersion } from "./utils/app-version";
|
||||
import Singleton from "./utils/singleton";
|
||||
|
||||
export interface BaseStoreParams<T = any> extends ConfOptions<T> {
|
||||
autoLoad?: boolean;
|
||||
|
||||
@ -18,16 +18,15 @@
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
|
||||
import { CatalogEntity, CatalogEntityActionContext, CatalogEntityAddMenuContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus, CatalogCategory, CatalogCategorySpec } from "../catalog";
|
||||
import { clusterActivateHandler, clusterDisconnectHandler } from "../cluster-ipc";
|
||||
import { app } from "electron";
|
||||
import { CatalogEntity, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog";
|
||||
import type { ActionContext, ContextMenu, MenuContext } from "../catalog/catalog-entity";
|
||||
import * as clusterIpc from "../cluster-ipc";
|
||||
import { ClusterStore } from "../cluster-store";
|
||||
import { requestMain } from "../ipc";
|
||||
import { productName } from "../vars";
|
||||
import { addClusterURL } from "../routes";
|
||||
import { storedKubeConfigFolder } from "../utils";
|
||||
import { app } from "electron";
|
||||
import { productName } from "../vars";
|
||||
|
||||
|
||||
export type KubernetesClusterPrometheusMetrics = {
|
||||
address?: {
|
||||
@ -49,7 +48,7 @@ export type KubernetesClusterSpec = {
|
||||
};
|
||||
|
||||
export interface KubernetesClusterStatus extends CatalogEntityStatus {
|
||||
phase: "connected" | "disconnected";
|
||||
phase?: "connected" | "disconnected";
|
||||
}
|
||||
|
||||
export class KubernetesCluster extends CatalogEntity<CatalogEntityMetadata, KubernetesClusterStatus, KubernetesClusterSpec> {
|
||||
@ -67,7 +66,7 @@ export class KubernetesCluster extends CatalogEntity<CatalogEntityMetadata, Kube
|
||||
return;
|
||||
}
|
||||
|
||||
await requestMain(clusterActivateHandler, this.metadata.uid, false);
|
||||
await requestMain(clusterIpc.activate, this.metadata.uid, false);
|
||||
|
||||
return;
|
||||
}
|
||||
@ -83,34 +82,39 @@ export class KubernetesCluster extends CatalogEntity<CatalogEntityMetadata, Kube
|
||||
return;
|
||||
}
|
||||
|
||||
await requestMain(clusterDisconnectHandler, this.metadata.uid, false);
|
||||
await requestMain(clusterIpc.disconnect, this.metadata.uid, false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
async onRun(context: CatalogEntityActionContext) {
|
||||
onRun = (context: ActionContext) => {
|
||||
context.navigate(`/cluster/${this.metadata.uid}`);
|
||||
}
|
||||
};
|
||||
|
||||
onDetailsOpen(): void {
|
||||
//
|
||||
}
|
||||
onContextMenuOpen = (context: MenuContext) => {
|
||||
const res: ContextMenu[] = [];
|
||||
|
||||
onSettingsOpen(): void {
|
||||
//
|
||||
}
|
||||
if (this.status.phase == "connected") {
|
||||
res.push({
|
||||
icon: "link_off",
|
||||
title: "Disconnect",
|
||||
onClick: async () => {
|
||||
ClusterStore.getInstance().deactivate(this.metadata.uid);
|
||||
requestMain(clusterIpc.disconnect, this.metadata.uid);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async onContextMenuOpen(context: CatalogEntityContextMenuContext) {
|
||||
context.menuItems = [
|
||||
{
|
||||
title: "Settings",
|
||||
onlyVisibleForSource: "local",
|
||||
onClick: async () => context.navigate(`/entity/${this.metadata.uid}/settings`)
|
||||
},
|
||||
];
|
||||
res.push({
|
||||
icon: "settings",
|
||||
title: "Settings",
|
||||
onlyVisibleForSource: "local",
|
||||
onClick: async () => context.navigate(`/entity/${this.metadata.uid}/settings`)
|
||||
});
|
||||
|
||||
if (this.metadata.labels["file"]?.startsWith(storedKubeConfigFolder())) {
|
||||
context.menuItems.push({
|
||||
res.push({
|
||||
icon: "delete",
|
||||
title: "Delete",
|
||||
onlyVisibleForSource: "local",
|
||||
onClick: async () => ClusterStore.getInstance().removeById(this.metadata.uid),
|
||||
@ -120,62 +124,6 @@ export class KubernetesCluster extends CatalogEntity<CatalogEntityMetadata, Kube
|
||||
});
|
||||
}
|
||||
|
||||
if (this.status.phase == "connected") {
|
||||
context.menuItems.push({
|
||||
title: "Disconnect",
|
||||
onClick: async () => {
|
||||
ClusterStore.getInstance().deactivate(this.metadata.uid);
|
||||
requestMain(clusterDisconnectHandler, this.metadata.uid);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
context.menuItems.push({
|
||||
title: "Connect",
|
||||
onClick: async () => {
|
||||
context.navigate(`/cluster/${this.metadata.uid}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const category = catalogCategoryRegistry.getCategoryForEntity<KubernetesClusterCategory>(this);
|
||||
|
||||
if (category) category.emit("contextMenuOpen", this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class KubernetesClusterCategory extends CatalogCategory {
|
||||
public readonly apiVersion = "catalog.k8slens.dev/v1alpha1";
|
||||
public readonly kind = "CatalogCategory";
|
||||
public metadata = {
|
||||
name: "Kubernetes Clusters",
|
||||
icon: require(`!!raw-loader!./icons/kubernetes.svg`).default // eslint-disable-line
|
||||
return res;
|
||||
};
|
||||
public spec: CatalogCategorySpec = {
|
||||
group: "entity.k8slens.dev",
|
||||
versions: [
|
||||
{
|
||||
name: "v1alpha1",
|
||||
entityClass: KubernetesCluster
|
||||
}
|
||||
],
|
||||
names: {
|
||||
kind: "KubernetesCluster"
|
||||
}
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.on("onCatalogAddMenu", (ctx: CatalogEntityAddMenuContext) => {
|
||||
ctx.menuItems.push({
|
||||
icon: "text_snippet",
|
||||
title: "Add from kubeconfig",
|
||||
onClick: () => {
|
||||
ctx.navigate(addClusterURL());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
catalogCategoryRegistry.add(new KubernetesClusterCategory());
|
||||
|
||||
@ -18,9 +18,7 @@
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { CatalogCategory, CatalogEntity, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog";
|
||||
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
|
||||
import { CatalogEntity, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog";
|
||||
|
||||
export interface WebLinkStatus extends CatalogEntityStatus {
|
||||
phase: "valid" | "invalid";
|
||||
@ -34,42 +32,7 @@ export class WebLink extends CatalogEntity<CatalogEntityMetadata, WebLinkStatus,
|
||||
public readonly apiVersion = "entity.k8slens.dev/v1alpha1";
|
||||
public readonly kind = "WebLink";
|
||||
|
||||
async onRun() {
|
||||
onRun = () => {
|
||||
window.open(this.spec.url, "_blank");
|
||||
}
|
||||
|
||||
public onSettingsOpen(): void {
|
||||
return;
|
||||
}
|
||||
|
||||
public onDetailsOpen(): void {
|
||||
return;
|
||||
}
|
||||
|
||||
public onContextMenuOpen(): void {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export class WebLinkCategory extends CatalogCategory {
|
||||
public readonly apiVersion = "catalog.k8slens.dev/v1alpha1";
|
||||
public readonly kind = "CatalogCategory";
|
||||
public metadata = {
|
||||
name: "Web Links",
|
||||
icon: "link"
|
||||
};
|
||||
public spec = {
|
||||
group: "entity.k8slens.dev",
|
||||
versions: [
|
||||
{
|
||||
name: "v1alpha1",
|
||||
entityClass: WebLink
|
||||
}
|
||||
],
|
||||
names: {
|
||||
kind: "WebLink"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
catalogCategoryRegistry.add(new WebLinkCategory());
|
||||
|
||||
@ -18,60 +18,234 @@
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { action, computed, observable, when } from "mobx";
|
||||
|
||||
import { action, computed, observable, toJS } from "mobx";
|
||||
import { CatalogCategory, CatalogEntityData, CatalogEntityKindData } from "./catalog-entity";
|
||||
import { navigate } from "../../renderer/navigation";
|
||||
import { Rest } from "../ipc";
|
||||
import { Disposer, disposer, ExtendedMap, Singleton, StrictMap } from "../utils";
|
||||
import {
|
||||
AddMenuOpenHandler,
|
||||
CatalogCategorySpec,
|
||||
CatalogCategoryVersion,
|
||||
CatalogEntity,
|
||||
CatalogEntityConstructor,
|
||||
CatalogEntityData,
|
||||
CatalogEntityKindData,
|
||||
CategoryHandler,
|
||||
ContextMenuOpenHandler,
|
||||
MatchingCatalogEntityData,
|
||||
parseApiVersion,
|
||||
SettingsMenuOpenHandler,
|
||||
} from "./catalog-entity";
|
||||
|
||||
export class CatalogCategoryRegistry {
|
||||
@observable protected categories: CatalogCategory[] = [];
|
||||
|
||||
@action add(category: CatalogCategory) {
|
||||
this.categories.push(category);
|
||||
}
|
||||
type KeysMatching<T, V> = { [K in keyof T]-?: T[K] extends V ? K : never }[keyof T];
|
||||
type KeysNotMatching<T, V> = { [K in keyof T]-?: T[K] extends V ? never : K }[keyof T];
|
||||
|
||||
@action remove(category: CatalogCategory) {
|
||||
this.categories = this.categories.filter((cat) => cat.apiVersion !== category.apiVersion && cat.kind !== category.kind);
|
||||
}
|
||||
export type CategoryHandlers = {
|
||||
[HandlerName in KeysMatching<CatalogCategory, Set<any>>]?: CatalogCategory[HandlerName] extends Set<infer Handler> ? Handler : never;
|
||||
};
|
||||
export type CategoryHandlerNames = keyof CategoryHandlers;
|
||||
export type CatalogHandler<Name extends CategoryHandlerNames> = CategoryHandlers[Name];
|
||||
|
||||
@computed get items() {
|
||||
return toJS(this.categories);
|
||||
}
|
||||
export type EntityContextHandlers = keyof EntityContextGetters;
|
||||
export type GlobalContextHandlers = keyof GlobalContextGetters;
|
||||
|
||||
getForGroupKind<T extends CatalogCategory>(group: string, kind: string) {
|
||||
return this.categories.find((c) => c.spec.group === group && c.spec.names.kind === kind) as T;
|
||||
}
|
||||
type EntityContextGetters = {
|
||||
[HandlerName in KeysMatching<CategoryHandlers, CategoryHandler<(...args: any) => any>>]: () => Rest<Parameters<CategoryHandlers[HandlerName]>>;
|
||||
};
|
||||
|
||||
getEntityForData(data: CatalogEntityData & CatalogEntityKindData) {
|
||||
const category = this.getCategoryForEntity(data);
|
||||
type GlobalContextGetters = {
|
||||
[HandlerName in KeysNotMatching<CategoryHandlers, CategoryHandler<(...args: any) => any>>]: () => Parameters<CategoryHandlers[HandlerName]>;
|
||||
};
|
||||
|
||||
if (!category) {
|
||||
return null;
|
||||
const EntityContexts: EntityContextGetters = {
|
||||
onContextMenuOpen: () => [{ navigate }],
|
||||
onSettingsOpen: () => [{ navigate }],
|
||||
};
|
||||
const GlobalContexts: GlobalContextGetters = {
|
||||
onAddMenuOpen: () => [{ navigate }],
|
||||
};
|
||||
|
||||
/**
|
||||
* Note: this type shouldn't be exported or leaked out of this file.
|
||||
* The registry should do everything for any consumer of this type.
|
||||
*/
|
||||
class CatalogCategory implements CatalogCategorySpec {
|
||||
onContextMenuOpen = new Set<CategoryHandler<ContextMenuOpenHandler>>();
|
||||
onSettingsOpen = new Set<CategoryHandler<SettingsMenuOpenHandler>>();
|
||||
onAddMenuOpen = new Set<AddMenuOpenHandler>();
|
||||
|
||||
public readonly id: string;
|
||||
|
||||
public readonly apiVersion: string;
|
||||
public readonly kind: string;
|
||||
public readonly metadata: {
|
||||
name: string;
|
||||
icon: string;
|
||||
};
|
||||
|
||||
public readonly spec: {
|
||||
group: string;
|
||||
versions: CatalogCategoryVersion<CatalogEntity>[];
|
||||
names: {
|
||||
kind: string;
|
||||
};
|
||||
};
|
||||
|
||||
constructor(specAndHandlers: CatalogCategorySpec & CategoryHandlers) {
|
||||
const { apiVersion, kind, metadata, spec, ...handlers } = specAndHandlers;
|
||||
|
||||
this.spec = spec;
|
||||
this.apiVersion = apiVersion;
|
||||
this.kind = kind;
|
||||
this.metadata = metadata;
|
||||
this.id = `${spec.group}/${spec.names.kind}`;
|
||||
|
||||
for (const name of Object.keys(handlers)) {
|
||||
const handlerName = name as CategoryHandlerNames;
|
||||
|
||||
if (typeof handlers[handlerName] === "function") {
|
||||
this[handlerName].add(handlers[handlerName] as any);
|
||||
}
|
||||
}
|
||||
|
||||
const splitApiVersion = data.apiVersion.split("/");
|
||||
const version = splitApiVersion[1];
|
||||
|
||||
const specVersion = category.spec.versions.find((v) => v.name === version);
|
||||
|
||||
if (!specVersion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new specVersion.entityClass(data);
|
||||
}
|
||||
|
||||
getCategoryForEntity<T extends CatalogCategory>(data: CatalogEntityData & CatalogEntityKindData) {
|
||||
const splitApiVersion = data.apiVersion.split("/");
|
||||
const group = splitApiVersion[0];
|
||||
|
||||
const category = this.categories.find((category) => {
|
||||
return category.spec.group === group && category.spec.names.kind === data.kind;
|
||||
});
|
||||
|
||||
if (!category) return null;
|
||||
|
||||
return category as T;
|
||||
}
|
||||
}
|
||||
|
||||
export const catalogCategoryRegistry = new CatalogCategoryRegistry();
|
||||
export class CatalogCategoryRegistry extends Singleton {
|
||||
/**
|
||||
* The three levels of keys are: (for category ApiVersions)
|
||||
* 1. `GROUP`
|
||||
* 2. `VERSION`
|
||||
*/
|
||||
protected categories = observable.set<CatalogCategory>();
|
||||
|
||||
/**
|
||||
* The three levels of keys are: (by entity ApiVersions)
|
||||
* 1. `GROUP`
|
||||
* 2. `VERSION`
|
||||
* 3. `KIND`
|
||||
*/
|
||||
@computed protected get entityToCategoryTable(): Map<string, Map<string, Map<string, [CatalogCategory, CatalogEntityConstructor<CatalogEntity>]>>> {
|
||||
const res = ExtendedMap.newExtendedStrict<string, string, string, [CatalogCategory, CatalogEntityConstructor<CatalogEntity>]>();
|
||||
|
||||
for (const category of this.categories.values()) {
|
||||
const grouping = res.getOrDefault(category.spec.group);
|
||||
|
||||
for (const { version, entityClass } of category.spec.versions) {
|
||||
grouping
|
||||
.getOrDefault(version)
|
||||
.strictSet(category.spec.names.kind, [category, entityClass]);
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@computed protected get categoryIdLookup(): Map<string, CatalogCategory> {
|
||||
const res = new StrictMap<string, CatalogCategory>();
|
||||
|
||||
for (const category of this.categories.values()) {
|
||||
res.strictSet(category.id, category);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers (and potentially overrides a previous category)
|
||||
* @param specAndHandlers The Category spec and initial handlers to register
|
||||
* @returns the ability to remove this category
|
||||
*/
|
||||
@action add(specAndHandlers: CatalogCategorySpec & CategoryHandlers): Disposer {
|
||||
parseApiVersion(specAndHandlers.apiVersion); // make sure this is valid
|
||||
const category = new CatalogCategory(specAndHandlers);
|
||||
|
||||
this.categories.add(category);
|
||||
|
||||
return () => void this.categories.delete(category);
|
||||
}
|
||||
|
||||
@computed get items(): CatalogCategorySpec[] {
|
||||
return Array.from(this.categoryIdLookup.values());
|
||||
}
|
||||
|
||||
getById(id: string): CatalogCategorySpec | undefined {
|
||||
return this.categoryIdLookup.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the `CatalogCategory` once it has been registered
|
||||
* @param apiVersion the ApiVersion string of the category
|
||||
* @param kind the kind of entity that is desired
|
||||
*/
|
||||
registerHandler(apiVersion: string, kind: string, handlerName: CategoryHandlerNames, handler: CatalogHandler<typeof handlerName>): Disposer {
|
||||
const { group, version } = parseApiVersion(apiVersion, false);
|
||||
|
||||
if (version) {
|
||||
// only one version to do
|
||||
return disposer(
|
||||
when(
|
||||
() => this.entityToCategoryTable.get(group)?.get(version)?.has(kind),
|
||||
() => {
|
||||
const [category] = this.entityToCategoryTable.get(group).get(version).get(kind);
|
||||
|
||||
category[handlerName].add(handler as any);
|
||||
},
|
||||
),
|
||||
() => void this.entityToCategoryTable.get(group)?.get(version)?.delete(kind),
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error("Not providing a version for groups is not supported at this time");
|
||||
// This would requiring observing future additions to the second level of the map
|
||||
// and waiting for them to add the kind
|
||||
// all wrapped up in disposers
|
||||
}
|
||||
|
||||
runEntityHandlersFor(entity: CatalogEntity, handlerName: "onContextMenuOpen"): ReturnType<CategoryHandlers[typeof handlerName]>;
|
||||
runEntityHandlersFor(entity: CatalogEntity, handlerName: "onSettingsOpen"): ReturnType<CategoryHandlers[typeof handlerName]>;
|
||||
runEntityHandlersFor(entity: CatalogEntity, handlerName: EntityContextHandlers): ReturnType<CategoryHandlers[typeof handlerName]> {
|
||||
const category = this.getCategoryForEntity(entity) as CatalogCategory; // safe and what it actually is
|
||||
const res = (entity[handlerName] as any)?.(...EntityContexts[handlerName]()) ?? [];
|
||||
|
||||
console.log(category, res);
|
||||
|
||||
for (const handler of category[handlerName].values()) {
|
||||
res.push((handler as any)(entity, ...EntityContexts[handlerName]()));
|
||||
}
|
||||
|
||||
return res.flat();
|
||||
}
|
||||
|
||||
runGlobalHandlersFor({ spec }: CatalogCategorySpec, handlerName: "onAddMenuOpen"): ReturnType<CategoryHandlers[typeof handlerName]>;
|
||||
runGlobalHandlersFor({ spec }: CatalogCategorySpec, handlerName: GlobalContextHandlers): ReturnType<CategoryHandlers[typeof handlerName]> {
|
||||
const category = this.categoryIdLookup.get(`${spec.group}/${spec.names.kind}`);
|
||||
const res = [];
|
||||
|
||||
for (const handler of category[handlerName].values()) {
|
||||
res.push((handler as any)(...GlobalContexts[handlerName]()));
|
||||
}
|
||||
|
||||
return res.flat();
|
||||
}
|
||||
|
||||
getEntityForData<Entity extends CatalogEntity>(data: MatchingCatalogEntityData<Entity> & CatalogEntityKindData): Entity {
|
||||
const { group, version } = parseApiVersion(data.apiVersion);
|
||||
|
||||
const [, entityClass] = this.entityToCategoryTable.get(group)?.get(version)?.get(data.kind);
|
||||
const res = new entityClass(data);
|
||||
|
||||
if (res.apiVersion !== data.apiVersion || res.kind !== data.kind) {
|
||||
throw new TypeError(`CatalogEntity class declared for ${group}/${version}:${data.kind} produced ${res.apiVersion}:${res.kind}`);
|
||||
}
|
||||
|
||||
return res as Entity;
|
||||
}
|
||||
|
||||
getCategoryForEntity(data: CatalogEntityData & CatalogEntityKindData): CatalogCategorySpec | undefined {
|
||||
const { group, version } = parseApiVersion(data.apiVersion);
|
||||
|
||||
return this.entityToCategoryTable.get(group)?.get(version)?.get(data.kind)?.[0];
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,10 +20,10 @@
|
||||
*/
|
||||
|
||||
import { action, computed, observable, IComputedValue, IObservableArray } from "mobx";
|
||||
import type { CatalogEntity } from "./catalog-entity";
|
||||
import { iter } from "../utils";
|
||||
import type { CatalogEntity, CatalogEntityData, CatalogEntityKindData } from "./catalog-entity";
|
||||
import { cloneJsonObject, iter, Singleton } from "../utils";
|
||||
|
||||
export class CatalogEntityRegistry {
|
||||
export class CatalogEntityRegistry extends Singleton {
|
||||
protected sources = observable.map<string, IComputedValue<CatalogEntity[]>>([], { deep: true });
|
||||
|
||||
@action addObservableSource(id: string, source: IObservableArray<CatalogEntity>) {
|
||||
@ -38,8 +38,25 @@ export class CatalogEntityRegistry {
|
||||
this.sources.delete(id);
|
||||
}
|
||||
|
||||
@computed get items(): CatalogEntity[] {
|
||||
return Array.from(iter.flatMap(this.sources.values(), source => source.get()));
|
||||
@computed get items(): (CatalogEntityData & CatalogEntityKindData)[] {
|
||||
// This is done to filter out non-serializable items, namely functions
|
||||
return Array.from(
|
||||
iter.flatMap(
|
||||
this.sources.values(),
|
||||
source => (
|
||||
iter.map(
|
||||
source.get(),
|
||||
({ apiVersion, kind, metadata, spec, status }) => ({
|
||||
apiVersion,
|
||||
kind,
|
||||
metadata: cloneJsonObject(metadata),
|
||||
spec: cloneJsonObject(spec),
|
||||
status: cloneJsonObject(status),
|
||||
}),
|
||||
)
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] {
|
||||
@ -48,5 +65,3 @@ export class CatalogEntityRegistry {
|
||||
return items as T[];
|
||||
}
|
||||
}
|
||||
|
||||
export const catalogEntityRegistry = new CatalogEntityRegistry();
|
||||
|
||||
@ -18,47 +18,99 @@
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from "events";
|
||||
import { observable } from "mobx";
|
||||
import URLParse from "url-parse";
|
||||
|
||||
export interface ParsedApiVersion {
|
||||
group: string;
|
||||
version?: string;
|
||||
}
|
||||
|
||||
const versionSchema = /^\/(?<version>v[1-9][0-9]*((alpha|beta)[1-9][0-9]*)?)$/;
|
||||
|
||||
/**
|
||||
* Attempts to parse an ApiVersion string or a group string
|
||||
* @param apiVersionOrGroup A string that should be either of the form `<group>/<version>` or `<group>` for any version
|
||||
* @param strict if true then will throw an error if `<version>` is not provided
|
||||
* @default strict = true
|
||||
* @returns A parsed data
|
||||
*/
|
||||
export function parseApiVersion(apiVersionOrGroup: string, strict: false): ParsedApiVersion;
|
||||
export function parseApiVersion(apiVersionOrGroup: string, strict?: true): Required<ParsedApiVersion>;
|
||||
|
||||
export function parseApiVersion(apiVersionOrGroup: string, strict?: boolean): ParsedApiVersion {
|
||||
strict ??= true;
|
||||
|
||||
const parsed = new URLParse(`lens://${apiVersionOrGroup}`);
|
||||
|
||||
if (
|
||||
parsed.protocol !== "lens:"
|
||||
|| parsed.hash
|
||||
|| parsed.query
|
||||
|| parsed.auth
|
||||
|| parsed.port
|
||||
|| parsed.password
|
||||
|| parsed.username
|
||||
) {
|
||||
throw new TypeError(`invalid apiVersion string: ${apiVersionOrGroup}`);
|
||||
}
|
||||
|
||||
if (!parsed.pathname) {
|
||||
throw new TypeError(`missing version on apiVersion: ${apiVersionOrGroup}`);
|
||||
}
|
||||
|
||||
const match = parsed.pathname.match(versionSchema);
|
||||
|
||||
if (versionSchema && !match && strict) {
|
||||
throw new TypeError(`invalid version on apiVersion: ${apiVersionOrGroup}`);
|
||||
}
|
||||
|
||||
return {
|
||||
group: parsed.hostname,
|
||||
version: match?.groups.version,
|
||||
};
|
||||
}
|
||||
|
||||
type ExtractEntityMetadataType<Entity> = Entity extends CatalogEntity<infer Metadata> ? Metadata : never;
|
||||
type ExtractEntityStatusType<Entity> = Entity extends CatalogEntity<any, infer Status> ? Status : never;
|
||||
type ExtractEntitySpecType<Entity> = Entity extends CatalogEntity<any, any, infer Spec> ? Spec : never;
|
||||
|
||||
export type CatalogEntityConstructor<Entity extends CatalogEntity> = (
|
||||
(new (data: CatalogEntityData<
|
||||
ExtractEntityMetadataType<Entity>,
|
||||
ExtractEntityStatusType<Entity>,
|
||||
ExtractEntitySpecType<Entity>
|
||||
>) => Entity)
|
||||
);
|
||||
export type MatchingCatalogEntityData<Entity extends CatalogEntity> = CatalogEntityData<
|
||||
ExtractEntityMetadataType<Entity>,
|
||||
ExtractEntityStatusType<Entity>,
|
||||
ExtractEntitySpecType<Entity>
|
||||
>;
|
||||
|
||||
export type CatalogEntityConstructor<Entity extends CatalogEntity> = new (data: MatchingCatalogEntityData<Entity>) => Entity;
|
||||
|
||||
export interface CatalogCategoryVersion<Entity extends CatalogEntity> {
|
||||
name: string;
|
||||
version: string;
|
||||
entityClass: CatalogEntityConstructor<Entity>;
|
||||
}
|
||||
|
||||
export interface CatalogCategorySpec {
|
||||
group: string;
|
||||
versions: CatalogCategoryVersion<CatalogEntity>[];
|
||||
names: {
|
||||
kind: string;
|
||||
};
|
||||
}
|
||||
|
||||
export abstract class CatalogCategory extends EventEmitter {
|
||||
abstract readonly apiVersion: string;
|
||||
abstract readonly kind: string;
|
||||
abstract metadata: {
|
||||
readonly apiVersion: string;
|
||||
readonly kind: string;
|
||||
readonly metadata: {
|
||||
name: string;
|
||||
icon: string;
|
||||
};
|
||||
abstract spec: CatalogCategorySpec;
|
||||
|
||||
public getId(): string {
|
||||
return `${this.spec.group}/${this.spec.names.kind}`;
|
||||
}
|
||||
/**
|
||||
* It will be a runtime error if any of the instances created through the
|
||||
* versions don't match the provided `group` and `names.kind` provided here.
|
||||
*/
|
||||
readonly spec: {
|
||||
group: string;
|
||||
versions: CatalogCategoryVersion<CatalogEntity>[];
|
||||
names: {
|
||||
kind: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function getCatalogCategoryId(category: CatalogCategorySpec): string {
|
||||
return `${category.spec.group}/${category.spec.names.kind}`;
|
||||
}
|
||||
|
||||
export interface CatalogEntityMetadata {
|
||||
@ -71,18 +123,21 @@ export interface CatalogEntityMetadata {
|
||||
}
|
||||
|
||||
export interface CatalogEntityStatus {
|
||||
phase: string;
|
||||
phase?: string;
|
||||
reason?: string;
|
||||
message?: string;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
export interface CatalogEntityActionContext {
|
||||
export interface ActionContext {
|
||||
navigate: (url: string) => void;
|
||||
setCommandPaletteContext: (context?: CatalogEntity) => void;
|
||||
}
|
||||
|
||||
export interface CatalogEntityContextMenu {
|
||||
export type ActionHandler = (ctx: ActionContext) => void;
|
||||
|
||||
export interface ContextMenu {
|
||||
icon: string;
|
||||
title: string;
|
||||
onlyVisibleForSource?: string; // show only if empty or if matches with entity source
|
||||
onClick: () => void | Promise<void>;
|
||||
@ -91,11 +146,19 @@ export interface CatalogEntityContextMenu {
|
||||
}
|
||||
}
|
||||
|
||||
export interface CatalogEntityAddMenu extends CatalogEntityContextMenu {
|
||||
icon: string;
|
||||
export interface MenuContext {
|
||||
navigate: (url: string) => void;
|
||||
}
|
||||
|
||||
export interface CatalogEntitySettingsMenu {
|
||||
export type ContextMenuOpenHandler = (ctx: MenuContext) => ContextMenu[];
|
||||
export type AddMenuOpenHandler = (ctx: MenuContext) => ContextMenu[];
|
||||
|
||||
export type CategoryHandler<EntityHandler extends (...args: any[]) => any> = (entity: CatalogEntity, ...args: Parameters<EntityHandler>) => ReturnType<EntityHandler>;
|
||||
|
||||
export interface SettingsContext {
|
||||
}
|
||||
|
||||
export interface SettingsMenu {
|
||||
group?: string;
|
||||
title: string;
|
||||
components: {
|
||||
@ -103,19 +166,7 @@ export interface CatalogEntitySettingsMenu {
|
||||
};
|
||||
}
|
||||
|
||||
export interface CatalogEntityContextMenuContext {
|
||||
navigate: (url: string) => void;
|
||||
menuItems: CatalogEntityContextMenu[];
|
||||
}
|
||||
|
||||
export interface CatalogEntitySettingsContext {
|
||||
menuItems: CatalogEntityContextMenu[];
|
||||
}
|
||||
|
||||
export interface CatalogEntityAddMenuContext {
|
||||
navigate: (url: string) => void;
|
||||
menuItems: CatalogEntityAddMenu[];
|
||||
}
|
||||
export type SettingsMenuOpenHandler = (ctx: SettingsContext) => SettingsMenu[];
|
||||
|
||||
export type CatalogEntitySpec = Record<string, any>;
|
||||
|
||||
@ -160,8 +211,7 @@ export abstract class CatalogEntity<
|
||||
return this.metadata.name;
|
||||
}
|
||||
|
||||
public abstract onRun?(context: CatalogEntityActionContext): void | Promise<void>;
|
||||
public abstract onDetailsOpen(context: CatalogEntityActionContext): void | Promise<void>;
|
||||
public abstract onContextMenuOpen(context: CatalogEntityContextMenuContext): void | Promise<void>;
|
||||
public abstract onSettingsOpen(context: CatalogEntitySettingsContext): void | Promise<void>;
|
||||
public onRun?: ActionHandler;
|
||||
public onContextMenuOpen?: ContextMenuOpenHandler;
|
||||
public onSettingsOpen?: SettingsMenuOpenHandler;
|
||||
}
|
||||
|
||||
@ -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>();
|
||||
@ -19,9 +19,8 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
export const clusterActivateHandler = "cluster:activate";
|
||||
export const clusterSetFrameIdHandler = "cluster:set-frame-id";
|
||||
export const clusterRefreshHandler = "cluster:refresh";
|
||||
export const clusterDisconnectHandler = "cluster:disconnect";
|
||||
export const clusterKubectlApplyAllHandler = "cluster:kubectl-apply-all";
|
||||
export const clusterKubectlDeleteAllHandler = "cluster:kubectl-delete-all";
|
||||
export const activate = "cluster:activate";
|
||||
export const setFrameId = "cluster:set-frame-id";
|
||||
export const refresh = "cluster:refresh";
|
||||
export const disconnect = "cluster:disconnect";
|
||||
export const kubectlApplyAll = "cluster:kubectl-apply-all";
|
||||
|
||||
@ -38,7 +38,6 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
private static StateChannel = "cluster:state";
|
||||
|
||||
@observable activeCluster: ClusterId;
|
||||
@observable removedClusters = observable.map<ClusterId, Cluster>();
|
||||
@observable clusters = observable.map<ClusterId, Cluster>();
|
||||
|
||||
private static stateRequestChannel = "cluster:states";
|
||||
@ -229,7 +228,6 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
protected fromStore({ activeCluster, clusters = [] }: ClusterStoreModel = {}) {
|
||||
const currentClusters = this.clusters.toJS();
|
||||
const newClusters = new Map<ClusterId, Cluster>();
|
||||
const removedClusters = new Map<ClusterId, Cluster>();
|
||||
|
||||
// update new clusters
|
||||
for (const clusterModel of clusters) {
|
||||
@ -247,16 +245,8 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
}
|
||||
}
|
||||
|
||||
// update removed clusters
|
||||
currentClusters.forEach(cluster => {
|
||||
if (!newClusters.has(cluster.id)) {
|
||||
removedClusters.set(cluster.id, cluster);
|
||||
}
|
||||
});
|
||||
|
||||
this.setActive(activeCluster);
|
||||
this.clusters.replace(newClusters);
|
||||
this.removedClusters.replace(removedClusters);
|
||||
}
|
||||
|
||||
toJSON(): ClusterStoreModel {
|
||||
|
||||
52
src/common/default-categories.ts
Normal file
52
src/common/default-categories.ts
Normal 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",
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
@ -22,11 +22,10 @@
|
||||
// Inter-process communications (main <-> renderer)
|
||||
// https://www.electronjs.org/docs/api/ipc-main
|
||||
// https://www.electronjs.org/docs/api/ipc-renderer
|
||||
import Electron, { ipcMain, ipcRenderer, remote } from "electron";
|
||||
|
||||
import { ipcMain, ipcRenderer, webContents, remote } from "electron";
|
||||
import { toJS } from "mobx";
|
||||
import { ClusterFrameInfo, ClusterManager } from "../../main/cluster-manager";
|
||||
import logger from "../../main/logger";
|
||||
import { ClusterFrameInfo, clusterFrameMap } from "../cluster-frames";
|
||||
import type { Disposer } from "../utils";
|
||||
|
||||
const subFramesChannel = "ipc:get-sub-frames";
|
||||
@ -36,11 +35,11 @@ export async function requestMain(channel: string, ...args: any[]) {
|
||||
}
|
||||
|
||||
function getSubFrames(): ClusterFrameInfo[] {
|
||||
return toJS(Array.from(clusterFrameMap.values()), { recurseEverything: true });
|
||||
return ClusterManager.getInstance().getAllFrameInfo();
|
||||
}
|
||||
|
||||
export function broadcastMessage(channel: string, ...args: any[]) {
|
||||
const views = (webContents || remote?.webContents)?.getAllWebContents();
|
||||
export async function broadcastMessage(channel: string, ...args: any[]) {
|
||||
const views = (Electron.webContents || remote?.webContents)?.getAllWebContents();
|
||||
|
||||
if (!views) return;
|
||||
|
||||
|
||||
@ -21,7 +21,38 @@
|
||||
|
||||
import { action, IEnhancer, IObservableMapInitialValues, ObservableMap } from "mobx";
|
||||
|
||||
export class ExtendedMap<K, V> extends Map<K, V> {
|
||||
export class DuplicateKeyError extends Error {
|
||||
constructor(public key: any) {
|
||||
super("Duplicate key in map");
|
||||
}
|
||||
}
|
||||
|
||||
export class StrictMap<K, V> extends Map<K, V> {
|
||||
/**
|
||||
* @throws if `key` already in map
|
||||
*/
|
||||
strictSet(key: K, val: V): this {
|
||||
if (this.has(key)) {
|
||||
throw new DuplicateKeyError(key);
|
||||
}
|
||||
|
||||
return this.set(key, val);
|
||||
}
|
||||
}
|
||||
|
||||
export class ExtendedMap<K, V> extends StrictMap<K, V> {
|
||||
static new<K, MK, MV>(): ExtendedMap<K, Map<MK, MV>> {
|
||||
return new ExtendedMap<K, Map<MK, MV>>(() => new Map<MK, MV>());
|
||||
}
|
||||
|
||||
static newExtended<K, MK, MV>(getDefault: () => MV): ExtendedMap<K, ExtendedMap<MK, MV>> {
|
||||
return new ExtendedMap<K, ExtendedMap<MK, MV>>(() => new ExtendedMap<MK, MV>(getDefault));
|
||||
}
|
||||
|
||||
static newExtendedStrict<K, MK, MMK, MMV>(): ExtendedMap<K, ExtendedMap<MK, StrictMap<MMK, MMV>>> {
|
||||
return new ExtendedMap<K, ExtendedMap<MK, StrictMap<MMK, MMV>>>(() => new ExtendedMap<MK, StrictMap<MMK, MMV>>(() => new StrictMap<MMK, MMV>()));
|
||||
}
|
||||
|
||||
constructor(protected getDefault: () => V, entries?: readonly (readonly [K, V])[] | null) {
|
||||
super(entries);
|
||||
}
|
||||
|
||||
@ -18,17 +18,63 @@
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import {
|
||||
CatalogCategorySpec,
|
||||
CatalogEntity,
|
||||
CatalogEntityData,
|
||||
CatalogEntityKindData,
|
||||
CatalogEntityRegistry as InternalCatalogEntityRegistry,
|
||||
MatchingCatalogEntityData,
|
||||
} from "../../common/catalog";
|
||||
import {
|
||||
CatalogCategoryRegistry as InternalCatalogCategoryRegistry,
|
||||
CatalogHandler,
|
||||
CategoryHandlerNames,
|
||||
CategoryHandlers,
|
||||
EntityContextHandlers,
|
||||
GlobalContextHandlers,
|
||||
} from "../../common/catalog/catalog-category-registry";
|
||||
import { Disposer } from "../../common/utils";
|
||||
|
||||
|
||||
import { CatalogEntity, catalogEntityRegistry as registry } from "../../common/catalog";
|
||||
|
||||
export { catalogCategoryRegistry as catalogCategories } from "../../common/catalog/catalog-category-registry";
|
||||
export * from "../../common/catalog-entities";
|
||||
|
||||
export class CatalogEntityRegistry {
|
||||
getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] {
|
||||
return registry.getItemsForApiKind<T>(apiVersion, kind);
|
||||
static getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] {
|
||||
return InternalCatalogEntityRegistry.getInstance().getItemsForApiKind<T>(apiVersion, kind);
|
||||
}
|
||||
}
|
||||
|
||||
export const catalogEntities = new CatalogEntityRegistry();
|
||||
export class CatalogCategoryRegistry {
|
||||
/**
|
||||
* Registers a new category
|
||||
* @param category The category to register
|
||||
* @throws if the apiVersion and kind conflict with a previously registered category
|
||||
* @returns a disposer to remove the category
|
||||
*/
|
||||
static add(category: CatalogCategorySpec): () => void {
|
||||
return InternalCatalogCategoryRegistry.getInstance().add(category);
|
||||
}
|
||||
|
||||
static registerHandler(apiVersion: string, kind: string, handlerName: CategoryHandlerNames, handler: CatalogHandler<typeof handlerName>): Disposer {
|
||||
return InternalCatalogCategoryRegistry.getInstance().registerHandler(apiVersion, kind, handlerName, handler);
|
||||
}
|
||||
|
||||
static runEntityHandlersFor(entity: CatalogEntity, handlerName: "onContextMenuOpen"): ReturnType<CategoryHandlers[typeof handlerName]>;
|
||||
static runEntityHandlersFor(entity: CatalogEntity, handlerName: "onSettingsOpen"): ReturnType<CategoryHandlers[typeof handlerName]>;
|
||||
static runEntityHandlersFor(entity: CatalogEntity, handlerName: EntityContextHandlers): ReturnType<CategoryHandlers[typeof handlerName]> {
|
||||
return InternalCatalogCategoryRegistry.getInstance().runEntityHandlersFor(entity, handlerName as any);
|
||||
}
|
||||
|
||||
static runGlobalHandlersFor(categorySpec: CatalogCategorySpec, handlerName: "onAddMenuOpen"): ReturnType<CategoryHandlers[typeof handlerName]>;
|
||||
static runGlobalHandlersFor(categorySpec: CatalogCategorySpec, handlerName: GlobalContextHandlers): ReturnType<CategoryHandlers[typeof handlerName]> {
|
||||
return InternalCatalogCategoryRegistry.getInstance().runGlobalHandlersFor(categorySpec, handlerName as any);
|
||||
}
|
||||
|
||||
static getEntityForData<Entity extends CatalogEntity>(data: MatchingCatalogEntityData<Entity> & CatalogEntityKindData): Entity {
|
||||
return InternalCatalogCategoryRegistry.getInstance().getEntityForData(data);
|
||||
}
|
||||
|
||||
static getCategorySpecForEntity(data: CatalogEntityData & CatalogEntityKindData): CatalogCategorySpec {
|
||||
return InternalCatalogCategoryRegistry.getInstance().getCategoryForEntity(data);
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,6 +19,6 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
export { Singleton, openExternal } from "../../common/utils";
|
||||
export { Singleton, openExternal, disposer, Disposer, ExtendableDisposer } from "../../common/utils";
|
||||
export { prevDefault, stopPropagation } from "../../renderer/utils/prevDefault";
|
||||
export { cssNames } from "../../renderer/utils/cssNames";
|
||||
|
||||
@ -24,7 +24,7 @@ import { action, observable, reaction } from "mobx";
|
||||
import { FilesystemProvisionerStore } from "../main/extension-filesystem";
|
||||
import logger from "../main/logger";
|
||||
import type { ProtocolHandlerRegistration } from "./registries";
|
||||
import { disposer } from "../common/utils";
|
||||
import { Disposer, disposer } from "../common/utils";
|
||||
|
||||
export type LensExtensionId = string; // path to manifest (package.json)
|
||||
export type LensExtensionConstructor = new (...args: ConstructorParameters<typeof LensExtension>) => LensExtension;
|
||||
@ -106,28 +106,21 @@ export class LensExtension {
|
||||
}
|
||||
}
|
||||
|
||||
async whenEnabled(handlers: () => Promise<Function[]>) {
|
||||
const disposers: Function[] = [];
|
||||
const unregisterHandlers = () => {
|
||||
disposers.forEach(unregister => unregister());
|
||||
disposers.length = 0;
|
||||
};
|
||||
const cancelReaction = reaction(() => this.isEnabled, async (isEnabled) => {
|
||||
if (isEnabled) {
|
||||
const handlerDisposers = await handlers();
|
||||
async whenEnabled(handlers: () => Promise<Disposer[]>) {
|
||||
const disposers = disposer();
|
||||
|
||||
disposers.push(...handlerDisposers);
|
||||
} else {
|
||||
unregisterHandlers();
|
||||
}
|
||||
}, {
|
||||
fireImmediately: true
|
||||
});
|
||||
|
||||
return () => {
|
||||
unregisterHandlers();
|
||||
cancelReaction();
|
||||
};
|
||||
return disposer(
|
||||
disposers,
|
||||
reaction(() => this.isEnabled, async (isEnabled) => {
|
||||
if (isEnabled) {
|
||||
disposers.push(...(await handlers()));
|
||||
} else {
|
||||
disposers();
|
||||
}
|
||||
}, {
|
||||
fireImmediately: true
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
protected onActivate(): void {
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
import { LensExtension } from "./lens-extension";
|
||||
import { WindowManager } from "../main/window-manager";
|
||||
import { getExtensionPageUrl } from "./registries/page-registry";
|
||||
import { CatalogEntity, catalogEntityRegistry } from "../common/catalog";
|
||||
import { CatalogEntity, CatalogEntityRegistry } from "../common/catalog";
|
||||
import type { IObservableArray } from "mobx";
|
||||
import type { MenuRegistration } from "./registries";
|
||||
|
||||
@ -41,10 +41,10 @@ export class LensMainExtension extends LensExtension {
|
||||
}
|
||||
|
||||
addCatalogSource(id: string, source: IObservableArray<CatalogEntity>) {
|
||||
catalogEntityRegistry.addObservableSource(`${this.name}:${id}`, source);
|
||||
CatalogEntityRegistry.getInstance().addObservableSource(`${this.name}:${id}`, source);
|
||||
}
|
||||
|
||||
removeCatalogSource(id: string) {
|
||||
catalogEntityRegistry.removeSource(`${this.name}:${id}`);
|
||||
CatalogEntityRegistry.getInstance().removeSource(`${this.name}:${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,27 +21,24 @@
|
||||
|
||||
import { reaction, toJS } from "mobx";
|
||||
import { broadcastMessage, ipcMainOn } from "../common/ipc";
|
||||
import type { CatalogEntityRegistry} from "../common/catalog";
|
||||
import { disposer, Singleton } from "../common/utils";
|
||||
import { CatalogEntityRegistry } from "../common/catalog";
|
||||
|
||||
import "../common/catalog-entities/kubernetes-cluster";
|
||||
import { disposer } from "../common/utils";
|
||||
|
||||
export class CatalogPusher {
|
||||
static init(catalog: CatalogEntityRegistry) {
|
||||
new CatalogPusher(catalog).init();
|
||||
}
|
||||
|
||||
private constructor(private catalog: CatalogEntityRegistry) {}
|
||||
|
||||
export class CatalogPusher extends Singleton {
|
||||
init() {
|
||||
return disposer(
|
||||
reaction(() => toJS(this.catalog.items, { recurseEverything: true }), (items) => {
|
||||
reaction(() => toJS(CatalogEntityRegistry.getInstance().items, { recurseEverything: true }), (items) => {
|
||||
console.log("pushing new items");
|
||||
broadcastMessage("catalog:items", items);
|
||||
}, {
|
||||
fireImmediately: true,
|
||||
}),
|
||||
ipcMainOn("catalog:broadcast", () => {
|
||||
broadcastMessage("catalog:items", toJS(this.catalog.items, { recurseEverything: true }));
|
||||
})
|
||||
broadcastMessage("catalog:items", toJS(CatalogEntityRegistry.getInstance().items, { recurseEverything: true }));
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,13 +20,13 @@
|
||||
*/
|
||||
|
||||
import { ObservableMap } from "mobx";
|
||||
import type { CatalogEntity } from "../../../common/catalog";
|
||||
import { loadFromOptions } from "../../../common/kube-helpers";
|
||||
import type { Cluster } from "../../cluster";
|
||||
import { computeDiff, configToModels } from "../kubeconfig-sync";
|
||||
import mockFs from "mock-fs";
|
||||
import fs from "fs";
|
||||
import { ClusterStore } from "../../../common/cluster-store";
|
||||
import type { KubernetesCluster } from "../../../common/catalog-entities";
|
||||
|
||||
describe("kubeconfig-sync.source tests", () => {
|
||||
beforeEach(() => {
|
||||
@ -79,7 +79,7 @@ describe("kubeconfig-sync.source tests", () => {
|
||||
describe("computeDiff", () => {
|
||||
it("should leave an empty source empty if there are no entries", () => {
|
||||
const contents = "";
|
||||
const rootSource = new ObservableMap<string, [Cluster, CatalogEntity]>();
|
||||
const rootSource = new ObservableMap<string, KubernetesCluster>();
|
||||
const filePath = "/bar";
|
||||
|
||||
computeDiff(contents, rootSource, filePath);
|
||||
@ -114,7 +114,7 @@ describe("kubeconfig-sync.source tests", () => {
|
||||
}],
|
||||
currentContext: "foobar"
|
||||
});
|
||||
const rootSource = new ObservableMap<string, [Cluster, CatalogEntity]>();
|
||||
const rootSource = new ObservableMap<string, KubernetesCluster>();
|
||||
const filePath = "/bar";
|
||||
|
||||
fs.writeFileSync(filePath, contents);
|
||||
@ -157,7 +157,7 @@ describe("kubeconfig-sync.source tests", () => {
|
||||
}],
|
||||
currentContext: "foobar"
|
||||
});
|
||||
const rootSource = new ObservableMap<string, [Cluster, CatalogEntity]>();
|
||||
const rootSource = new ObservableMap<string, KubernetesCluster>();
|
||||
const filePath = "/bar";
|
||||
|
||||
fs.writeFileSync(filePath, contents);
|
||||
@ -211,7 +211,7 @@ describe("kubeconfig-sync.source tests", () => {
|
||||
}],
|
||||
currentContext: "foobar"
|
||||
});
|
||||
const rootSource = new ObservableMap<string, [Cluster, CatalogEntity]>();
|
||||
const rootSource = new ObservableMap<string, KubernetesCluster>();
|
||||
const filePath = "/bar";
|
||||
|
||||
fs.writeFileSync(filePath, contents);
|
||||
|
||||
@ -18,9 +18,6 @@
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { action, observable, IComputedValue, computed, ObservableMap, runInAction } from "mobx";
|
||||
import { CatalogEntity, catalogEntityRegistry } from "../../common/catalog";
|
||||
import { watch } from "chokidar";
|
||||
import fs from "fs";
|
||||
import fse from "fs-extra";
|
||||
@ -29,22 +26,32 @@ import { Disposer, ExtendedObservableMap, iter, Singleton, storedKubeConfigFolde
|
||||
import logger from "../logger";
|
||||
import type { KubeConfig } from "@kubernetes/client-node";
|
||||
import { loadConfigFromString, splitConfig, validateKubeConfig } from "../../common/kube-helpers";
|
||||
import { Cluster } from "../cluster";
|
||||
import { catalogEntityFromCluster } from "../cluster-manager";
|
||||
import { UserStore } from "../../common/user-store";
|
||||
import { ClusterStore } from "../../common/cluster-store";
|
||||
import type { UpdateClusterModel } from "../../common/cluster-types";
|
||||
import { createHash } from "crypto";
|
||||
import { observable, ObservableMap, computed, action, runInAction } from "mobx";
|
||||
import { CatalogEntityRegistry } from "../../common/catalog";
|
||||
import { KubernetesCluster } from "../../common/catalog-entities";
|
||||
import { Cluster } from "../cluster";
|
||||
|
||||
const logPrefix = "[KUBECONFIG-SYNC]:";
|
||||
|
||||
export class KubeconfigSyncManager extends Singleton {
|
||||
protected sources = observable.map<string, [IComputedValue<CatalogEntity[]>, Disposer]>();
|
||||
protected sources = observable.map<string, [ExtendedObservableMap<string, ObservableMap<string, KubernetesCluster>>, Disposer]>();
|
||||
protected syncing = false;
|
||||
protected syncListDisposer?: Disposer;
|
||||
|
||||
protected static readonly syncName = "lens:kube-sync";
|
||||
|
||||
protected items = computed(() => (
|
||||
Array.from(iter.flatMap(
|
||||
this.sources.values(),
|
||||
([sources]) => iter.flatMap(
|
||||
sources.values(),
|
||||
source => source.values(),
|
||||
),
|
||||
))
|
||||
));
|
||||
|
||||
@action
|
||||
startSync(): void {
|
||||
if (this.syncing) {
|
||||
@ -55,12 +62,7 @@ export class KubeconfigSyncManager extends Singleton {
|
||||
|
||||
logger.info(`${logPrefix} starting requested syncs`);
|
||||
|
||||
catalogEntityRegistry.addComputedSource(KubeconfigSyncManager.syncName, computed(() => (
|
||||
Array.from(iter.flatMap(
|
||||
this.sources.values(),
|
||||
([entities]) => entities.get()
|
||||
))
|
||||
)));
|
||||
CatalogEntityRegistry.getInstance().addComputedSource(KubeconfigSyncManager.syncName, this.items);
|
||||
|
||||
// This must be done so that c&p-ed clusters are visible
|
||||
this.startNewSync(storedKubeConfigFolder());
|
||||
@ -89,7 +91,7 @@ export class KubeconfigSyncManager extends Singleton {
|
||||
this.stopOldSync(filePath);
|
||||
}
|
||||
|
||||
catalogEntityRegistry.removeSource(KubeconfigSyncManager.syncName);
|
||||
CatalogEntityRegistry.getInstance().removeSource(KubeconfigSyncManager.syncName);
|
||||
this.syncing = false;
|
||||
}
|
||||
|
||||
@ -142,8 +144,7 @@ export function configToModels(config: KubeConfig, filePath: string): UpdateClus
|
||||
return validConfigs;
|
||||
}
|
||||
|
||||
type RootSourceValue = [Cluster, CatalogEntity];
|
||||
type RootSource = ObservableMap<string, RootSourceValue>;
|
||||
type RootSource = ObservableMap<string, KubernetesCluster>;
|
||||
|
||||
// exported for testing
|
||||
export function computeDiff(contents: string, source: RootSource, filePath: string): void {
|
||||
@ -154,12 +155,11 @@ export function computeDiff(contents: string, source: RootSource, filePath: stri
|
||||
|
||||
logger.debug(`${logPrefix} File now has ${models.size} entries`, { filePath });
|
||||
|
||||
for (const [contextName, value] of source) {
|
||||
for (const [contextName] of source) {
|
||||
const model = models.get(contextName);
|
||||
|
||||
// remove and disconnect clusters that were removed from the config
|
||||
// remove clusters that were removed from the config
|
||||
if (!model) {
|
||||
value[0].disconnect();
|
||||
source.delete(contextName);
|
||||
logger.debug(`${logPrefix} Removed old cluster from sync`, { filePath, contextName });
|
||||
continue;
|
||||
@ -169,31 +169,32 @@ export function computeDiff(contents: string, source: RootSource, filePath: stri
|
||||
// Probably should make it so that cluster keeps a copy of the config in its memory and
|
||||
// diff against that
|
||||
|
||||
// or update the model and mark it as not needed to be added
|
||||
value[0].updateModel(model);
|
||||
// or mark it as not needed to be added
|
||||
models.delete(contextName);
|
||||
logger.debug(`${logPrefix} Updated old cluster from sync`, { filePath, contextName });
|
||||
}
|
||||
|
||||
for (const [contextName, model] of models) {
|
||||
// add new clusters to the source
|
||||
try {
|
||||
const clusterId = createHash("md5").update(`${filePath}:${contextName}`).digest("hex");
|
||||
const cluster = ClusterStore.getInstance().getById(clusterId) || new Cluster({ ...model, id: clusterId});
|
||||
|
||||
if (!cluster.apiUrl) {
|
||||
throw new Error("Cluster constructor failed, see above error");
|
||||
source.set(contextName, new KubernetesCluster({
|
||||
metadata: {
|
||||
uid: Cluster.getDeteministicId(model),
|
||||
name: model.contextName,
|
||||
source: "local",
|
||||
labels: {
|
||||
file: filePath,
|
||||
}
|
||||
},
|
||||
spec: {
|
||||
kubeconfigPath: model.kubeConfigPath,
|
||||
kubeconfigContext: model.contextName
|
||||
},
|
||||
status: {
|
||||
phase: "disconnected",
|
||||
}
|
||||
}));
|
||||
|
||||
const entity = catalogEntityFromCluster(cluster);
|
||||
|
||||
entity.metadata.labels.file = filePath;
|
||||
source.set(contextName, [cluster, entity]);
|
||||
|
||||
logger.debug(`${logPrefix} Added new cluster from sync`, { filePath, contextName });
|
||||
} catch (error) {
|
||||
logger.warn(`${logPrefix} Failed to create cluster from model: ${error}`, { filePath, contextName });
|
||||
}
|
||||
logger.debug(`${logPrefix} Added new cluster from sync`, { filePath, contextName });
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`${logPrefix} Failed to compute diff: ${error}`, { filePath });
|
||||
@ -242,15 +243,14 @@ function diffChangedConfig(filePath: string, source: RootSource): Disposer {
|
||||
return cleanup;
|
||||
}
|
||||
|
||||
async function watchFileChanges(filePath: string): Promise<[IComputedValue<CatalogEntity[]>, Disposer]> {
|
||||
async function watchFileChanges(filePath: string): Promise<[ExtendedObservableMap<string, ObservableMap<string, KubernetesCluster>>, Disposer]> {
|
||||
const stat = await fse.stat(filePath); // traverses symlinks, is a race condition
|
||||
const watcher = watch(filePath, {
|
||||
followSymlinks: true,
|
||||
depth: stat.isDirectory() ? 0 : 1, // DIRs works with 0 but files need 1 (bug: https://github.com/paulmillr/chokidar/issues/1095)
|
||||
disableGlobbing: true,
|
||||
});
|
||||
const rootSource = new ExtendedObservableMap<string, ObservableMap<string, RootSourceValue>>(observable.map);
|
||||
const derivedSource = computed(() => Array.from(iter.flatMap(rootSource.values(), from => iter.map(from.values(), child => child[1]))));
|
||||
const rootSource = new ExtendedObservableMap<string, ObservableMap<string, KubernetesCluster>>(observable.map);
|
||||
const stoppers = new Map<string, Disposer>();
|
||||
|
||||
watcher
|
||||
@ -268,5 +268,5 @@ async function watchFileChanges(filePath: string): Promise<[IComputedValue<Catal
|
||||
})
|
||||
.on("error", error => logger.error(`${logPrefix} watching file/folder failed: ${error}`, { filePath }));
|
||||
|
||||
return [derivedSource, () => watcher.close()];
|
||||
return [rootSource, () => watcher.close()];
|
||||
}
|
||||
|
||||
@ -18,129 +18,161 @@
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import type http from "http";
|
||||
import { ipcMain } from "electron";
|
||||
import { action, autorun, reaction, toJS } from "mobx";
|
||||
import { ClusterStore } from "../common/cluster-store";
|
||||
import { getClusterIdFromHost } from "../common/cluster-types";
|
||||
import type { Cluster } from "./cluster";
|
||||
import logger from "./logger";
|
||||
import { action, computed, observable, reaction } from "mobx";
|
||||
import { CatalogEntityRegistry } from "../common/catalog";
|
||||
import type { KubernetesCluster, KubernetesClusterStatus } from "../common/catalog-entities/kubernetes-cluster";
|
||||
import * as ClusterChannels from "../common/cluster-ipc";
|
||||
import { appEventBus } from "../common/event-bus";
|
||||
import { iter, noop, Singleton } from "../common/utils";
|
||||
import { apiKubePrefix } from "../common/vars";
|
||||
import { Singleton } from "../common/utils";
|
||||
import { catalogEntityRegistry } from "../common/catalog";
|
||||
import { KubernetesCluster, KubernetesClusterPrometheusMetrics } from "../common/catalog-entities/kubernetes-cluster";
|
||||
import { Cluster } from "./cluster";
|
||||
import logger from "./logger";
|
||||
import { ResourceApplier } from "./resource-applier";
|
||||
import type http from "http";
|
||||
import { ClusterId, getClusterIdFromHost } from "../common/cluster-types";
|
||||
|
||||
export type ClusterFrameInfo = {
|
||||
frameId: number;
|
||||
processId: number
|
||||
};
|
||||
|
||||
export class ClusterManager extends Singleton {
|
||||
protected clusterInstances = observable.map<ClusterId, Cluster>();
|
||||
protected clusterFrameMap = observable.map<string, ClusterFrameInfo>();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
reaction(() => toJS(ClusterStore.getInstance().clustersList, { recurseEverything: true }), () => {
|
||||
this.updateCatalog(ClusterStore.getInstance().clustersList);
|
||||
}, { fireImmediately: true });
|
||||
|
||||
reaction(() => catalogEntityRegistry.getItemsForApiKind<KubernetesCluster>("entity.k8slens.dev/v1alpha1", "KubernetesCluster"), (entities) => {
|
||||
reaction(() => CatalogEntityRegistry.getInstance().getItemsForApiKind<KubernetesCluster>("entity.k8slens.dev/v1alpha1", "KubernetesCluster"), (entities) => {
|
||||
this.syncClustersFromCatalog(entities);
|
||||
});
|
||||
|
||||
|
||||
// auto-stop removed clusters
|
||||
autorun(() => {
|
||||
const removedClusters = Array.from(ClusterStore.getInstance().removedClusters.values());
|
||||
|
||||
if (removedClusters.length > 0) {
|
||||
const meta = removedClusters.map(cluster => cluster.getMeta());
|
||||
|
||||
logger.info(`[CLUSTER-MANAGER]: removing clusters`, meta);
|
||||
removedClusters.forEach(cluster => cluster.disconnect());
|
||||
ClusterStore.getInstance().removedClusters.clear();
|
||||
}
|
||||
}, {
|
||||
delay: 250
|
||||
});
|
||||
|
||||
ipcMain.on("network:offline", () => { this.onNetworkOffline(); });
|
||||
ipcMain.on("network:online", () => { this.onNetworkOnline(); });
|
||||
ipcMain.on("network:offline", this.onNetworkOffline);
|
||||
ipcMain.on("network:online", this.onNetworkOnline);
|
||||
ipcMain.handle(ClusterChannels.activate, this.handleClusterActivate);
|
||||
ipcMain.handle(ClusterChannels.setFrameId, this.handleClusteSetFrameId);
|
||||
ipcMain.handle(ClusterChannels.refresh, this.handleClusterRefresh);
|
||||
ipcMain.handle(ClusterChannels.disconnect, this.handleClusterDisconnect);
|
||||
ipcMain.handle(ClusterChannels.kubectlApplyAll, this.handleKubectlApplyAll);
|
||||
}
|
||||
|
||||
@action protected updateCatalog(clusters: Cluster[]) {
|
||||
for (const cluster of clusters) {
|
||||
const index = catalogEntityRegistry.items.findIndex((entity) => entity.metadata.uid === cluster.id);
|
||||
|
||||
if (index !== -1) {
|
||||
const entity = catalogEntityRegistry.items[index] as KubernetesCluster;
|
||||
|
||||
entity.status.phase = cluster.disconnected ? "disconnected" : "connected";
|
||||
entity.status.active = !cluster.disconnected;
|
||||
|
||||
if (cluster.preferences?.clusterName) {
|
||||
entity.metadata.name = cluster.preferences.clusterName;
|
||||
}
|
||||
|
||||
entity.spec.metrics ||= { source: "local" };
|
||||
|
||||
if (entity.spec.metrics.source === "local") {
|
||||
const prometheus: KubernetesClusterPrometheusMetrics = entity.spec?.metrics?.prometheus || {};
|
||||
|
||||
prometheus.type = cluster.preferences.prometheusProvider?.type;
|
||||
prometheus.address = cluster.preferences.prometheus;
|
||||
entity.spec.metrics.prometheus = prometheus;
|
||||
}
|
||||
|
||||
catalogEntityRegistry.items.splice(index, 1, entity);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Is a computed mapping between `frameId`'s and their associated `ClusterFrameInfo`
|
||||
*/
|
||||
@computed get frameMapById(): Map<number, number> {
|
||||
return new Map(iter.map(this.clusterFrameMap.values(), info => [info.frameId, info.processId]));
|
||||
}
|
||||
|
||||
@action syncClustersFromCatalog(entities: KubernetesCluster[]) {
|
||||
for (const entity of entities) {
|
||||
const cluster = ClusterStore.getInstance().getById(entity.metadata.uid);
|
||||
const cluster = this.clusterInstances.get(entity.metadata.uid);
|
||||
|
||||
if (!cluster) {
|
||||
ClusterStore.getInstance().addCluster({
|
||||
this.clusterInstances.set(entity.metadata.uid, new Cluster({
|
||||
id: entity.metadata.uid,
|
||||
preferences: {
|
||||
clusterName: entity.metadata.name
|
||||
},
|
||||
kubeConfigPath: entity.spec.kubeconfigPath,
|
||||
contextName: entity.spec.kubeconfigContext
|
||||
}));
|
||||
|
||||
// This is done so that the push to renderer is updated as necessary
|
||||
// This also should prevent extensions from trying to set this themselves
|
||||
// in the future.
|
||||
Object.defineProperty(entity, "status", {
|
||||
enumerable: true,
|
||||
configurable: false,
|
||||
writable: false,
|
||||
get(): KubernetesClusterStatus {
|
||||
return {
|
||||
phase: cluster.disconnected ? "disconnected" : "connected",
|
||||
active: !cluster.disconnected,
|
||||
};
|
||||
}
|
||||
});
|
||||
} else {
|
||||
cluster.kubeConfigPath = entity.spec.kubeconfigPath;
|
||||
cluster.contextName = entity.spec.kubeconfigContext;
|
||||
|
||||
entity.status = {
|
||||
phase: cluster.disconnected ? "disconnected" : "connected",
|
||||
active: !cluster.disconnected
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected onNetworkOffline() {
|
||||
protected onNetworkOffline = () => {
|
||||
logger.info("[CLUSTER-MANAGER]: network is offline");
|
||||
ClusterStore.getInstance().clustersList.forEach((cluster) => {
|
||||
|
||||
for (const cluster of this.clusterInstances.values()) {
|
||||
if (!cluster.disconnected) {
|
||||
cluster.online = false;
|
||||
cluster.accessible = false;
|
||||
cluster.refreshConnectionStatus().catch((e) => e);
|
||||
cluster.refreshConnectionStatus().catch(noop);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
protected onNetworkOnline = () => {
|
||||
logger.info("[CLUSTER-MANAGER]: network is online");
|
||||
|
||||
for (const cluster of this.clusterInstances.values()) {
|
||||
if (!cluster.disconnected) {
|
||||
cluster.refreshConnectionStatus().catch(noop);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
protected handleClusterActivate = (event: Electron.IpcMainInvokeEvent, clusterId: ClusterId, force = false) => {
|
||||
return this.clusterInstances.get(clusterId)?.activate(force);
|
||||
};
|
||||
|
||||
protected handleClusteSetFrameId = ({ frameId, processId }: Electron.IpcMainInvokeEvent, clusterId: ClusterId) => {
|
||||
const cluster = this.clusterInstances.get(clusterId);
|
||||
|
||||
if (!cluster) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.clusterFrameMap.set(cluster.id, { frameId, processId });
|
||||
|
||||
return cluster.pushState();
|
||||
};
|
||||
|
||||
protected handleClusterRefresh = (event: Electron.IpcMainInvokeEvent, clusterId: ClusterId) => {
|
||||
return this.clusterInstances.get(clusterId)?.refresh({ refreshMetadata: true });
|
||||
};
|
||||
|
||||
protected handleClusterDisconnect = (event: Electron.IpcMainInvokeEvent, clusterId: ClusterId) => {
|
||||
return this.clusterInstances.get(clusterId)?.disconnect();
|
||||
};
|
||||
|
||||
protected handleKubectlApplyAll = (event: Electron.IpcMainInvokeEvent, clusterId: ClusterId, resources: string[]) => {
|
||||
appEventBus.emit({ name: "cluster", action: "kubectl-apply-all" });
|
||||
|
||||
const cluster = this.clusterInstances.get(clusterId);
|
||||
|
||||
if (!cluster) {
|
||||
throw new Error(`${clusterId} is not a valid ID`);
|
||||
}
|
||||
|
||||
return ResourceApplier.new(cluster).kubectlApplyAll(resources);
|
||||
};
|
||||
|
||||
getFrameInfoByClusterId(clusterId: ClusterId): ClusterFrameInfo {
|
||||
return this.clusterFrameMap.get(clusterId);
|
||||
}
|
||||
|
||||
protected onNetworkOnline() {
|
||||
logger.info("[CLUSTER-MANAGER]: network is online");
|
||||
ClusterStore.getInstance().clustersList.forEach((cluster) => {
|
||||
if (!cluster.disconnected) {
|
||||
cluster.refreshConnectionStatus().catch((e) => e);
|
||||
}
|
||||
});
|
||||
getFrameProcessIdById(frameId: number): number {
|
||||
return this.frameMapById.get(frameId);
|
||||
}
|
||||
|
||||
getAllFrameInfo(): ClusterFrameInfo[] {
|
||||
return Array.from(this.clusterFrameMap.values());
|
||||
}
|
||||
|
||||
stop() {
|
||||
ClusterStore.getInstance().clusters.forEach((cluster: Cluster) => {
|
||||
for (const cluster of this.clusterInstances.values()) {
|
||||
cluster.disconnect();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getClusterForRequest(req: http.IncomingMessage): Cluster {
|
||||
@ -150,45 +182,20 @@ export class ClusterManager extends Singleton {
|
||||
if (req.headers.host.startsWith("127.0.0.1")) {
|
||||
const clusterId = req.url.split("/")[1];
|
||||
|
||||
cluster = ClusterStore.getInstance().getById(clusterId);
|
||||
cluster = this.clusterInstances.get(clusterId);
|
||||
|
||||
if (cluster) {
|
||||
// we need to swap path prefix so that request is proxied to kube api
|
||||
req.url = req.url.replace(`/${clusterId}`, apiKubePrefix);
|
||||
}
|
||||
} else if (req.headers["x-cluster-id"]) {
|
||||
cluster = ClusterStore.getInstance().getById(req.headers["x-cluster-id"].toString());
|
||||
cluster = this.clusterInstances.get(req.headers["x-cluster-id"].toString());
|
||||
} else {
|
||||
const clusterId = getClusterIdFromHost(req.headers.host);
|
||||
|
||||
cluster = ClusterStore.getInstance().getById(clusterId);
|
||||
cluster = this.clusterInstances.get(clusterId);
|
||||
}
|
||||
|
||||
return cluster;
|
||||
}
|
||||
}
|
||||
|
||||
export function catalogEntityFromCluster(cluster: Cluster) {
|
||||
return new KubernetesCluster(toJS({
|
||||
apiVersion: "entity.k8slens.dev/v1alpha1",
|
||||
kind: "KubernetesCluster",
|
||||
metadata: {
|
||||
uid: cluster.id,
|
||||
name: cluster.name,
|
||||
source: "local",
|
||||
labels: {
|
||||
distro: cluster.distribution,
|
||||
}
|
||||
},
|
||||
spec: {
|
||||
kubeconfigPath: cluster.kubeConfigPath,
|
||||
kubeconfigContext: cluster.contextName
|
||||
},
|
||||
status: {
|
||||
phase: cluster.disconnected ? "disconnected" : "connected",
|
||||
reason: "",
|
||||
message: "",
|
||||
active: !cluster.disconnected
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@ -18,20 +18,21 @@
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { AuthorizationV1Api, CoreV1Api, HttpError, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node";
|
||||
import { createHash } from "crypto";
|
||||
import { ipcMain } from "electron";
|
||||
import { action, comparer, computed, observable, reaction, toJS, when } from "mobx";
|
||||
import plimit from "p-limit";
|
||||
|
||||
import { broadcastMessage, ClusterListNamespaceForbiddenChannel } from "../common/ipc";
|
||||
import { ContextHandler } from "./context-handler";
|
||||
import { AuthorizationV1Api, CoreV1Api, HttpError, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node";
|
||||
import { Kubectl } from "./kubectl";
|
||||
import { KubeconfigManager } from "./kubeconfig-manager";
|
||||
import { loadConfig, validateKubeConfig } from "../common/kube-helpers";
|
||||
import { apiResourceRecord, apiResources, KubeApiResource, KubeResource } from "../common/rbac";
|
||||
import logger from "./logger";
|
||||
import { VersionDetector } from "./cluster-detectors/version-detector";
|
||||
import { detectorRegistry } from "./cluster-detectors/detector-registry";
|
||||
import plimit from "p-limit";
|
||||
import { VersionDetector } from "./cluster-detectors/version-detector";
|
||||
import { ContextHandler } from "./context-handler";
|
||||
import { KubeconfigManager } from "./kubeconfig-manager";
|
||||
import { Kubectl } from "./kubectl";
|
||||
import logger from "./logger";
|
||||
import type { ClusterModel, ClusterState, ClusterId, ClusterPreferences, ClusterMetadata, ClusterPrometheusPreferences, UpdateClusterModel, ClusterRefreshOptions } from "../common/cluster-types";
|
||||
import { ClusterStatus } from "../common/cluster-types";
|
||||
|
||||
@ -41,6 +42,10 @@ import { ClusterStatus } from "../common/cluster-types";
|
||||
* @beta
|
||||
*/
|
||||
export class Cluster implements ClusterModel, ClusterState {
|
||||
public static getDeteministicId(model: UpdateClusterModel): ClusterId {
|
||||
return createHash("md5").update(`${model.kubeConfigPath}:${model.contextName}`).digest("hex");
|
||||
}
|
||||
|
||||
/** Unique id for a cluster */
|
||||
public readonly id: ClusterId;
|
||||
/**
|
||||
|
||||
@ -23,6 +23,7 @@
|
||||
|
||||
import "../common/system-ca";
|
||||
import "../common/prometheus-providers";
|
||||
import "./initilizers";
|
||||
import * as Mobx from "mobx";
|
||||
import * as LensExtensions from "../extensions/core-api";
|
||||
import { app, autoUpdater, ipcMain, dialog, powerMonitor } from "electron";
|
||||
@ -50,7 +51,6 @@ import { initGetSubFramesHandler } from "../common/ipc";
|
||||
import { startUpdateChecking } from "./app-updater";
|
||||
import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
|
||||
import { CatalogPusher } from "./catalog-pusher";
|
||||
import { catalogEntityRegistry } from "../common/catalog";
|
||||
import { HotbarStore } from "../common/hotbar-store";
|
||||
import { HelmRepoManager } from "./helm/helm-repo-manager";
|
||||
import { KubeconfigSyncManager } from "./catalog-sources";
|
||||
@ -59,6 +59,8 @@ import { initIpcMainHandlers } from "./initializers/ipc-handlers";
|
||||
import { Router } from "./router";
|
||||
import { initMenu } from "./menu";
|
||||
import { initTray } from "./tray";
|
||||
import { CatalogCategoryRegistry, CatalogEntityRegistry } from "../common/catalog";
|
||||
import { registerDefaultCategories } from "../common/default-categories";
|
||||
|
||||
const workingDir = path.join(app.getPath("appData"), appName);
|
||||
const cleanup = disposer();
|
||||
@ -127,6 +129,10 @@ app.on("ready", async () => {
|
||||
|
||||
registerFileProtocol("static", __static);
|
||||
|
||||
CatalogCategoryRegistry.createInstance();
|
||||
registerDefaultCategories();
|
||||
CatalogEntityRegistry.createInstance();
|
||||
|
||||
const userStore = UserStore.createInstance();
|
||||
const clusterStore = ClusterStore.createInstance();
|
||||
const hotbarStore = HotbarStore.createInstance();
|
||||
@ -197,7 +203,7 @@ app.on("ready", async () => {
|
||||
}
|
||||
|
||||
ipcMain.on(IpcRendererNavigationEvents.LOADED, () => {
|
||||
CatalogPusher.init(catalogEntityRegistry);
|
||||
CatalogPusher.createInstance().init();
|
||||
startUpdateChecking();
|
||||
LensProtocolRouterMain
|
||||
.getInstance()
|
||||
|
||||
@ -31,7 +31,11 @@ import { appEventBus } from "../common/event-bus";
|
||||
import { cloneJsonObject } from "../common/utils";
|
||||
|
||||
export class ResourceApplier {
|
||||
constructor(protected cluster: Cluster) {
|
||||
static new(cluster: Cluster) {
|
||||
return new ResourceApplier(cluster);
|
||||
}
|
||||
|
||||
protected constructor(protected cluster: Cluster) {
|
||||
}
|
||||
|
||||
async apply(resource: KubernetesObject | any): Promise<any> {
|
||||
|
||||
@ -26,10 +26,10 @@ import windowStateKeeper from "electron-window-state";
|
||||
import { appEventBus } from "../common/event-bus";
|
||||
import { ipcMainOn } from "../common/ipc";
|
||||
import { Singleton } from "../common/utils";
|
||||
import { ClusterFrameInfo, clusterFrameMap } from "../common/cluster-frames";
|
||||
import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
|
||||
import logger from "./logger";
|
||||
import { productName } from "../common/vars";
|
||||
import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
|
||||
import { ClusterManager } from "./cluster-manager";
|
||||
import logger from "./logger";
|
||||
import { LensProxy } from "./proxy/lens-proxy";
|
||||
|
||||
export class WindowManager extends Singleton {
|
||||
@ -135,34 +135,33 @@ export class WindowManager extends Singleton {
|
||||
return this.mainWindow;
|
||||
}
|
||||
|
||||
sendToView({ channel, frameInfo, data = [] }: { channel: string, frameInfo?: ClusterFrameInfo, data?: any[] }) {
|
||||
if (frameInfo) {
|
||||
this.mainWindow.webContents.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...data);
|
||||
} else {
|
||||
this.mainWindow.webContents.send(channel, ...data);
|
||||
}
|
||||
}
|
||||
|
||||
async navigate(url: string, frameId?: number) {
|
||||
await this.ensureMainWindow();
|
||||
|
||||
const frameInfo = Array.from(clusterFrameMap.values()).find((frameInfo) => frameInfo.frameId === frameId);
|
||||
const channel = frameInfo
|
||||
? IpcRendererNavigationEvents.NAVIGATE_IN_CLUSTER
|
||||
: IpcRendererNavigationEvents.NAVIGATE_IN_APP;
|
||||
if (frameId === undefined) {
|
||||
const processId = ClusterManager.getInstance().getFrameProcessIdById(frameId);
|
||||
|
||||
this.sendToView({
|
||||
channel,
|
||||
frameInfo,
|
||||
data: [url],
|
||||
});
|
||||
this.mainWindow.webContents.sendToFrame(
|
||||
[processId, frameId],
|
||||
IpcRendererNavigationEvents.NAVIGATE_IN_APP,
|
||||
url
|
||||
);
|
||||
} else {
|
||||
this.mainWindow.webContents.send(
|
||||
IpcRendererNavigationEvents.NAVIGATE_IN_CLUSTER,
|
||||
url
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
reload() {
|
||||
const frameInfo = clusterFrameMap.get(this.activeClusterId);
|
||||
const frameInfo = ClusterManager.getInstance().getFrameInfoByClusterId(this.activeClusterId);
|
||||
|
||||
if (frameInfo) {
|
||||
this.sendToView({ channel: IpcRendererNavigationEvents.RELOAD_PAGE, frameInfo });
|
||||
this.mainWindow.webContents.sendToFrame(
|
||||
[frameInfo.processId, frameInfo.frameId],
|
||||
IpcRendererNavigationEvents.RELOAD_PAGE,
|
||||
);
|
||||
} else {
|
||||
webContents.getFocusedWebContents()?.reload();
|
||||
}
|
||||
|
||||
@ -21,12 +21,15 @@
|
||||
|
||||
import { CatalogEntityRegistry } from "../catalog-entity-registry";
|
||||
import "../../../common/catalog-entities";
|
||||
import { catalogCategoryRegistry } from "../../../common/catalog/catalog-category-registry";
|
||||
|
||||
describe("CatalogEntityRegistry", () => {
|
||||
describe("updateItems", () => {
|
||||
beforeEach(() => {
|
||||
CatalogEntityRegistry.resetInstance();
|
||||
});
|
||||
|
||||
it("adds new catalog item", () => {
|
||||
const catalog = new CatalogEntityRegistry(catalogCategoryRegistry);
|
||||
const catalog = CatalogEntityRegistry.createInstance();
|
||||
const items = [{
|
||||
apiVersion: "entity.k8slens.dev/v1alpha1",
|
||||
kind: "KubernetesCluster",
|
||||
@ -65,7 +68,7 @@ describe("CatalogEntityRegistry", () => {
|
||||
});
|
||||
|
||||
it("updates existing items", () => {
|
||||
const catalog = new CatalogEntityRegistry(catalogCategoryRegistry);
|
||||
const catalog = CatalogEntityRegistry.createInstance();
|
||||
const items = [{
|
||||
apiVersion: "entity.k8slens.dev/v1alpha1",
|
||||
kind: "KubernetesCluster",
|
||||
@ -93,7 +96,7 @@ describe("CatalogEntityRegistry", () => {
|
||||
});
|
||||
|
||||
it("removes deleted items", () => {
|
||||
const catalog = new CatalogEntityRegistry(catalogCategoryRegistry);
|
||||
const catalog = CatalogEntityRegistry.createInstance();
|
||||
const items = [
|
||||
{
|
||||
apiVersion: "entity.k8slens.dev/v1alpha1",
|
||||
|
||||
@ -19,4 +19,4 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
export { catalogCategoryRegistry } from "../../common/catalog";
|
||||
export { CatalogCategoryRegistry } from "../../common/catalog";
|
||||
|
||||
@ -18,18 +18,24 @@
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { action, observable } from "mobx";
|
||||
import { broadcastMessage, ipcRendererOn } from "../../common/ipc";
|
||||
import { CatalogCategory, CatalogEntity, CatalogEntityData, catalogCategoryRegistry, CatalogCategoryRegistry, CatalogEntityKindData } from "../../common/catalog";
|
||||
import "../../common/catalog-entities";
|
||||
|
||||
export class CatalogEntityRegistry {
|
||||
import { action, observable } from "mobx";
|
||||
|
||||
import {
|
||||
CatalogCategoryRegistry,
|
||||
CatalogCategorySpec,
|
||||
CatalogEntity,
|
||||
CatalogEntityData,
|
||||
CatalogEntityKindData,
|
||||
} from "../../common/catalog";
|
||||
import { broadcastMessage, ipcRendererOn } from "../../common/ipc";
|
||||
import { Singleton } from "../utils";
|
||||
|
||||
export class CatalogEntityRegistry extends Singleton {
|
||||
@observable protected _items: CatalogEntity[] = observable.array([], { deep: true });
|
||||
@observable protected _activeEntity: CatalogEntity;
|
||||
|
||||
constructor(private categoryRegistry: CatalogCategoryRegistry) {}
|
||||
|
||||
init() {
|
||||
ipcRendererOn("catalog:items", (ev, items: (CatalogEntityData & CatalogEntityKindData)[]) => {
|
||||
this.updateItems(items);
|
||||
@ -38,7 +44,9 @@ export class CatalogEntityRegistry {
|
||||
}
|
||||
|
||||
@action updateItems(items: (CatalogEntityData & CatalogEntityKindData)[]) {
|
||||
this._items = items.map(data => this.categoryRegistry.getEntityForData(data));
|
||||
const registry = CatalogCategoryRegistry.getInstance();
|
||||
|
||||
this._items = items.map(data => registry.getEntityForData(data));
|
||||
}
|
||||
|
||||
set activeEntity(entity: CatalogEntity) {
|
||||
@ -46,6 +54,8 @@ export class CatalogEntityRegistry {
|
||||
}
|
||||
|
||||
get activeEntity() {
|
||||
console.log(this._activeEntity);
|
||||
|
||||
return this._activeEntity;
|
||||
}
|
||||
|
||||
@ -63,12 +73,10 @@ export class CatalogEntityRegistry {
|
||||
return items as T[];
|
||||
}
|
||||
|
||||
getItemsForCategory<T extends CatalogEntity>(category: CatalogCategory): T[] {
|
||||
const supportedVersions = category.spec.versions.map((v) => `${category.spec.group}/${v.name}`);
|
||||
getItemsForCategory<T extends CatalogEntity>(category: CatalogCategorySpec): T[] {
|
||||
const supportedVersions = category.spec.versions.map((v) => `${category.spec.group}/${v.version}`);
|
||||
const items = this._items.filter((item) => supportedVersions.includes(item.apiVersion) && item.kind === category.spec.names.kind);
|
||||
|
||||
return items as T[];
|
||||
}
|
||||
}
|
||||
|
||||
export const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
|
||||
|
||||
@ -23,19 +23,21 @@ import { navigate } from "../navigation";
|
||||
import { CommandRegistry } from "../../extensions/registries";
|
||||
import type { CatalogEntity } from "../../common/catalog";
|
||||
|
||||
export { CatalogCategory, CatalogEntity } from "../../common/catalog";
|
||||
export { CatalogEntity } from "../../common/catalog";
|
||||
export type {
|
||||
CatalogCategorySpec,
|
||||
CatalogEntityData,
|
||||
CatalogEntityKindData,
|
||||
CatalogEntityActionContext,
|
||||
CatalogEntityAddMenuContext,
|
||||
CatalogEntityAddMenu,
|
||||
CatalogEntityContextMenu,
|
||||
CatalogEntityContextMenuContext,
|
||||
ActionContext,
|
||||
ActionHandler,
|
||||
MenuContext,
|
||||
ContextMenu,
|
||||
AddMenuOpenHandler,
|
||||
ContextMenuOpenHandler,
|
||||
} from "../../common/catalog";
|
||||
|
||||
export const catalogEntityRunContext = {
|
||||
navigate: (url: string) => navigate(url),
|
||||
navigate,
|
||||
setCommandPaletteContext: (entity?: CatalogEntity) => {
|
||||
CommandRegistry.getInstance().activeEntity = entity;
|
||||
}
|
||||
|
||||
@ -43,7 +43,9 @@ import { ThemeStore } from "./theme.store";
|
||||
import { HelmRepoManager } from "../main/helm/helm-repo-manager";
|
||||
import { ExtensionInstallationStateStore } from "./components/+extensions/extension-install.store";
|
||||
import { DefaultProps } from "./mui-base-theme";
|
||||
import { initCommandRegistry, initEntitySettingsRegistry, initKubeObjectMenuRegistry, initRegistries, initWelcomeMenuRegistry, intiKubeObjectDetailRegistry } from "./initializers";
|
||||
import * as initializers from "./initializers";
|
||||
import { CatalogCategoryRegistry, CatalogEntityRegistry } from "../common/catalog";
|
||||
import { registerDefaultCategories } from "../common/default-categories";
|
||||
|
||||
/**
|
||||
* If this is a development buid, wait a second to attach
|
||||
@ -75,12 +77,16 @@ export async function bootstrap(App: AppComponent) {
|
||||
await attachChromeDebugger();
|
||||
rootElem.classList.toggle("is-mac", isMac);
|
||||
|
||||
initRegistries();
|
||||
initCommandRegistry();
|
||||
initEntitySettingsRegistry();
|
||||
initKubeObjectMenuRegistry();
|
||||
intiKubeObjectDetailRegistry();
|
||||
initWelcomeMenuRegistry();
|
||||
initializers.initRegistries();
|
||||
initializers.initCommandRegistry();
|
||||
initializers.initEntitySettingsRegistry();
|
||||
initializers.initKubeObjectMenuRegistry();
|
||||
initializers.intiKubeObjectDetailRegistry();
|
||||
initializers.initWelcomeMenuRegistry();
|
||||
|
||||
CatalogCategoryRegistry.createInstance();
|
||||
registerDefaultCategories();
|
||||
CatalogEntityRegistry.createInstance();
|
||||
|
||||
ExtensionLoader.createInstance().init();
|
||||
ExtensionDiscovery.createInstance().init();
|
||||
|
||||
@ -26,32 +26,22 @@ import { Icon } from "../icon";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { observable, reaction } from "mobx";
|
||||
import { autobind } from "../../../common/utils";
|
||||
import type { CatalogCategory, CatalogEntityAddMenuContext, CatalogEntityAddMenu } from "../../api/catalog-entity";
|
||||
import { EventEmitter } from "events";
|
||||
import { navigate } from "../../navigation";
|
||||
import { CatalogCategoryRegistry } from "../../api/catalog-category-registry";
|
||||
import type { CatalogCategorySpec, ContextMenu } from "../../api/catalog-entity";
|
||||
|
||||
export type CatalogAddButtonProps = {
|
||||
category: CatalogCategory
|
||||
category: CatalogCategorySpec,
|
||||
};
|
||||
|
||||
@observer
|
||||
export class CatalogAddButton extends React.Component<CatalogAddButtonProps> {
|
||||
@observable protected isOpen = false;
|
||||
protected menuItems = observable.array<CatalogEntityAddMenu>([]);
|
||||
@observable protected menuItems: ContextMenu[] = [];
|
||||
|
||||
componentDidMount() {
|
||||
disposeOnUnmount(this, [
|
||||
reaction(() => this.props.category, (category) => {
|
||||
this.menuItems.clear();
|
||||
|
||||
if (category && category instanceof EventEmitter) {
|
||||
const context: CatalogEntityAddMenuContext = {
|
||||
navigate: (url: string) => navigate(url),
|
||||
menuItems: this.menuItems
|
||||
};
|
||||
|
||||
category.emit("onCatalogAddMenu", context);
|
||||
}
|
||||
this.menuItems = CatalogCategoryRegistry.getInstance().runGlobalHandlersFor(category, "onAddMenuOpen");
|
||||
}, { fireImmediately: true })
|
||||
]);
|
||||
}
|
||||
|
||||
@ -18,13 +18,11 @@
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { action, computed, IReactionDisposer, observable, reaction } from "mobx";
|
||||
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||
import type { CatalogEntity, CatalogEntityActionContext } from "../../api/catalog-entity";
|
||||
import { computed, IReactionDisposer, observable, reaction } from "mobx";
|
||||
import type { CatalogEntity, ActionContext, MenuContext, CatalogCategorySpec } from "../../api/catalog-entity";
|
||||
import { CatalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||
import { ItemObject, ItemStore } from "../../item.store";
|
||||
import { autobind } from "../../utils";
|
||||
import { CatalogCategory } from "../../../common/catalog";
|
||||
|
||||
export class CatalogEntityItem implements ItemObject {
|
||||
constructor(public entity: CatalogEntity) {}
|
||||
@ -74,26 +72,27 @@ export class CatalogEntityItem implements ItemObject {
|
||||
];
|
||||
}
|
||||
|
||||
onRun(ctx: CatalogEntityActionContext) {
|
||||
onRun(ctx: ActionContext) {
|
||||
this.entity.onRun(ctx);
|
||||
}
|
||||
|
||||
@action
|
||||
async onContextMenuOpen(ctx: any) {
|
||||
onContextMenuOpen(ctx: MenuContext) {
|
||||
return this.entity.onContextMenuOpen(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
@autobind()
|
||||
export class CatalogEntityStore extends ItemStore<CatalogEntityItem> {
|
||||
@observable activeCategory?: CatalogCategory;
|
||||
@observable activeCategory?: CatalogCategorySpec;
|
||||
|
||||
@computed get entities() {
|
||||
const registry = CatalogEntityRegistry.getInstance();
|
||||
|
||||
if (!this.activeCategory) {
|
||||
return catalogEntityRegistry.items.map(entity => new CatalogEntityItem(entity));
|
||||
return registry.items.map(entity => new CatalogEntityItem(entity));
|
||||
}
|
||||
|
||||
return catalogEntityRegistry.getItemsForCategory(this.activeCategory).map(entity => new CatalogEntityItem(entity));
|
||||
return registry.getItemsForCategory(this.activeCategory).map(entity => new CatalogEntityItem(entity));
|
||||
}
|
||||
|
||||
watch() {
|
||||
|
||||
@ -25,41 +25,43 @@ import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { ItemListLayout } from "../item-object-list";
|
||||
import { action, observable, reaction } from "mobx";
|
||||
import { CatalogEntityItem, CatalogEntityStore } from "./catalog-entity.store";
|
||||
import { navigate } from "../../navigation";
|
||||
import { kebabCase } from "lodash";
|
||||
import { PageLayout } from "../layout/page-layout";
|
||||
import { MenuItem, MenuActions } from "../menu";
|
||||
import { CatalogEntityContextMenu, CatalogEntityContextMenuContext, catalogEntityRunContext } from "../../api/catalog-entity";
|
||||
import { Icon } from "../icon";
|
||||
import { catalogEntityRunContext } from "../../api/catalog-entity";
|
||||
import { Badge } from "../badge";
|
||||
import { HotbarStore } from "../../../common/hotbar-store";
|
||||
import { autobind } from "../../utils";
|
||||
import { ConfirmDialog } from "../confirm-dialog";
|
||||
import { Tab, Tabs } from "../tabs";
|
||||
import { catalogCategoryRegistry } from "../../../common/catalog";
|
||||
import { CatalogCategoryRegistry, CatalogCategorySpec, ContextMenu } from "../../../common/catalog";
|
||||
import { CatalogAddButton } from "./catalog-add-button";
|
||||
import { navigate } from "../../navigation";
|
||||
|
||||
enum sortBy {
|
||||
name = "name",
|
||||
source = "source",
|
||||
status = "status"
|
||||
}
|
||||
|
||||
function getId({ spec }: CatalogCategorySpec): string {
|
||||
return `${spec.group}/${spec.names.kind}`;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class Catalog extends React.Component {
|
||||
@observable private catalogEntityStore?: CatalogEntityStore;
|
||||
@observable.deep private contextMenu: CatalogEntityContextMenuContext;
|
||||
@observable private menuItems: ContextMenu[] = [];
|
||||
@observable activeTab?: string;
|
||||
|
||||
async componentDidMount() {
|
||||
this.contextMenu = {
|
||||
menuItems: [],
|
||||
navigate: (url: string) => navigate(url)
|
||||
};
|
||||
this.catalogEntityStore = new CatalogEntityStore();
|
||||
disposeOnUnmount(this, [
|
||||
this.catalogEntityStore.watch(),
|
||||
reaction(() => catalogCategoryRegistry.items, (items) => {
|
||||
reaction(() => CatalogCategoryRegistry.getInstance().items, (items) => {
|
||||
if (!this.activeTab && items.length > 0) {
|
||||
this.activeTab = items[0].getId();
|
||||
this.activeTab = getId(items[0]);
|
||||
this.catalogEntityStore.activeCategory = items[0];
|
||||
}
|
||||
}, { fireImmediately: true })
|
||||
@ -74,7 +76,7 @@ export class Catalog extends React.Component {
|
||||
item.onRun(catalogEntityRunContext);
|
||||
}
|
||||
|
||||
onMenuItemClick(menuItem: CatalogEntityContextMenu) {
|
||||
onMenuItemClick(menuItem: ContextMenu) {
|
||||
if (menuItem.confirm) {
|
||||
ConfirmDialog.open({
|
||||
okButtonProps: {
|
||||
@ -91,16 +93,15 @@ export class Catalog extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
get categories() {
|
||||
return catalogCategoryRegistry.items;
|
||||
}
|
||||
|
||||
@action
|
||||
onTabChange = (tabId: string | null) => {
|
||||
const activeCategory = this.categories.find(category => category.getId() === tabId);
|
||||
const activeCategory = CatalogCategoryRegistry.getInstance().getById(tabId);
|
||||
|
||||
this.catalogEntityStore.activeCategory = activeCategory;
|
||||
this.activeTab = activeCategory?.getId();
|
||||
|
||||
if (activeCategory) {
|
||||
this.activeTab = `${activeCategory.spec.group}/${activeCategory.spec.names.kind}`;
|
||||
}
|
||||
};
|
||||
|
||||
renderNavigation() {
|
||||
@ -115,14 +116,16 @@ export class Catalog extends React.Component {
|
||||
data-testid="*-tab"
|
||||
/>
|
||||
{
|
||||
this.categories.map(category => (
|
||||
<Tab
|
||||
value={category.getId()}
|
||||
key={category.getId()}
|
||||
label={category.metadata.name}
|
||||
data-testid={`${category.getId()}-tab`}
|
||||
/>
|
||||
))
|
||||
CatalogCategoryRegistry.getInstance()
|
||||
.items
|
||||
.map(category => (
|
||||
<Tab
|
||||
value={getId(category)}
|
||||
key={getId(category)}
|
||||
label={category.metadata.name}
|
||||
data-testid={`${getId(category)}-tab`}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</Tabs>
|
||||
@ -131,10 +134,13 @@ export class Catalog extends React.Component {
|
||||
|
||||
@autobind()
|
||||
renderItemMenu(item: CatalogEntityItem) {
|
||||
const menuItems = this.contextMenu.menuItems.filter((menuItem) => !menuItem.onlyVisibleForSource || menuItem.onlyVisibleForSource === item.entity.metadata.source);
|
||||
const menuItems = this.menuItems.filter((menuItem) => !menuItem.onlyVisibleForSource || menuItem.onlyVisibleForSource === item.entity.metadata.source);
|
||||
|
||||
return (
|
||||
<MenuActions onOpen={() => item.onContextMenuOpen(this.contextMenu)}>
|
||||
<MenuActions onOpen={() => this.menuItems = item.onContextMenuOpen({ navigate })} onClose={() => this.menuItems = []}>
|
||||
<MenuItem key="add-to-hotbar" onClick={() => this.addToHotbar(item) }>
|
||||
<Icon material="add" small interactive={true} title="Add to hotbar"/> Add to Hotbar
|
||||
</MenuItem>
|
||||
{
|
||||
menuItems.map((menuItem, index) => (
|
||||
<MenuItem key={index} onClick={() => this.onMenuItemClick(menuItem)}>
|
||||
|
||||
@ -29,7 +29,7 @@ import { PageLayout } from "../layout/page-layout";
|
||||
import { navigation } from "../../navigation";
|
||||
import { Tabs, Tab } from "../tabs";
|
||||
import type { CatalogEntity } from "../../api/catalog-entity";
|
||||
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||
import { CatalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||
import { EntitySettingRegistry } from "../../../extensions/registries";
|
||||
import type { EntitySettingsRouteParams } from "../../../common/routes";
|
||||
import { groupBy } from "lodash";
|
||||
@ -46,7 +46,7 @@ export class EntitySettings extends React.Component<Props> {
|
||||
}
|
||||
|
||||
get entity(): CatalogEntity {
|
||||
return catalogEntityRegistry.getById(this.entityId);
|
||||
return CatalogEntityRegistry.getInstance().getById(this.entityId);
|
||||
}
|
||||
|
||||
get menuItems() {
|
||||
|
||||
@ -18,55 +18,53 @@
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import React from "react";
|
||||
import { Redirect, Route, Router, Switch } from "react-router";
|
||||
import { history } from "../navigation";
|
||||
import { Notifications } from "./notifications";
|
||||
import { NotFound } from "./+404";
|
||||
import { UserManagement } from "./+user-management/user-management";
|
||||
import { ConfirmDialog } from "./confirm-dialog";
|
||||
import { KubeConfigDialog } from "./kubeconfig-dialog/kubeconfig-dialog";
|
||||
import { Nodes } from "./+nodes";
|
||||
import { Workloads } from "./+workloads";
|
||||
import { Namespaces } from "./+namespaces";
|
||||
import { Network } from "./+network";
|
||||
import { Storage } from "./+storage";
|
||||
import { ClusterOverview } from "./+cluster/cluster-overview";
|
||||
import { Config } from "./+config";
|
||||
import { Events } from "./+events/events";
|
||||
import { Apps } from "./+apps";
|
||||
import { KubeObjectDetails } from "./kube-object/kube-object-details";
|
||||
import { AddRoleBindingDialog } from "./+user-management-roles-bindings";
|
||||
import { DeploymentScaleDialog } from "./+workloads-deployments/deployment-scale-dialog";
|
||||
import { CronJobTriggerDialog } from "./+workloads-cronjobs/cronjob-trigger-dialog";
|
||||
import { CustomResources } from "./+custom-resources/custom-resources";
|
||||
import { MainLayout } from "./layout/main-layout";
|
||||
import { ErrorBoundary } from "./error-boundary";
|
||||
import { Terminal } from "./dock/terminal";
|
||||
import whatInput from "what-input";
|
||||
import { setFrameId } from "../../common/cluster-ipc";
|
||||
import { getHostedCluster } from "../../common/cluster-store";
|
||||
import logger from "../../main/logger";
|
||||
import { webFrame } from "electron";
|
||||
import { ClusterPageRegistry, getExtensionPageUrl } from "../../extensions/registries/page-registry";
|
||||
import { ExtensionLoader } from "../../extensions/extension-loader";
|
||||
import { getHostedClusterId } from "../../common/cluster-types";
|
||||
import { appEventBus } from "../../common/event-bus";
|
||||
import { requestMain } from "../../common/ipc";
|
||||
import whatInput from "what-input";
|
||||
import { clusterSetFrameIdHandler } from "../../common/cluster-ipc";
|
||||
import { ExtensionLoader } from "../../extensions/extension-loader";
|
||||
import { ClusterPageMenuRegistration, ClusterPageMenuRegistry } from "../../extensions/registries";
|
||||
import { TabLayout, TabLayoutRoute } from "./layout/tab-layout";
|
||||
import { StatefulSetScaleDialog } from "./+workloads-statefulsets/statefulset-scale-dialog";
|
||||
import { KubeWatchApi } from "../api/kube-watch-api";
|
||||
import { ReplicaSetScaleDialog } from "./+workloads-replicasets/replicaset-scale-dialog";
|
||||
import { CommandContainer } from "./command-palette/command-container";
|
||||
import * as routes from "../../common/routes";
|
||||
import { getHostedClusterId } from "../../common/cluster-types";
|
||||
import { initApiManagerStores } from "../initializers/api-manager-stores";
|
||||
import { ApiManager } from "../api/api-manager";
|
||||
import { ClusterPageRegistry, getExtensionPageUrl } from "../../extensions/registries/page-registry";
|
||||
import type { Cluster } from "../../main/cluster";
|
||||
import { eventApi, namespacesApi, nodesApi, podsApi } from "../api/endpoints";
|
||||
import logger from "../../main/logger";
|
||||
import { ApiManager } from "../api/api-manager";
|
||||
import { podsApi, nodesApi, eventApi, namespacesApi } from "../api/endpoints";
|
||||
import { KubeWatchApi } from "../api/kube-watch-api";
|
||||
import { initApiManagerStores } from "../initializers/api-manager-stores";
|
||||
import { history } from "../navigation";
|
||||
import { NotFound } from "./+404";
|
||||
import { Apps } from "./+apps";
|
||||
import { ReleaseStore } from "./+apps-releases/release.store";
|
||||
import { ClusterOverview } from "./+cluster/cluster-overview";
|
||||
import { Config } from "./+config";
|
||||
import { CustomResources } from "./+custom-resources/custom-resources";
|
||||
import { Events } from "./+events/events";
|
||||
import { Namespaces } from "./+namespaces";
|
||||
import { Network } from "./+network";
|
||||
import { Nodes } from "./+nodes";
|
||||
import { Storage } from "./+storage";
|
||||
import { AddRoleBindingDialog } from "./+user-management-roles-bindings";
|
||||
import { UserManagement } from "./+user-management/user-management";
|
||||
import { Workloads } from "./+workloads";
|
||||
import { CronJobTriggerDialog } from "./+workloads-cronjobs/cronjob-trigger-dialog";
|
||||
import { DeploymentScaleDialog } from "./+workloads-deployments/deployment-scale-dialog";
|
||||
import { ReplicaSetScaleDialog } from "./+workloads-replicasets/replicaset-scale-dialog";
|
||||
import { StatefulSetScaleDialog } from "./+workloads-statefulsets/statefulset-scale-dialog";
|
||||
import { CommandContainer } from "./command-palette/command-container";
|
||||
import { ConfirmDialog } from "./confirm-dialog";
|
||||
import { Terminal } from "./dock/terminal";
|
||||
import { ErrorBoundary } from "./error-boundary";
|
||||
import { KubeObjectDetails } from "./kube-object/kube-object-details";
|
||||
import { KubeConfigDialog } from "./kubeconfig-dialog/kubeconfig-dialog";
|
||||
import { MainLayout } from "./layout/main-layout";
|
||||
import { TabLayout, TabLayoutRoute } from "./layout/tab-layout";
|
||||
import { Notifications } from "./notifications";
|
||||
import * as routes from "../../common/routes";
|
||||
|
||||
@observer
|
||||
export class ClusterFrame extends React.Component {
|
||||
@ -74,12 +72,12 @@ export class ClusterFrame extends React.Component {
|
||||
static cluster: Cluster;
|
||||
|
||||
static async init() {
|
||||
const frameId = webFrame.routingId;
|
||||
const frameId = Electron.webFrame.routingId;
|
||||
const clusterId = getHostedClusterId();
|
||||
|
||||
logger.info(`[APP]: Init dashboard, clusterId=${clusterId}, frameId=${frameId}`);
|
||||
await Terminal.preloadFonts();
|
||||
await requestMain(clusterSetFrameIdHandler, clusterId);
|
||||
await requestMain(setFrameId, clusterId);
|
||||
|
||||
this.cluster = getHostedCluster();
|
||||
|
||||
|
||||
@ -18,14 +18,14 @@
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import "./badge.scss";
|
||||
|
||||
import React from "react";
|
||||
|
||||
import { cssNames } from "../../utils/cssNames";
|
||||
import { TooltipDecoratorProps, withTooltip } from "../tooltip";
|
||||
|
||||
export interface BadgeProps extends React.HTMLAttributes<any>, TooltipDecoratorProps {
|
||||
export interface BadgeProps extends React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, TooltipDecoratorProps {
|
||||
small?: boolean;
|
||||
flat?: boolean;
|
||||
label?: React.ReactNode;
|
||||
@ -36,11 +36,11 @@ export class Badge extends React.Component<BadgeProps> {
|
||||
render() {
|
||||
const { className, label, small, flat, children, ...elemProps } = this.props;
|
||||
|
||||
return <>
|
||||
return (
|
||||
<span className={cssNames("Badge", { small, flat }, className)} {...elemProps}>
|
||||
{label}
|
||||
{children}
|
||||
</span>
|
||||
</>;
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,7 +20,9 @@
|
||||
*/
|
||||
|
||||
import "./button.scss";
|
||||
|
||||
import React, { ButtonHTMLAttributes } from "react";
|
||||
|
||||
import { cssNames } from "../../utils";
|
||||
import { TooltipDecoratorProps, withTooltip } from "../tooltip";
|
||||
|
||||
|
||||
@ -18,12 +18,8 @@
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import type { KubeAuthProxyLog } from "../../../main/kube-auth-proxy";
|
||||
|
||||
import "./cluster-status.scss";
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
import { ipcRenderer } from "electron";
|
||||
import { computed, observable } from "mobx";
|
||||
import { ipcRendererOn, requestMain } from "../../../common/ipc";
|
||||
@ -34,7 +30,10 @@ import type { Cluster } from "../../../main/cluster";
|
||||
import { ClusterStore } from "../../../common/cluster-store";
|
||||
import type { ClusterId } from "../../../common/cluster-types";
|
||||
import { CubeSpinner } from "../spinner";
|
||||
import { clusterActivateHandler } from "../../../common/cluster-ipc";
|
||||
import type { KubeAuthProxyLog } from "../../../main/kube-auth-proxy";
|
||||
import { observer } from "mobx-react";
|
||||
import React from "react-transition-group/node_modules/@types/react";
|
||||
import { activate } from "../../../common/cluster-ipc";
|
||||
|
||||
interface Props {
|
||||
className?: IClassName;
|
||||
@ -68,7 +67,7 @@ export class ClusterStatus extends React.Component<Props> {
|
||||
}
|
||||
|
||||
activateCluster = async (force = false) => {
|
||||
await requestMain(clusterActivateHandler, this.props.clusterId, force);
|
||||
await requestMain(activate, this.props.clusterId, force);
|
||||
};
|
||||
|
||||
reconnect = async () => {
|
||||
|
||||
@ -20,6 +20,7 @@
|
||||
*/
|
||||
|
||||
import "./cluster-view.scss";
|
||||
|
||||
import React from "react";
|
||||
import { reaction } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
@ -29,10 +30,10 @@ import { hasLoadedView, initView, lensViews, refreshViews } from "./lens-views";
|
||||
import type { Cluster } from "../../../main/cluster";
|
||||
import { ClusterStore } from "../../../common/cluster-store";
|
||||
import { requestMain } from "../../../common/ipc";
|
||||
import { clusterActivateHandler } from "../../../common/cluster-ipc";
|
||||
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||
import { navigate } from "../../navigation";
|
||||
import { catalogURL, ClusterViewRouteParams } from "../../../common/routes";
|
||||
import { activate } from "../../../common/cluster-ipc";
|
||||
import { CatalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||
|
||||
interface Props extends RouteComponentProps<ClusterViewRouteParams> {
|
||||
}
|
||||
@ -69,20 +70,20 @@ export class ClusterView extends React.Component<Props> {
|
||||
|
||||
showCluster(clusterId: string) {
|
||||
initView(clusterId);
|
||||
requestMain(clusterActivateHandler, this.clusterId, false);
|
||||
requestMain(activate, this.clusterId, false);
|
||||
|
||||
const entity = catalogEntityRegistry.getById(this.clusterId);
|
||||
const entity = CatalogEntityRegistry.getInstance().getById(this.clusterId);
|
||||
|
||||
if (entity) {
|
||||
catalogEntityRegistry.activeEntity = entity;
|
||||
CatalogEntityRegistry.getInstance().activeEntity = entity;
|
||||
}
|
||||
}
|
||||
|
||||
hideCluster() {
|
||||
refreshViews();
|
||||
|
||||
if (catalogEntityRegistry.activeEntity?.metadata?.uid === this.clusterId) {
|
||||
catalogEntityRegistry.activeEntity = null;
|
||||
if (CatalogEntityRegistry.getInstance().activeEntity?.metadata?.uid === this.clusterId) {
|
||||
CatalogEntityRegistry.getInstance().activeEntity = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -25,13 +25,12 @@ import React, { DOMAttributes, useState } from "react";
|
||||
import { Avatar } from "@material-ui/core";
|
||||
import randomColor from "randomcolor";
|
||||
import GraphemeSplitter from "grapheme-splitter";
|
||||
|
||||
import type { CatalogEntityContextMenu } from "../../../common/catalog";
|
||||
import { cssNames, IClassName, iter } from "../../utils";
|
||||
import { ConfirmDialog } from "../confirm-dialog";
|
||||
import { Menu, MenuItem } from "../menu";
|
||||
import { MaterialTooltip } from "../+catalog/material-tooltip/material-tooltip";
|
||||
import { observer } from "mobx-react";
|
||||
import type { ContextMenu } from "../../api/catalog-entity";
|
||||
|
||||
interface Props extends DOMAttributes<HTMLElement> {
|
||||
uid: string;
|
||||
@ -40,7 +39,7 @@ interface Props extends DOMAttributes<HTMLElement> {
|
||||
onMenuOpen?: () => void;
|
||||
className?: IClassName;
|
||||
active?: boolean;
|
||||
menuItems?: CatalogEntityContextMenu[];
|
||||
menuItems?: ContextMenu[];
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
@ -50,7 +49,7 @@ function generateAvatarStyle(seed: string): React.CSSProperties {
|
||||
};
|
||||
}
|
||||
|
||||
function onMenuItemClick(menuItem: CatalogEntityContextMenu) {
|
||||
function onMenuItemClick(menuItem: ContextMenu) {
|
||||
if (menuItem.confirm) {
|
||||
ConfirmDialog.open({
|
||||
okButtonProps: {
|
||||
|
||||
@ -25,7 +25,7 @@ import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { HotbarEntityIcon } from "./hotbar-entity-icon";
|
||||
import { cssNames, IClassName } from "../../utils";
|
||||
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||
import { CatalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||
import { defaultHotbarCells, HotbarItem, HotbarStore } from "../../../common/hotbar-store";
|
||||
import { CatalogEntity, catalogEntityRunContext } from "../../api/catalog-entity";
|
||||
import { DragDropContext, Draggable, Droppable, DropResult } from "react-beautiful-dnd";
|
||||
@ -44,6 +44,10 @@ export class HotbarMenu extends React.Component<Props> {
|
||||
return HotbarStore.getInstance().getActive();
|
||||
}
|
||||
|
||||
isActive(item: CatalogEntity) {
|
||||
return CatalogEntityRegistry.getInstance().activeEntity?.metadata?.uid == item.getId();
|
||||
}
|
||||
|
||||
getEntity(item: HotbarItem) {
|
||||
const hotbar = HotbarStore.getInstance().getActive();
|
||||
|
||||
@ -51,7 +55,7 @@ export class HotbarMenu extends React.Component<Props> {
|
||||
return null;
|
||||
}
|
||||
|
||||
return item ? catalogEntityRegistry.items.find((entity) => entity.metadata.uid === item.entity.uid) : null;
|
||||
return item ? CatalogEntityRegistry.getInstance().items.find((entity) => entity.metadata.uid === item.entity.uid) : null;
|
||||
}
|
||||
|
||||
onDragEnd(result: DropResult) {
|
||||
@ -91,7 +95,7 @@ export class HotbarMenu extends React.Component<Props> {
|
||||
|
||||
@computed get items() {
|
||||
const items = this.hotbar.items;
|
||||
const activeEntity = catalogEntityRegistry.activeEntity;
|
||||
const activeEntity = CatalogEntityRegistry.getInstance().activeEntity;
|
||||
|
||||
if (!activeEntity) return items;
|
||||
|
||||
|
||||
@ -22,8 +22,8 @@
|
||||
import "./menu-actions.scss";
|
||||
|
||||
import React, { isValidElement } from "react";
|
||||
import { observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { observable, reaction } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { autobind, cssNames } from "../../utils";
|
||||
import { ConfirmDialog } from "../confirm-dialog";
|
||||
import { Icon, IconProps } from "../icon";
|
||||
@ -40,6 +40,7 @@ export interface MenuActionsProps extends Partial<MenuProps> {
|
||||
updateAction?(): void;
|
||||
removeAction?(): void;
|
||||
onOpen?(): void;
|
||||
onClose?(): void;
|
||||
}
|
||||
|
||||
@observer
|
||||
@ -54,6 +55,12 @@ export class MenuActions extends React.Component<MenuActionsProps> {
|
||||
|
||||
@observable isOpen = !!this.props.toolbar;
|
||||
|
||||
componentDidMount() {
|
||||
disposeOnUnmount(this, [
|
||||
reaction(() => !this.isOpen, () => this.props.onClose?.()),
|
||||
]);
|
||||
}
|
||||
|
||||
toggle = () => {
|
||||
if (this.props.toolbar) return;
|
||||
this.isOpen = !this.isOpen;
|
||||
|
||||
@ -35,14 +35,14 @@ import { LensProtocolRouterRenderer, bindProtocolAddRouteHandlers } from "./prot
|
||||
import { registerIpcHandlers } from "./ipc";
|
||||
import { ipcRenderer } from "electron";
|
||||
import { IpcRendererNavigationEvents } from "./navigation/events";
|
||||
import { catalogEntityRegistry } from "./api/catalog-entity-registry";
|
||||
import { CommandRegistry } from "../extensions/registries";
|
||||
import { CatalogEntityRegistry } from "./api/catalog-entity-registry";
|
||||
import { reaction } from "mobx";
|
||||
|
||||
@observer
|
||||
export class LensApp extends React.Component {
|
||||
static async init() {
|
||||
catalogEntityRegistry.init();
|
||||
CatalogEntityRegistry.createInstance().init();
|
||||
ExtensionLoader.getInstance().loadOnClusterManagerRenderer();
|
||||
LensProtocolRouterRenderer.createInstance().init();
|
||||
bindProtocolAddRouteHandlers();
|
||||
@ -55,7 +55,7 @@ export class LensApp extends React.Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
reaction(() => catalogEntityRegistry.items, (items) => {
|
||||
reaction(() => CatalogEntityRegistry.getInstance().items, (items) => {
|
||||
const reg = CommandRegistry.getInstance();
|
||||
|
||||
if (reg.activeEntity && items.includes(reg.activeEntity)) {
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
import { attemptInstallByInfo } from "../components/+extensions";
|
||||
import { LensProtocolRouterRenderer } from "./router";
|
||||
import { navigate } from "../navigation/helpers";
|
||||
import { catalogEntityRegistry } from "../api/catalog-entity-registry";
|
||||
import { CatalogEntityRegistry } from "../api/catalog-entity-registry";
|
||||
import { ClusterStore } from "../../common/cluster-store";
|
||||
import { EXTENSION_NAME_MATCH, EXTENSION_PUBLISHER_MATCH, LensProtocolRouter } from "../../common/protocol-handler";
|
||||
import * as routes from "../../common/routes";
|
||||
@ -43,7 +43,7 @@ export function bindProtocolAddRouteHandlers() {
|
||||
navigate(routes.addClusterURL());
|
||||
})
|
||||
.addInternalHandler("/entity/:entityId/settings", ({ pathname: { entityId } }) => {
|
||||
const entity = catalogEntityRegistry.getById(entityId);
|
||||
const entity = CatalogEntityRegistry.getInstance().getById(entityId);
|
||||
|
||||
if (entity) {
|
||||
navigate(routes.entitySettingsURL({ params: { entityId } }));
|
||||
|
||||
@ -51,6 +51,9 @@ export function createStorage<T>(key: string, defaultValue: T, observableOptions
|
||||
|
||||
// bind auto-saving
|
||||
reaction(() => storage.toJSON(), saveFile, { delay: 250 });
|
||||
|
||||
// We don't clean up the cluster because it might come back in the future
|
||||
// as cluster IDs are now deterministic based on "path" + "contextName"
|
||||
}
|
||||
|
||||
async function saveFile(json = {}) {
|
||||
|
||||
@ -117,6 +117,9 @@ export default function generateExtensionTypes(): webpack.Configuration {
|
||||
extensions: [".ts", ".tsx", ".js"]
|
||||
},
|
||||
plugins: [
|
||||
new CircularDependencyPlugin({
|
||||
exclude: /node_modules/,
|
||||
}),
|
||||
// In ts-loader's README they said to output a built .d.ts file,
|
||||
// you can set "declaration": true in tsconfig.extensions.json,
|
||||
// and use the DeclarationBundlerPlugin in your webpack config... but
|
||||
|
||||
24
yarn.lock
24
yarn.lock
@ -1644,20 +1644,13 @@
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react@*":
|
||||
version "16.9.35"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.35.tgz#a0830d172e8aadd9bd41709ba2281a3124bbd368"
|
||||
integrity sha512-q0n0SsWcGc8nDqH2GJfWQWUOmZSJhXV64CjVN5SvcNti3TdEaA3AH0D8DwNmMdzjMAC/78tB8nAZIlV8yTz+zQ==
|
||||
dependencies:
|
||||
"@types/prop-types" "*"
|
||||
csstype "^2.2.0"
|
||||
|
||||
"@types/react@^17.0.0":
|
||||
version "17.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.0.tgz#5af3eb7fad2807092f0046a1302b7823e27919b8"
|
||||
integrity sha512-aj/L7RIMsRlWML3YB6KZiXB3fV2t41+5RBGYF8z+tAKU43Px8C3cYUZsDvf1/+Bm4FK21QWBrDutu8ZJ/70qOw==
|
||||
"@types/react@*", "@types/react@^17.0.0":
|
||||
version "17.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.5.tgz#3d887570c4489011f75a3fc8f965bf87d09a1bea"
|
||||
integrity sha512-bj4biDB9ZJmGAYTWSKJly6bMr4BLUiBrx9ujiJEoP9XIDY9CTaPGxE5QWN/1WjpPLzYF7/jRNnV2nNxNe970sw==
|
||||
dependencies:
|
||||
"@types/prop-types" "*"
|
||||
"@types/scheduler" "*"
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/readable-stream@^2.3.9":
|
||||
@ -1695,6 +1688,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d"
|
||||
integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==
|
||||
|
||||
"@types/scheduler@*":
|
||||
version "0.16.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275"
|
||||
integrity sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA==
|
||||
|
||||
"@types/semver@^7.2.0":
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.2.0.tgz#0d72066965e910531e1db4621c15d0ca36b8d83b"
|
||||
@ -4355,7 +4353,7 @@ cssstyle@^2.2.0:
|
||||
dependencies:
|
||||
cssom "~0.3.6"
|
||||
|
||||
csstype@^2.2.0, csstype@^2.5.2, csstype@^2.5.7, csstype@^2.6.5, csstype@^2.6.7:
|
||||
csstype@^2.5.2, csstype@^2.5.7, csstype@^2.6.5, csstype@^2.6.7:
|
||||
version "2.6.10"
|
||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.10.tgz#e63af50e66d7c266edb6b32909cfd0aabe03928b"
|
||||
integrity sha512-D34BqZU4cIlMCY93rZHbrq9pjTAQJ3U8S8rfBqjwHxkGPThWFjzZDQpgMJY0QViLxth6ZKYiwFBo14RdN44U/w==
|
||||
|
||||
Loading…
Reference in New Issue
Block a user