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