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

Split Catalog Categories and Registries

- Main and Renderer now have different types
- No longer have unified class declarations
- Move towards a computed model on main, with the CatalogEntityRegistry
  folding over the declared handlers

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

finish design work, still doesn't compile

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2021-05-20 18:20:52 -04:00
parent f3b3d15e50
commit 4d05eff051
82 changed files with 1767 additions and 1196 deletions

View File

@ -23,7 +23,7 @@ import fs from "fs";
import mockFs from "mock-fs";
import yaml from "js-yaml";
import { Cluster } from "../../main/cluster";
import { ClusterStore, getClusterIdFromHost } from "../cluster-store";
import { ClusterPreferencesStore, getClusterIdFromHost } from "../cluster-store";
import { Console } from "console";
import { stdout, stderr } from "process";
@ -74,8 +74,8 @@ jest.mock("electron", () => {
describe("empty config", () => {
beforeEach(async () => {
ClusterStore.getInstance(false)?.unregisterIpcListener();
ClusterStore.resetInstance();
ClusterPreferencesStore.getInstance(false)?.unregisterIpcListener();
ClusterPreferencesStore.resetInstance();
const mockOpts = {
"tmp": {
"lens-cluster-store.json": JSON.stringify({})
@ -84,7 +84,7 @@ describe("empty config", () => {
mockFs(mockOpts);
await ClusterStore.createInstance().load();
await ClusterPreferencesStore.createInstance().load();
});
afterEach(() => {
@ -93,7 +93,7 @@ describe("empty config", () => {
describe("with foo cluster added", () => {
beforeEach(() => {
ClusterStore.getInstance().addCluster(
ClusterPreferencesStore.getInstance().addCluster(
new Cluster({
id: "foo",
contextName: "foo",
@ -102,13 +102,13 @@ describe("empty config", () => {
icon: "data:image/jpeg;base64, iVBORw0KGgoAAAANSUhEUgAAA1wAAAKoCAYAAABjkf5",
clusterName: "minikube"
},
kubeConfigPath: ClusterStore.embedCustomKubeConfig("foo", kubeconfig)
kubeConfigPath: ClusterPreferencesStore.embedCustomKubeConfig("foo", kubeconfig)
})
);
});
it("adds new cluster to store", async () => {
const storedCluster = ClusterStore.getInstance().getById("foo");
const storedCluster = ClusterPreferencesStore.getInstance().getById("foo");
expect(storedCluster.id).toBe("foo");
expect(storedCluster.preferences.terminalCWD).toBe("/tmp");
@ -116,26 +116,26 @@ describe("empty config", () => {
});
it("removes cluster from store", async () => {
await ClusterStore.getInstance().removeById("foo");
expect(ClusterStore.getInstance().getById("foo")).toBeNull();
await ClusterPreferencesStore.getInstance().removeById("foo");
expect(ClusterPreferencesStore.getInstance().getById("foo")).toBeNull();
});
it("sets active cluster", () => {
ClusterStore.getInstance().setActive("foo");
expect(ClusterStore.getInstance().active.id).toBe("foo");
ClusterPreferencesStore.getInstance().setActive("foo");
expect(ClusterPreferencesStore.getInstance().active.id).toBe("foo");
});
});
describe("with prod and dev clusters added", () => {
beforeEach(() => {
ClusterStore.getInstance().addClusters(
ClusterPreferencesStore.getInstance().addClusters(
new Cluster({
id: "prod",
contextName: "foo",
preferences: {
clusterName: "prod"
},
kubeConfigPath: ClusterStore.embedCustomKubeConfig("prod", kubeconfig)
kubeConfigPath: ClusterPreferencesStore.embedCustomKubeConfig("prod", kubeconfig)
}),
new Cluster({
id: "dev",
@ -143,18 +143,18 @@ describe("empty config", () => {
preferences: {
clusterName: "dev"
},
kubeConfigPath: ClusterStore.embedCustomKubeConfig("dev", kubeconfig)
kubeConfigPath: ClusterPreferencesStore.embedCustomKubeConfig("dev", kubeconfig)
})
);
});
it("check if store can contain multiple clusters", () => {
expect(ClusterStore.getInstance().hasClusters()).toBeTruthy();
expect(ClusterStore.getInstance().clusters.size).toBe(2);
expect(ClusterPreferencesStore.getInstance().hasClusters()).toBeTruthy();
expect(ClusterPreferencesStore.getInstance().clusters.size).toBe(2);
});
it("check if cluster's kubeconfig file saved", () => {
const file = ClusterStore.embedCustomKubeConfig("boo", "kubeconfig");
const file = ClusterPreferencesStore.embedCustomKubeConfig("boo", "kubeconfig");
expect(fs.readFileSync(file, "utf8")).toBe("kubeconfig");
});
@ -163,7 +163,7 @@ describe("empty config", () => {
describe("config with existing clusters", () => {
beforeEach(() => {
ClusterStore.resetInstance();
ClusterPreferencesStore.resetInstance();
const mockOpts = {
"tmp": {
"lens-cluster-store.json": JSON.stringify({
@ -201,7 +201,7 @@ describe("config with existing clusters", () => {
mockFs(mockOpts);
return ClusterStore.createInstance().load();
return ClusterPreferencesStore.createInstance().load();
});
afterEach(() => {
@ -209,24 +209,24 @@ describe("config with existing clusters", () => {
});
it("allows to retrieve a cluster", () => {
const storedCluster = ClusterStore.getInstance().getById("cluster1");
const storedCluster = ClusterPreferencesStore.getInstance().getById("cluster1");
expect(storedCluster.id).toBe("cluster1");
expect(storedCluster.preferences.terminalCWD).toBe("/foo");
});
it("allows to delete a cluster", () => {
ClusterStore.getInstance().removeById("cluster2");
const storedCluster = ClusterStore.getInstance().getById("cluster1");
ClusterPreferencesStore.getInstance().removeById("cluster2");
const storedCluster = ClusterPreferencesStore.getInstance().getById("cluster1");
expect(storedCluster).toBeTruthy();
const storedCluster2 = ClusterStore.getInstance().getById("cluster2");
const storedCluster2 = ClusterPreferencesStore.getInstance().getById("cluster2");
expect(storedCluster2).toBeNull();
});
it("allows getting all of the clusters", async () => {
const storedClusters = ClusterStore.getInstance().clustersList;
const storedClusters = ClusterPreferencesStore.getInstance().clustersList;
expect(storedClusters.length).toBe(3);
expect(storedClusters[0].id).toBe("cluster1");
@ -259,7 +259,7 @@ users:
token: kubeconfig-user-q4lm4:xxxyyyy
`;
ClusterStore.resetInstance();
ClusterPreferencesStore.resetInstance();
const mockOpts = {
"tmp": {
"lens-cluster-store.json": JSON.stringify({
@ -291,7 +291,7 @@ users:
mockFs(mockOpts);
return ClusterStore.createInstance().load();
return ClusterPreferencesStore.createInstance().load();
});
afterEach(() => {
@ -299,7 +299,7 @@ users:
});
it("does not enable clusters with invalid kubeconfig", () => {
const storedClusters = ClusterStore.getInstance().clustersList;
const storedClusters = ClusterPreferencesStore.getInstance().clustersList;
expect(storedClusters.length).toBe(1);
});
@ -334,7 +334,7 @@ const minimalValidKubeConfig = JSON.stringify({
describe("pre 2.0 config with an existing cluster", () => {
beforeEach(() => {
ClusterStore.resetInstance();
ClusterPreferencesStore.resetInstance();
const mockOpts = {
"tmp": {
"lens-cluster-store.json": JSON.stringify({
@ -350,7 +350,7 @@ describe("pre 2.0 config with an existing cluster", () => {
mockFs(mockOpts);
return ClusterStore.createInstance().load();
return ClusterPreferencesStore.createInstance().load();
});
afterEach(() => {
@ -358,7 +358,7 @@ describe("pre 2.0 config with an existing cluster", () => {
});
it("migrates to modern format with kubeconfig in a file", async () => {
const config = ClusterStore.getInstance().clustersList[0].kubeConfigPath;
const config = ClusterPreferencesStore.getInstance().clustersList[0].kubeConfigPath;
expect(fs.readFileSync(config, "utf8")).toContain(`"contexts":[`);
});
@ -366,7 +366,7 @@ describe("pre 2.0 config with an existing cluster", () => {
describe("pre 2.6.0 config with a cluster that has arrays in auth config", () => {
beforeEach(() => {
ClusterStore.resetInstance();
ClusterPreferencesStore.resetInstance();
const mockOpts = {
"tmp": {
"lens-cluster-store.json": JSON.stringify({
@ -420,7 +420,7 @@ describe("pre 2.6.0 config with a cluster that has arrays in auth config", () =>
mockFs(mockOpts);
return ClusterStore.createInstance().load();
return ClusterPreferencesStore.createInstance().load();
});
afterEach(() => {
@ -428,7 +428,7 @@ describe("pre 2.6.0 config with a cluster that has arrays in auth config", () =>
});
it("replaces array format access token and expiry into string", async () => {
const file = ClusterStore.getInstance().clustersList[0].kubeConfigPath;
const file = ClusterPreferencesStore.getInstance().clustersList[0].kubeConfigPath;
const config = fs.readFileSync(file, "utf8");
const kc = yaml.safeLoad(config);
@ -439,7 +439,7 @@ describe("pre 2.6.0 config with a cluster that has arrays in auth config", () =>
describe("pre 2.6.0 config with a cluster icon", () => {
beforeEach(() => {
ClusterStore.resetInstance();
ClusterPreferencesStore.resetInstance();
const mockOpts = {
"tmp": {
"lens-cluster-store.json": JSON.stringify({
@ -462,7 +462,7 @@ describe("pre 2.6.0 config with a cluster icon", () => {
mockFs(mockOpts);
return ClusterStore.createInstance().load();
return ClusterPreferencesStore.createInstance().load();
});
afterEach(() => {
@ -470,7 +470,7 @@ describe("pre 2.6.0 config with a cluster icon", () => {
});
it("moves the icon into preferences", async () => {
const storedClusterData = ClusterStore.getInstance().clustersList[0];
const storedClusterData = ClusterPreferencesStore.getInstance().clustersList[0];
expect(storedClusterData.hasOwnProperty("icon")).toBe(false);
expect(storedClusterData.preferences.hasOwnProperty("icon")).toBe(true);
@ -480,7 +480,7 @@ describe("pre 2.6.0 config with a cluster icon", () => {
describe("for a pre 2.7.0-beta.0 config without a workspace", () => {
beforeEach(() => {
ClusterStore.resetInstance();
ClusterPreferencesStore.resetInstance();
const mockOpts = {
"tmp": {
"lens-cluster-store.json": JSON.stringify({
@ -501,7 +501,7 @@ describe("for a pre 2.7.0-beta.0 config without a workspace", () => {
mockFs(mockOpts);
return ClusterStore.createInstance().load();
return ClusterPreferencesStore.createInstance().load();
});
afterEach(() => {
@ -511,7 +511,7 @@ describe("for a pre 2.7.0-beta.0 config without a workspace", () => {
describe("pre 3.6.0-beta.1 config with an existing cluster", () => {
beforeEach(() => {
ClusterStore.resetInstance();
ClusterPreferencesStore.resetInstance();
const mockOpts = {
"tmp": {
"lens-cluster-store.json": JSON.stringify({
@ -537,7 +537,7 @@ describe("pre 3.6.0-beta.1 config with an existing cluster", () => {
mockFs(mockOpts);
return ClusterStore.createInstance().load();
return ClusterPreferencesStore.createInstance().load();
});
afterEach(() => {
@ -545,13 +545,13 @@ describe("pre 3.6.0-beta.1 config with an existing cluster", () => {
});
it("migrates to modern format with kubeconfig in a file", async () => {
const config = ClusterStore.getInstance().clustersList[0].kubeConfigPath;
const config = ClusterPreferencesStore.getInstance().clustersList[0].kubeConfigPath;
expect(fs.readFileSync(config, "utf8")).toBe(minimalValidKubeConfig);
});
it("migrates to modern format with icon not in file", async () => {
const { icon } = ClusterStore.getInstance().clustersList[0].preferences;
const { icon } = ClusterPreferencesStore.getInstance().clustersList[0].preferences;
expect(icon.startsWith("data:;base64,")).toBe(true);
});

View File

@ -20,7 +20,7 @@
*/
import mockFs from "mock-fs";
import { ClusterStore } from "../cluster-store";
import { ClusterPreferencesStore } from "../cluster-store";
import { HotbarStore } from "../hotbar-store";
jest.mock("../../renderer/api/catalog-entity-registry", () => ({
@ -45,7 +45,7 @@ jest.mock("../../renderer/api/catalog-entity-registry", () => ({
}));
const testCluster = {
uid: "test",
id: "test",
name: "test",
apiVersion: "v1",
kind: "Cluster",
@ -53,11 +53,6 @@ const testCluster = {
phase: "Running"
},
spec: {},
getName: jest.fn(),
getId: jest.fn(),
onDetailsOpen: jest.fn(),
onContextMenuOpen: jest.fn(),
onSettingsOpen: jest.fn(),
metadata: {
uid: "test",
name: "test",
@ -66,7 +61,7 @@ const testCluster = {
};
const minikubeCluster = {
uid: "minikube",
id: "minikube",
name: "minikube",
apiVersion: "v1",
kind: "Cluster",
@ -74,11 +69,6 @@ const minikubeCluster = {
phase: "Running"
},
spec: {},
getName: jest.fn(),
getId: jest.fn(),
onDetailsOpen: jest.fn(),
onContextMenuOpen: jest.fn(),
onSettingsOpen: jest.fn(),
metadata: {
uid: "minikube",
name: "minikube",
@ -87,7 +77,7 @@ const minikubeCluster = {
};
const awsCluster = {
uid: "aws",
id: "aws",
name: "aws",
apiVersion: "v1",
kind: "Cluster",
@ -95,11 +85,6 @@ const awsCluster = {
phase: "Running"
},
spec: {},
getName: jest.fn(),
getId: jest.fn(),
onDetailsOpen: jest.fn(),
onContextMenuOpen: jest.fn(),
onSettingsOpen: jest.fn(),
metadata: {
uid: "aws",
name: "aws",
@ -120,8 +105,8 @@ jest.mock("electron", () => {
describe("HotbarStore", () => {
beforeEach(() => {
ClusterStore.resetInstance();
ClusterStore.createInstance();
ClusterPreferencesStore.resetInstance();
ClusterPreferencesStore.createInstance();
HotbarStore.resetInstance();
mockFs({ tmp: { "lens-hotbar-store.json": "{}" } });

View File

@ -19,15 +19,8 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
import { CatalogEntity, CatalogEntityActionContext, CatalogEntityAddMenuContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog";
import { clusterActivateHandler, clusterDisconnectHandler } from "../cluster-ipc";
import { ClusterStore } from "../cluster-store";
import { requestMain } from "../ipc";
import { productName } from "../vars";
import { CatalogCategory, CatalogCategorySpec } from "../catalog";
import { app } from "electron";
import type { CatalogEntityStatus } from "../catalog";
import type { CatalogEntitySpec } from "../catalog/catalog-entity";
export type KubernetesClusterPrometheusMetrics = {
address?: {
@ -39,143 +32,15 @@ export type KubernetesClusterPrometheusMetrics = {
type?: string;
};
export type KubernetesClusterSpec = {
export interface KubernetesClusterSpec extends CatalogEntitySpec {
kubeconfigPath: string;
kubeconfigContext: string;
metrics?: {
source: string;
prometheus?: KubernetesClusterPrometheusMetrics;
}
};
}
export interface KubernetesClusterStatus extends CatalogEntityStatus {
phase: "connected" | "disconnected";
}
export class KubernetesCluster extends CatalogEntity<CatalogEntityMetadata, KubernetesClusterStatus, KubernetesClusterSpec> {
public readonly apiVersion = "entity.k8slens.dev/v1alpha1";
public readonly kind = "KubernetesCluster";
async connect(): Promise<void> {
if (app) {
const cluster = ClusterStore.getInstance().getById(this.metadata.uid);
if (!cluster) return;
await cluster.activate();
return;
}
await requestMain(clusterActivateHandler, this.metadata.uid, false);
return;
}
async disconnect(): Promise<void> {
if (app) {
const cluster = ClusterStore.getInstance().getById(this.metadata.uid);
if (!cluster) return;
cluster.disconnect();
return;
}
await requestMain(clusterDisconnectHandler, this.metadata.uid, false);
return;
}
async onRun(context: CatalogEntityActionContext) {
context.navigate(`/cluster/${this.metadata.uid}`);
}
onDetailsOpen(): void {
//
}
onSettingsOpen(): void {
//
}
async onContextMenuOpen(context: CatalogEntityContextMenuContext) {
context.menuItems = [
{
title: "Settings",
onlyVisibleForSource: "local",
onClick: async () => context.navigate(`/entity/${this.metadata.uid}/settings`)
},
];
if (this.metadata.labels["file"]?.startsWith(ClusterStore.storedKubeConfigFolder)) {
context.menuItems.push({
title: "Delete",
onlyVisibleForSource: "local",
onClick: async () => ClusterStore.getInstance().removeById(this.metadata.uid),
confirm: {
message: `Remove Kubernetes Cluster "${this.metadata.name} from ${productName}?`
}
});
}
if (this.status.phase == "connected") {
context.menuItems.push({
title: "Disconnect",
onClick: async () => {
ClusterStore.getInstance().deactivate(this.metadata.uid);
requestMain(clusterDisconnectHandler, this.metadata.uid);
}
});
} else {
context.menuItems.push({
title: "Connect",
onClick: async () => {
context.navigate(`/cluster/${this.metadata.uid}`);
}
});
}
const category = catalogCategoryRegistry.getCategoryForEntity<KubernetesClusterCategory>(this);
if (category) category.emit("contextMenuOpen", this, context);
}
}
export class KubernetesClusterCategory extends CatalogCategory {
public readonly apiVersion = "catalog.k8slens.dev/v1alpha1";
public readonly kind = "CatalogCategory";
public metadata = {
name: "Kubernetes Clusters",
icon: require(`!!raw-loader!./icons/kubernetes.svg`).default // eslint-disable-line
};
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("/add-cluster");
}
});
});
}
}
catalogCategoryRegistry.add(new KubernetesClusterCategory());

View File

@ -19,57 +19,12 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { CatalogCategory, CatalogEntity, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog";
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
import type { CatalogEntitySpec, CatalogEntityStatus } from "../catalog";
export interface WebLinkStatus extends CatalogEntityStatus {
phase: "valid" | "invalid";
}
export type WebLinkSpec = {
export interface WebLinkSpec extends CatalogEntitySpec {
url: string;
};
export class WebLink extends CatalogEntity<CatalogEntityMetadata, WebLinkStatus, WebLinkSpec> {
public readonly apiVersion = "entity.k8slens.dev/v1alpha1";
public readonly kind = "WebLink";
async onRun() {
window.open(this.spec.url, "_blank");
}
public onSettingsOpen(): void {
return;
}
public onDetailsOpen(): void {
return;
}
public onContextMenuOpen(): void {
return;
}
}
export class WebLinkCategory extends CatalogCategory {
public readonly apiVersion = "catalog.k8slens.dev/v1alpha1";
public readonly kind = "CatalogCategory";
public metadata = {
name: "Web Links",
icon: "link"
};
public spec = {
group: "entity.k8slens.dev",
versions: [
{
name: "v1alpha1",
entityClass: WebLink
}
],
names: {
kind: "WebLink"
}
};
}
catalogCategoryRegistry.add(new WebLinkCategory());

View File

@ -19,67 +19,103 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { action, computed, observable, makeObservable } from "mobx";
import { Disposer, ExtendedMap } from "../utils";
import { CatalogCategory, CatalogEntityData, CatalogEntityKindData } from "./catalog-entity";
import { action, computed } from "mobx";
import type { CatalogEntity } from "../../main/catalog";
import { Disposer, disposer, ExtendedObservableMap, iter, Singleton } from "../utils";
import { CatalogCategoryRegistration as CommonCatalogCategoryRegistration, CatalogCategorySpecVersion, CategoryMetadata, parseApiVersion, WithId } from "./catalog-entity";
import util from "util";
import { once } from "lodash";
export class CatalogCategoryRegistry {
protected categories = observable.set<CatalogCategory>();
protected groupKinds = new ExtendedMap<string, ExtendedMap<string, CatalogCategory>>();
const validApiVersions = new Map<string, Set<string>>(
[
["catalog.k8slens.dev", new Set("v1alpha1")]
],
);
constructor() {
makeObservable(this);
}
function getValidityList(items: Iterable<string>): string {
let res = "";
@action add(category: CatalogCategory): Disposer {
this.categories.add(category);
this.updateGroupKinds(category);
return () => {
this.categories.delete(category);
this.groupKinds.clear();
};
}
private updateGroupKinds(category: CatalogCategory) {
this.groupKinds
.getOrInsert(category.spec.group, ExtendedMap.new)
.strictSet(category.spec.names.kind, category);
}
@computed get items() {
return Array.from(this.categories);
}
getForGroupKind<T extends CatalogCategory>(group: string, kind: string): T | undefined {
return this.groupKinds.get(group)?.get(kind) as T;
}
getEntityForData(data: CatalogEntityData & CatalogEntityKindData) {
const category = this.getCategoryForEntity(data);
if (!category) {
return null;
for (const item of items) {
if (res.length) {
res += ", ";
}
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);
res += item;
}
getCategoryForEntity<T extends CatalogCategory>(data: CatalogEntityData & CatalogEntityKindData): T | undefined {
const splitApiVersion = data.apiVersion.split("/");
const group = splitApiVersion[0];
return res;
}
return this.getForGroupKind(group, data.kind);
const validGroupList = getValidityList(validApiVersions.keys());
function validateCatalogCategoryRegistration<CatalogCategoryRegistration extends CommonCatalogCategoryRegistration<CategoryMetadata, CatalogCategorySpecVersion>>(reg: CatalogCategoryRegistration): void {
const { group, version } = parseApiVersion(reg.apiVersion);
const validVersions = validApiVersions.get(group);
const fGroup = util.inspect(group, false, null, false);
const fVersion = util.inspect(version, false, null, false);
if (!validVersions) {
throw new TypeError(`Invalid group: ${fGroup}. Valid groups are: ${validGroupList}`);
}
if (!validVersions.has(version)) {
throw new TypeError(`Unsupported version: ${fVersion} for ${fGroup}. Valid versions are: ${getValidityList(validVersions)}`);
}
}
export const catalogCategoryRegistry = new CatalogCategoryRegistry();
export abstract class CatalogCategoryRegistry<
Registration extends CommonCatalogCategoryRegistration<CategoryMetadata, CatalogCategorySpecVersion>,
Registered extends Registration,
> extends Singleton {
/**
* This is a mapping based on the versions of Categories, see `./catalog-entity` for the validation
*/
protected groupVersionKinds = new ExtendedObservableMap<string, ExtendedObservableMap<string, ExtendedObservableMap<string, Registered & WithId>>>();
protected abstract register(registration: Registration): Registered;
@action add(registration: Registration): Disposer {
validateCatalogCategoryRegistration(registration);
return this.updateGroupKinds(this.register(registration));
}
private updateGroupKinds(category: Registered): Disposer {
const { group, versions, names: { kind } } = category.spec;
const groups = this.groupVersionKinds.getOrInsert(group, ExtendedObservableMap.new);
const cleanup = disposer();
for (const { version } of versions) {
const versioning = groups.getOrInsert(version, ExtendedObservableMap.new);
versioning.strictSet(kind, { ...category, id: `${group}/${kind}` });
cleanup.push(once(() => versioning.delete(kind)));
}
return cleanup;
}
@computed get items() {
return Array.from(iter.flatMap(this.groupVersionKinds.values(), groups => iter.flatMap(groups.values(), kinds => kinds.values())));
}
getForGroupKind(group: string, version: string, kind: string): Registration | undefined {
return this.groupVersionKinds.get(group)?.get(version)?.get(kind);
}
protected getRegistered(apiVersion: string, kind: string) {
const { group, version } = parseApiVersion(apiVersion);
return this.groupVersionKinds.get(group)?.get(version)?.get(kind);
}
hasForGroupKind(group: string, version: string, kind: string): boolean {
return Boolean(this.getForGroupKind(group, version, kind));
}
getCategoryForEntity(data: CatalogEntity): Registration | undefined {
const { group, version } = parseApiVersion(data.apiVersion);
return this.getForGroupKind(group, version, data.kind);
}
}

View File

@ -19,52 +19,83 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { EventEmitter } from "events";
import { observable, makeObservable } from "mobx";
import URLParse from "url-parse";
type ExtractEntityMetadataType<Entity> = Entity extends CatalogEntity<infer Metadata> ? Metadata : never;
type ExtractEntityStatusType<Entity> = Entity extends CatalogEntity<any, infer Status> ? Status : never;
type ExtractEntitySpecType<Entity> = Entity extends CatalogEntity<any, any, infer Spec> ? Spec : never;
export type CatalogEntityConstructor<Entity extends CatalogEntity> = (
(new (data: CatalogEntityData<
ExtractEntityMetadataType<Entity>,
ExtractEntityStatusType<Entity>,
ExtractEntitySpecType<Entity>
>) => Entity)
);
export interface CatalogCategoryVersion<Entity extends CatalogEntity> {
name: string;
entityClass: CatalogEntityConstructor<Entity>;
export interface ParsedApiVersion {
group: string;
version?: string;
}
export interface CatalogCategorySpec {
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,
};
}
export interface CatalogCategorySpecVersion {
version: string;
}
export interface CatalogCategorySpec<Version extends CatalogCategorySpecVersion> {
group: string;
versions: CatalogCategoryVersion<CatalogEntity>[];
versions: Version[];
names: {
kind: string;
};
}
export abstract class CatalogCategory extends EventEmitter {
abstract readonly apiVersion: string;
abstract readonly kind: string;
abstract metadata: {
name: string;
icon: string;
};
abstract spec: CatalogCategorySpec;
export interface CategoryMetadata {
name: string;
}
static parseId(id = ""): { group?: string, kind?: string } {
const [group, kind] = id.split("/") ?? [];
export interface CatalogCategoryRegistration<Metadata extends CategoryMetadata, SpecVersion extends CatalogCategorySpecVersion> {
readonly apiVersion: string;
readonly kind: string;
metadata: Metadata;
spec: CatalogCategorySpec<SpecVersion>;
}
return { group, kind };
}
public getId(): string {
return `${this.spec.group}/${this.spec.names.kind}`;
}
export interface WithId {
readonly id: string;
}
export interface CatalogEntityMetadata {
@ -83,92 +114,9 @@ export interface CatalogEntityStatus {
active?: boolean;
}
export interface CatalogEntityActionContext {
navigate: (url: string) => void;
setCommandPaletteContext: (context?: CatalogEntity) => void;
}
export interface CatalogEntityContextMenu {
title: string;
onlyVisibleForSource?: string; // show only if empty or if matches with entity source
onClick: () => void | Promise<void>;
confirm?: {
message: string;
}
}
export interface CatalogEntityAddMenu extends CatalogEntityContextMenu {
icon: string;
}
export interface CatalogEntitySettingsMenu {
group?: string;
title: string;
components: {
View: React.ComponentType<any>
};
}
export interface CatalogEntityContextMenuContext {
navigate: (url: string) => void;
menuItems: CatalogEntityContextMenu[];
}
export interface CatalogEntitySettingsContext {
menuItems: CatalogEntityContextMenu[];
}
export interface CatalogEntityAddMenuContext {
navigate: (url: string) => void;
menuItems: CatalogEntityAddMenu[];
}
export type CatalogEntitySpec = Record<string, any>;
export interface CatalogEntityData<
Metadata extends CatalogEntityMetadata = CatalogEntityMetadata,
Status extends CatalogEntityStatus = CatalogEntityStatus,
Spec extends CatalogEntitySpec = CatalogEntitySpec,
> {
metadata: Metadata;
status: Status;
spec: Spec;
}
export interface CatalogEntityKindData {
readonly apiVersion: string;
readonly kind: string;
}
export abstract class CatalogEntity<
Metadata extends CatalogEntityMetadata = CatalogEntityMetadata,
Status extends CatalogEntityStatus = CatalogEntityStatus,
Spec extends CatalogEntitySpec = CatalogEntitySpec,
> implements CatalogEntityKindData {
public abstract readonly apiVersion: string;
public abstract readonly kind: string;
@observable metadata: Metadata;
@observable status: Status;
@observable spec: Spec;
constructor(data: CatalogEntityData<Metadata, Status, Spec>) {
makeObservable(this);
this.metadata = data.metadata;
this.status = data.status;
this.spec = data.spec;
}
public getId(): string {
return this.metadata.uid;
}
public getName(): string {
return this.metadata.name;
}
public abstract onRun?(context: CatalogEntityActionContext): void | Promise<void>;
public abstract onDetailsOpen(context: CatalogEntityActionContext): void | Promise<void>;
public abstract onContextMenuOpen(context: CatalogEntityContextMenuContext): void | Promise<void>;
public abstract onSettingsOpen(context: CatalogEntitySettingsContext): void | Promise<void>;
}

View File

@ -19,5 +19,5 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
export * from "./catalog-category-registry";
export * from "./catalog-entity";
export * from "./catalog-category-registry";

View File

@ -20,11 +20,12 @@
*/
import { handleRequest } from "./ipc";
import { ClusterId, ClusterStore } from "./cluster-store";
import type { ClusterId } from "./cluster-store";
import { appEventBus } from "./event-bus";
import { ResourceApplier } from "../main/resource-applier";
import { ipcMain, IpcMainInvokeEvent } from "electron";
import { clusterFrameMap } from "./cluster-frames";
import { ClusterManager } from "../main/cluster-manager";
export const clusterActivateHandler = "cluster:activate";
export const clusterSetFrameIdHandler = "cluster:set-frame-id";
@ -35,13 +36,13 @@ export const clusterKubectlDeleteAllHandler = "cluster:kubectl-delete-all";
if (ipcMain) {
handleRequest(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => {
return ClusterStore.getInstance()
return ClusterManager.getInstance()
.getById(clusterId)
?.activate(force);
});
handleRequest(clusterSetFrameIdHandler, (event: IpcMainInvokeEvent, clusterId: ClusterId) => {
const cluster = ClusterStore.getInstance().getById(clusterId);
const cluster = ClusterManager.getInstance().getById(clusterId);
if (cluster) {
clusterFrameMap.set(cluster.id, { frameId: event.frameId, processId: event.processId });
@ -50,14 +51,14 @@ if (ipcMain) {
});
handleRequest(clusterRefreshHandler, (event, clusterId: ClusterId) => {
return ClusterStore.getInstance()
return ClusterManager.getInstance()
.getById(clusterId)
?.refresh({ refreshMetadata: true });
});
handleRequest(clusterDisconnectHandler, (event, clusterId: ClusterId) => {
appEventBus.emit({name: "cluster", action: "stop"});
const cluster = ClusterStore.getInstance().getById(clusterId);
const cluster = ClusterManager.getInstance().getById(clusterId);
if (cluster) {
cluster.disconnect();
@ -67,7 +68,7 @@ if (ipcMain) {
handleRequest(clusterKubectlApplyAllHandler, async (event, clusterId: ClusterId, resources: string[], extraArgs: string[]) => {
appEventBus.emit({name: "cluster", action: "kubectl-apply-all"});
const cluster = ClusterStore.getInstance().getById(clusterId);
const cluster = ClusterManager.getInstance().getById(clusterId);
if (cluster) {
const applier = new ResourceApplier(cluster);
@ -86,7 +87,7 @@ if (ipcMain) {
handleRequest(clusterKubectlDeleteAllHandler, async (event, clusterId: ClusterId, resources: string[], extraArgs: string[]) => {
appEventBus.emit({name: "cluster", action: "kubectl-delete-all"});
const cluster = ClusterStore.getInstance().getById(clusterId);
const cluster = ClusterManager.getInstance().getById(clusterId);
if (cluster) {
const applier = new ResourceApplier(cluster);

View File

@ -20,20 +20,15 @@
*/
import path from "path";
import { app, ipcMain, ipcRenderer, remote, webFrame } from "electron";
import { unlink } from "fs-extra";
import { action, comparer, computed, observable, reaction, makeObservable } from "mobx";
import { app, ipcRenderer, remote } from "electron";
import { action, comparer, observable, toJS } from "mobx";
import { BaseStore } from "./base-store";
import { Cluster, ClusterState } from "../main/cluster";
import migrations from "../migrations/cluster-store";
import logger from "../main/logger";
import { appEventBus } from "./event-bus";
import { dumpConfigYaml } from "./kube-helpers";
import { saveToAppFiles } from "./utils/saveToAppFiles";
import type { KubeConfig } from "@kubernetes/client-node";
import { handleRequest, requestMain, subscribeToBroadcast, unsubscribeAllFromBroadcast } from "./ipc";
import { disposer } from "./utils";
import type { ResourceType } from "../renderer/components/cluster-settings/components/cluster-metrics-setting";
import { disposer, noop, toJS } from "./utils";
export interface ClusterIconUpload {
clusterId: string;
@ -52,8 +47,7 @@ export type ClusterPrometheusMetadata = {
};
export interface ClusterStoreModel {
activeCluster?: ClusterId; // last opened cluster
clusters?: ClusterModel[];
preferences?: [string, ClusterPreferences][];
}
export type ClusterId = string;
@ -113,17 +107,17 @@ export interface ClusterPrometheusPreferences {
};
}
export class ClusterStore extends BaseStore<ClusterStoreModel> {
export class ClusterPreferencesStore extends BaseStore<ClusterStoreModel> {
static get storedKubeConfigFolder(): string {
return path.resolve((app || remote.app).getPath("userData"), "kubeconfigs");
}
static getCustomKubeConfigPath(clusterId: ClusterId): string {
return path.resolve(ClusterStore.storedKubeConfigFolder, clusterId);
return path.resolve(ClusterPreferencesStore.storedKubeConfigFolder, clusterId);
}
static embedCustomKubeConfig(clusterId: ClusterId, kubeConfig: KubeConfig | string): string {
const filePath = ClusterStore.getCustomKubeConfigPath(clusterId);
const filePath = ClusterPreferencesStore.getCustomKubeConfigPath(clusterId);
const fileContents = typeof kubeConfig == "string" ? kubeConfig : dumpConfigYaml(kubeConfig);
saveToAppFiles(filePath, fileContents, { mode: 0o600 });
@ -131,11 +125,8 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
return filePath;
}
@observable activeCluster: ClusterId;
@observable removedClusters = observable.map<ClusterId, Cluster>();
@observable clusters = observable.map<ClusterId, Cluster>();
clusterPreferences = observable.map<string, ClusterPreferences>();
private static stateRequestChannel = "cluster:states";
protected disposer = disposer();
constructor() {
@ -147,206 +138,32 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
},
migrations,
});
makeObservable(this);
this.pushStateToViewsAutomatically();
}
async load() {
await super.load();
type clusterStateSync = {
id: string;
state: ClusterState;
};
getById(id: string): ClusterPreferences {
return this.clusterPreferences.get(id);
}
isMetricHidden(resource: ResourceType): boolean {
if (ipcRenderer) {
logger.info("[CLUSTER-STORE] requesting initial state sync");
const clusterStates: clusterStateSync[] = await requestMain(ClusterStore.stateRequestChannel);
const id = getHostedClusterId();
clusterStates.forEach((clusterState) => {
const cluster = this.getById(clusterState.id);
if (cluster) {
cluster.setState(clusterState.state);
}
});
} else if (ipcMain) {
handleRequest(ClusterStore.stateRequestChannel, (): clusterStateSync[] => {
const clusterStates: clusterStateSync[] = [];
this.clustersList.forEach((cluster) => {
clusterStates.push({
state: cluster.getState(),
id: cluster.id
});
});
return clusterStates;
});
return Boolean(this.clusterPreferences.get(id).hiddenMetrics?.includes(resource));
}
}
protected pushStateToViewsAutomatically() {
if (ipcMain) {
this.disposer.push(
reaction(() => this.connectedClustersList, () => {
this.pushState();
}),
() => unsubscribeAllFromBroadcast("cluster:state"),
);
}
}
registerIpcListener() {
logger.info(`[CLUSTER-STORE] start to listen (${webFrame.routingId})`);
subscribeToBroadcast("cluster:state", (event, clusterId: string, state: ClusterState) => {
logger.silly(`[CLUSTER-STORE]: received push-state at ${location.host} (${webFrame.routingId})`, clusterId, state);
this.getById(clusterId)?.setState(state);
});
}
unregisterIpcListener() {
super.unregisterIpcListener();
this.disposer();
}
pushState() {
this.clusters.forEach((c) => {
c.pushState();
});
}
get activeClusterId() {
return this.activeCluster;
}
@computed get clustersList(): Cluster[] {
return Array.from(this.clusters.values());
}
@computed get active(): Cluster | null {
return this.getById(this.activeCluster);
}
@computed get connectedClustersList(): Cluster[] {
return this.clustersList.filter((c) => !c.disconnected);
}
isActive(id: ClusterId) {
return this.activeCluster === id;
}
isMetricHidden(resource: ResourceType) {
return Boolean(this.active?.preferences.hiddenMetrics?.includes(resource));
return true;
}
@action
setActive(clusterId: ClusterId) {
this.activeCluster = this.clusters.has(clusterId)
? clusterId
: null;
}
deactivate(id: ClusterId) {
if (this.isActive(id)) {
this.setActive(null);
}
}
hasClusters() {
return this.clusters.size > 0;
}
getById(id: ClusterId): Cluster | null {
return this.clusters.get(id) ?? null;
}
@action
addClusters(...models: ClusterModel[]): Cluster[] {
const clusters: Cluster[] = [];
models.forEach(model => {
clusters.push(this.addCluster(model));
});
return clusters;
}
@action
addCluster(clusterOrModel: ClusterModel | Cluster): Cluster {
appEventBus.emit({ name: "cluster", action: "add" });
const cluster = clusterOrModel instanceof Cluster
? clusterOrModel
: new Cluster(clusterOrModel);
this.clusters.set(cluster.id, cluster);
return cluster;
}
async removeCluster(model: ClusterModel) {
await this.removeById(model.id);
}
@action
async removeById(clusterId: ClusterId) {
appEventBus.emit({ name: "cluster", action: "remove" });
const cluster = this.getById(clusterId);
if (cluster) {
this.clusters.delete(clusterId);
if (this.activeCluster === clusterId) {
this.setActive(null);
}
// remove only custom kubeconfigs (pasted as text)
if (cluster.kubeConfigPath == ClusterStore.getCustomKubeConfigPath(clusterId)) {
await unlink(cluster.kubeConfigPath).catch(noop);
}
}
}
@action
protected fromStore({ activeCluster, clusters = [] }: ClusterStoreModel = {}) {
const currentClusters = new Map(this.clusters);
const newClusters = new Map<ClusterId, Cluster>();
const removedClusters = new Map<ClusterId, Cluster>();
// update new clusters
for (const clusterModel of clusters) {
try {
let cluster = currentClusters.get(clusterModel.id);
if (cluster) {
cluster.updateModel(clusterModel);
} else {
cluster = new Cluster(clusterModel);
}
newClusters.set(clusterModel.id, cluster);
} catch {
// ignore
}
}
// update removed clusters
currentClusters.forEach(cluster => {
if (!newClusters.has(cluster.id)) {
removedClusters.set(cluster.id, cluster);
}
});
this.setActive(activeCluster);
this.clusters.replace(newClusters);
this.removedClusters.replace(removedClusters);
protected fromStore({ preferences = [] }: ClusterStoreModel = {}) {
this.clusterPreferences.replace(preferences);
}
toJSON(): ClusterStoreModel {
return toJS({
activeCluster: this.activeCluster,
clusters: this.clustersList.map(cluster => cluster.toJSON()),
preferences: Array.from(this.clusterPreferences.entries()),
}, {
recurseEverything: true
});
}
}
@ -366,6 +183,6 @@ export function getHostedClusterId() {
return getClusterIdFromHost(location.host);
}
export function getHostedCluster(): Cluster {
return ClusterStore.getInstance().getById(getHostedClusterId());
export function getHostedCluster() {
return ClusterPreferencesStore.getInstance().getById(getHostedClusterId());
}

View File

@ -25,7 +25,7 @@ import migrations from "../migrations/hotbar-store";
import * as uuid from "uuid";
import isNull from "lodash/isNull";
import { toJS } from "./utils";
import { CatalogEntity } from "./catalog";
import { CatalogEntity } from "../renderer/catalog";
export interface HotbarItem {
entity: {

View File

@ -23,6 +23,7 @@ export type Disposer = () => void;
interface Extendable<T> {
push(...vals: T[]): void;
isEmpty: boolean;
}
export type ExtendableDisposer = Disposer & Extendable<Disposer>;
@ -37,5 +38,10 @@ export function disposer(...args: Disposer[]): ExtendableDisposer {
args.push(...vals);
};
return res;
Object.defineProperty(res, "isEmpty", {
writable: false,
get: () => args.length === 0,
});
return res as ExtendableDisposer;
}

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { action, ObservableMap } from "mobx";
import { action, IEnhancer, IObservableMapInitialValues, ObservableMap } from "mobx";
export class ExtendedMap<K, V> extends Map<K, V> {
static new<K, V>(entries?: readonly (readonly [K, V])[] | null): ExtendedMap<K, V> {
@ -67,6 +67,10 @@ export class ExtendedMap<K, V> extends Map<K, V> {
}
export class ExtendedObservableMap<K, V> extends ObservableMap<K, V> {
static new<K, V>(initialData?: IObservableMapInitialValues<K, V>, enhancer?: IEnhancer<V>, name?: string): ExtendedObservableMap<K, V> {
return new ExtendedObservableMap<K, V>(initialData, enhancer, name);
}
@action
getOrInsert(key: K, getVal: () => V): V {
if (this.has(key)) {
@ -75,4 +79,17 @@ export class ExtendedObservableMap<K, V> extends ObservableMap<K, V> {
return this.set(key, getVal()).get(key);
}
/**
* Set the value associated with `key` iff there was not a previous value
* @throws if `key` already in map
* @returns `this` so that `strictSet` can be chained
*/
strictSet(key: K, val: V): this {
if (this.has(key)) {
throw new TypeError("Duplicate key in map");
}
return this.set(key, val);
}
}

View File

@ -29,12 +29,16 @@ import * as EventBus from "./event-bus";
import * as Store from "./stores";
import * as Util from "./utils";
import * as Interface from "../interfaces";
import * as Catalog from "./catalog";
import * as Main from "./main";
import * as Renderer from "./renderer";
import * as Catalog from "../../common/catalog";
import * as Types from "./types";
export {
App,
EventBus,
Main,
Renderer,
Catalog,
Interface,
Store,

View File

@ -0,0 +1,59 @@
/**
* 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 { CatalogEntityRegistry as InternalCatalogEntityRegistry, CatalogCategoryRegistry as InternalCatalogCategoryRegistry, CatalogEntity, CatalogCategoryRegistration, SpecEnhancer } from "../../../main/catalog";
export type {
CatalogEntity,
} from "../../../main/catalog";
export class CatalogEntityRegistry {
static get items() {
return InternalCatalogEntityRegistry.getInstance().items;
}
}
export class CatalogCategoryRegistry {
static add(category: CatalogCategoryRegistration) {
return InternalCatalogCategoryRegistry.getInstance().add(category);
}
static get items() {
return InternalCatalogCategoryRegistry.getInstance().items;
}
static getForGroupKind(group: string, version: string, kind: string) {
return InternalCatalogCategoryRegistry.getInstance().getForGroupKind(group, version, kind);
}
static hasForGroupKind(group: string, version: string, kind: string) {
return InternalCatalogCategoryRegistry.getInstance().hasForGroupKind(group, version, kind);
}
static getCategoryForEntity(data: CatalogEntity) {
return InternalCatalogCategoryRegistry.getInstance().getCategoryForEntity(data);
}
static registerSpecEnhancer(apiVersion: string, kind: string, handler: SpecEnhancer) {
return InternalCatalogCategoryRegistry.getInstance().registerSpecEnhancer(apiVersion, kind, handler);
}
}

View File

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

View File

@ -0,0 +1,71 @@
/**
* 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 { CatalogEntityRegistry as InternalCatalogEntityRegistry, CatalogCategoryRegistry as InternalCatalogCategoryRegistry, CatalogEntity, CategoryHandlerNames, CatalogHandler, EntityContextHandlers, CategoryHandlers, GlobalContextHandlers } from "../../../renderer/catalog";
import type { CatalogCategoryRegistration } from "../../../renderer/catalog";
export type {
CatalogEntity,
} from "../../../renderer/catalog";
export class CatalogEntityRegistry {
static getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] {
return InternalCatalogEntityRegistry.getInstance().getItemsForApiKind<T>(apiVersion, kind);
}
}
export class CatalogCategoryRegistry {
static add(category: CatalogCategoryRegistration) {
return InternalCatalogCategoryRegistry.getInstance().add(category);
}
static get items() {
return InternalCatalogCategoryRegistry.getInstance().items;
}
static getForGroupKind(group: string, version: string, kind: string) {
return InternalCatalogCategoryRegistry.getInstance().getForGroupKind(group, version, kind);
}
static hasForGroupKind(group: string, version: string, kind: string) {
return InternalCatalogCategoryRegistry.getInstance().hasForGroupKind(group, version, kind);
}
static getCategoryForEntity(data: CatalogEntity) {
return InternalCatalogCategoryRegistry.getInstance().getCategoryForEntity(data);
}
static registerHandler(apiVersion: string, kind: string, handlerName: CategoryHandlerNames, handler: CatalogHandler<typeof handlerName>) {
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(reg: CatalogCategoryRegistration, handlerName: "onCatalogAddMenu"): ReturnType<CategoryHandlers[typeof handlerName]>;
static runGlobalHandlersFor(reg: CatalogCategoryRegistration, handlerName: GlobalContextHandlers): ReturnType<CategoryHandlers[typeof handlerName]> {
return InternalCatalogCategoryRegistry.getInstance().runGlobalHandlersFor(reg, handlerName as any);
}
}

View File

@ -19,24 +19,4 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { navigate } from "../navigation";
import { commandRegistry } from "../../extensions/registries";
import type { CatalogEntity } from "../../common/catalog";
export { CatalogCategory, CatalogEntity } from "../../common/catalog";
export type {
CatalogEntityData,
CatalogEntityKindData,
CatalogEntityActionContext,
CatalogEntityAddMenuContext,
CatalogEntityAddMenu,
CatalogEntityContextMenu,
CatalogEntityContextMenuContext,
} from "../../common/catalog";
export const catalogEntityRunContext = {
navigate: (url: string) => navigate(url),
setCommandPaletteContext: (entity?: CatalogEntity) => {
commandRegistry.activeEntity = entity;
}
};
export * as Catalog from "./catalog";

View File

@ -19,12 +19,12 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { LensExtension } from "./lens-extension";
import { Disposers, LensExtension } from "./lens-extension";
import { WindowManager } from "../main/window-manager";
import { getExtensionPageUrl } from "./registries/page-registry";
import { catalogEntityRegistry } from "../main/catalog";
import type { CatalogEntity } from "../common/catalog";
import type { IObservableArray } from "mobx";
import { CatalogEntityRegistry } from "../main/catalog";
import type { CatalogEntity } from "../main/catalog";
import type { IComputedValue, IObservableArray } from "mobx";
import type { MenuRegistration } from "./registries";
export class LensMainExtension extends LensExtension {
@ -41,11 +41,11 @@ export class LensMainExtension extends LensExtension {
await windowManager.navigate(pageUrl, frameId);
}
addCatalogSource(id: string, source: IObservableArray<CatalogEntity>) {
catalogEntityRegistry.addObservableSource(`${this.name}:${id}`, source);
addObservableCatalogSource(id: string, source: IObservableArray<CatalogEntity>) {
this[Disposers].push(CatalogEntityRegistry.getInstance().addObservableSource(`${this.name}:${id}`, source));
}
removeCatalogSource(id: string) {
catalogEntityRegistry.removeSource(`${this.name}:${id}`);
addComputedCatalogSource(id: string, source: IComputedValue<CatalogEntity[]>) {
this[Disposers].push(CatalogEntityRegistry.getInstance().addComputedSource(`${this.name}:${id}`, source));
}
}

View File

@ -24,7 +24,7 @@
import { BaseRegistry } from "./base-registry";
import { makeObservable, observable } from "mobx";
import type { LensExtension } from "../lens-extension";
import type { CatalogEntity } from "../../common/catalog";
import type { CatalogEntity } from "../../renderer/catalog";
export type CommandContext = {
entity?: CatalogEntity;

View File

@ -20,7 +20,7 @@
*/
import type React from "react";
import type { CatalogEntity } from "../../common/catalog";
import type { CatalogEntity } from "../../renderer/catalog";
import { BaseRegistry } from "./base-registry";
export interface EntitySettingViewProps {

View File

@ -20,7 +20,7 @@
*/
export { isAllowedResource } from "../../common/rbac";
export { ResourceStack } from "../../common/k8s/resource-stack";
export { ResourceStack } from "../../renderer/k8s/resource-stack";
export { apiManager } from "../../renderer/api/api-manager";
export { KubeObjectStore } from "../../renderer/kube-object.store";
export { KubeApi, forCluster } from "../../renderer/api/kube-api";

View File

@ -21,12 +21,11 @@
import { reaction } from "mobx";
import { broadcastMessage } from "../common/ipc";
import type { CatalogEntityRegistry } from "./catalog";
import "../common/catalog-entities/kubernetes-cluster";
import { toJS } from "../common/utils";
import { CatalogEntityRegistry } from "./catalog/catalog-entity-registry";
export function pushCatalogToRenderer(catalog: CatalogEntityRegistry) {
return reaction(() => toJS(catalog.items), (items) => {
export function pushCatalogToRenderer() {
return reaction(() => toJS(CatalogEntityRegistry.getInstance().items), (items) => {
broadcastMessage("catalog:items", items);
}, {
fireImmediately: true,

View File

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

View File

@ -20,20 +20,19 @@
*/
import { action, observable, IComputedValue, computed, ObservableMap, runInAction, makeObservable, observe } from "mobx";
import type { CatalogEntity } from "../../common/catalog";
import { catalogEntityRegistry } from "../../main/catalog";
import type { CatalogEntity } from "../../main/catalog";
import { CatalogEntityRegistry } from "../../main/catalog";
import { watch } from "chokidar";
import fs from "fs";
import fse from "fs-extra";
import type stream from "stream";
import { Disposer, ExtendedObservableMap, iter, Singleton } from "../../common/utils";
import { disposer, Disposer, ExtendedObservableMap, iter, Singleton } from "../../common/utils";
import logger from "../logger";
import type { KubeConfig } from "@kubernetes/client-node";
import { loadConfigFromString, splitConfig, validateKubeConfig } from "../../common/kube-helpers";
import { Cluster } from "../cluster";
import { catalogEntityFromCluster } from "../cluster-manager";
import { UserStore } from "../../common/user-store";
import { ClusterStore, UpdateClusterModel } from "../../common/cluster-store";
import { ClusterPreferencesStore, UpdateClusterModel } from "../../common/cluster-store";
import { createHash } from "crypto";
import { homedir } from "os";
@ -41,63 +40,67 @@ const logPrefix = "[KUBECONFIG-SYNC]:";
export class KubeconfigSyncManager extends Singleton {
protected sources = observable.map<string, [IComputedValue<CatalogEntity[]>, Disposer]>();
protected syncing = false;
protected syncListDisposer?: Disposer;
protected disposers = disposer();
protected static readonly syncName = "lens:kube-sync";
constructor() {
super();
makeObservable(this);
}
protected computedSource = computed(() => (
Array.from(iter.flatMap(
this.sources.values(),
([entities]) => entities.get()
))
));
get syncing(): boolean {
return !this.disposers.isEmpty;
}
@action
startSync(): void {
if (this.syncing) {
return;
}
this.syncing = true;
logger.info(`${logPrefix} starting requested syncs`);
catalogEntityRegistry.addComputedSource(KubeconfigSyncManager.syncName, computed(() => (
Array.from(iter.flatMap(
this.sources.values(),
([entities]) => entities.get()
))
)));
this.disposers.push(
CatalogEntityRegistry.getInstance()
.addComputedSource(KubeconfigSyncManager.syncName, this.computedSource)
);
// This must be done so that c&p-ed clusters are visible
this.startNewSync(ClusterStore.storedKubeConfigFolder);
this.startNewSync(ClusterPreferencesStore.storedKubeConfigFolder);
for (const filePath of UserStore.getInstance().syncKubeconfigEntries.keys()) {
this.startNewSync(filePath);
}
this.syncListDisposer = observe(UserStore.getInstance().syncKubeconfigEntries, change => {
switch (change.type) {
case "add":
this.startNewSync(change.name);
break;
case "delete":
this.stopOldSync(change.name);
break;
}
});
this.disposers.push(
observe(UserStore.getInstance().syncKubeconfigEntries, change => {
switch (change.type) {
case "add":
this.startNewSync(change.name);
break;
case "delete":
this.stopOldSync(change.name);
break;
}
}, true)
);
}
@action
stopSync() {
this.syncListDisposer?.();
this.disposers();
for (const filePath of this.sources.keys()) {
this.stopOldSync(filePath);
}
catalogEntityRegistry.removeSource(KubeconfigSyncManager.syncName);
this.syncing = false;
}
@action
@ -149,7 +152,7 @@ export function configToModels(config: KubeConfig, filePath: string): UpdateClus
return validConfigs;
}
type RootSourceValue = [Cluster, CatalogEntity];
type RootSourceValue = CatalogEntity;
type RootSource = ObservableMap<string, RootSourceValue>;
// exported for testing
@ -161,12 +164,11 @@ export function computeDiff(contents: string, source: RootSource, filePath: stri
logger.debug(`${logPrefix} File now has ${models.size} entries`, { filePath });
for (const [contextName, value] of source) {
for (const contextName of source.keys()) {
const model = models.get(contextName);
// remove and disconnect clusters that were removed from the config
if (!model) {
value[0].disconnect();
source.delete(contextName);
logger.debug(`${logPrefix} Removed old cluster from sync`, { filePath, contextName });
continue;
@ -177,7 +179,6 @@ export function computeDiff(contents: string, source: RootSource, filePath: stri
// diff against that
// or update the model and mark it as not needed to be added
value[0].updateModel(model);
models.delete(contextName);
logger.debug(`${logPrefix} Updated old cluster from sync`, { filePath, contextName });
}
@ -186,18 +187,13 @@ export function computeDiff(contents: string, source: RootSource, filePath: stri
// add new clusters to the source
try {
const clusterId = createHash("md5").update(`${filePath}:${contextName}`).digest("hex");
const cluster = ClusterStore.getInstance().getById(clusterId) || new Cluster({ ...model, id: clusterId});
const entity = catalogEntityFromCluster({
id: clusterId,
...model
});
if (!cluster.apiUrl) {
throw new Error("Cluster constructor failed, see above error");
}
const entity = catalogEntityFromCluster(cluster);
if (!filePath.startsWith(ClusterStore.storedKubeConfigFolder)) {
entity.metadata.labels.file = filePath.replace(homedir(), "~");
}
source.set(contextName, [cluster, entity]);
entity.metadata.labels.file = filePath.replace(homedir(), "~");
source.set(contextName, entity);
logger.debug(`${logPrefix} Added new cluster from sync`, { filePath, contextName });
} catch (error) {
@ -258,17 +254,17 @@ async function watchFileChanges(filePath: string): Promise<[IComputedValue<Catal
depth: stat.isDirectory() ? 0 : 1, // DIRs works with 0 but files need 1 (bug: https://github.com/paulmillr/chokidar/issues/1095)
disableGlobbing: true,
});
const rootSource = new ExtendedObservableMap<string, ObservableMap<string, RootSourceValue>>();
const derivedSource = computed(() => Array.from(iter.flatMap(rootSource.values(), from => iter.map(from.values(), child => child[1]))));
const rootSource = new ExtendedObservableMap<string, ExtendedObservableMap<string, RootSourceValue>>();
const derivedSource = computed(() => Array.from(iter.flatMap(rootSource.values(), from => from.values())));
const stoppers = new Map<string, Disposer>();
watcher
.on("change", (childFilePath) => {
stoppers.get(childFilePath)();
stoppers.set(childFilePath, diffChangedConfig(childFilePath, rootSource.getOrInsert(childFilePath, observable.map)));
stoppers.set(childFilePath, diffChangedConfig(childFilePath, rootSource.getOrInsert(childFilePath, ExtendedObservableMap.new)));
})
.on("add", (childFilePath) => {
stoppers.set(childFilePath, diffChangedConfig(childFilePath, rootSource.getOrInsert(childFilePath, observable.map)));
stoppers.set(childFilePath, diffChangedConfig(childFilePath, rootSource.getOrInsert(childFilePath, ExtendedObservableMap.new)));
})
.on("unlink", (childFilePath) => {
stoppers.get(childFilePath)();

View File

@ -20,34 +20,31 @@
*/
import { observable, reaction } from "mobx";
import { WebLink, WebLinkSpec, WebLinkStatus } from "../../../common/catalog-entities";
import { catalogCategoryRegistry, CatalogEntity, CatalogEntityMetadata } from "../../../common/catalog";
import type { CatalogEntityData } from "../../../renderer/catalog";
import { initCatalogCategories } from "../../initializers";
import { CatalogCategoryRegistry } from "../catalog-category-registry";
import type { CatalogEntity } from "../catalog-entity";
import { CatalogEntityRegistry } from "../catalog-entity-registry";
class InvalidEntity extends CatalogEntity<CatalogEntityMetadata, WebLinkStatus, WebLinkSpec> {
public readonly apiVersion = "entity.k8slens.dev/v1alpha1";
public readonly kind = "Invalid";
function getInvalidEntity(data: CatalogEntityData): CatalogEntity {
return {
apiVersion: "entity.k8slens.dev/v1alpha1",
kind: "Invalid",
...data
};
}
async onRun() {
return;
}
public onSettingsOpen(): void {
return;
}
public onDetailsOpen(): void {
return;
}
public onContextMenuOpen(): void {
return;
}
function getWeblinkEntity(data: CatalogEntityData): CatalogEntity {
return {
apiVersion: "entity.k8slens.dev/v1alpha1",
kind: "WebLink",
...data
};
}
describe("CatalogEntityRegistry", () => {
let registry: CatalogEntityRegistry;
const entity = new WebLink({
const entity = getWeblinkEntity({
metadata: {
uid: "test",
name: "test-link",
@ -61,7 +58,7 @@ describe("CatalogEntityRegistry", () => {
phase: "valid"
}
});
const invalidEntity = new InvalidEntity({
const invalidEntity = getInvalidEntity({
metadata: {
uid: "invalid",
name: "test-link",
@ -77,7 +74,9 @@ describe("CatalogEntityRegistry", () => {
});
beforeEach(() => {
registry = new CatalogEntityRegistry(catalogCategoryRegistry);
CatalogCategoryRegistry.createInstance();
initCatalogCategories();
CatalogEntityRegistry.createInstance();
});
describe("addSource", () => {
@ -108,9 +107,10 @@ describe("CatalogEntityRegistry", () => {
it ("removes source", () => {
const source = observable.array([]);
registry.addObservableSource("test", source);
const d1 = registry.addObservableSource("test", source);
source.push(entity);
registry.removeSource("test");
d1();
expect(registry.items.length).toEqual(0);
});

View File

@ -0,0 +1,108 @@
/**
* 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 { once } from "lodash";
import { IComputedValue, observable, ObservableSet, when } from "mobx";
import { CatalogCategorySpecVersion, CatalogCategoryRegistration as CommonCatalogCategoryRegistration, CategoryMetadata, CatalogEntityStatus, parseApiVersion } from "../../common/catalog";
import { CatalogCategoryRegistry as CommonCatalogCategoryRegistry } from "../../common/catalog";
import { disposer, Disposer } from "../../common/utils";
import type { CatalogEntity } from "./catalog-entity";
type SpecFromEntity<Entity> = Entity extends CatalogEntity<any, infer Spec> ? Spec : never;
export type StatusComputation = (entity: CatalogEntity) => IComputedValue<CatalogEntityStatus>;
export type SpecEnhancer = (entity: CatalogEntity) => IComputedValue<Partial<SpecFromEntity<CatalogEntity>>>;
export interface CategorySpecVersion extends CatalogCategorySpecVersion {
/**
* This function is called once per ID, even if there was a period of time when that item was no longer in the catalog
*/
getStatus: StatusComputation;
}
export type CatalogCategoryRegistration = CommonCatalogCategoryRegistration<CategoryMetadata, CategorySpecVersion>;
export interface CatalogCategory extends CatalogCategoryRegistration {
specEnhancers: ObservableSet<SpecEnhancer>;
}
export interface EntityEnhancerFunctions {
status: StatusComputation,
spec: SpecEnhancer[];
}
export class CatalogCategoryRegistry extends CommonCatalogCategoryRegistry<CatalogCategoryRegistration, CatalogCategory> {
protected register(registration: CatalogCategoryRegistration): CatalogCategory {
return {
specEnhancers: observable.set(),
...registration
};
}
/**
* Adds a way compute optional part of a CatalogEntity's spec field.
* The value passed into the `handler` is the non-computed value.
* The returned value should respect the initial spec.
* @param apiVersion The apiVersion of the entity
* @param kind The kind of the entity
* @param handler A function that is called with the raw entity data, once on initial creation.
* @returns A function to remove this enhancer
*/
registerSpecEnhancer(apiVersion: string, kind: string, handler: SpecEnhancer): Disposer {
const { group, version } = parseApiVersion(apiVersion, false);
if (version) {
// only one version to do
return disposer(
when(
() => this.hasForGroupKind(group, version, kind),
() => {
this.groupVersionKinds
.get(group)
.get(version)
.get(kind)
.specEnhancers.add(handler);
},
),
once(() => this.groupVersionKinds.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
}
getEnhancerForEntity(entity: CatalogEntity): EntityEnhancerFunctions | null {
const { group, version } = parseApiVersion(entity.apiVersion);
const catalog = this.groupVersionKinds.get(group)?.get(version)?.get(entity.kind);
if (!catalog) {
return null;
}
return {
status: catalog.spec.versions.find(spec => spec.version === version).getStatus,
spec: Array.from(catalog.specEnhancers)
};
}
}

View File

@ -19,40 +19,77 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { action, computed, IComputedValue, IObservableArray, makeObservable, observable } from "mobx";
import { CatalogCategoryRegistry, catalogCategoryRegistry, CatalogEntity } from "../../common/catalog";
import { iter } from "../../common/utils";
import { computed, observable, IComputedValue, IObservableArray } from "mobx";
import type { CatalogEntity, CatalogEntityComputed } from "./catalog-entity";
import { Disposer, ExtendedObservableMap, iter, Singleton } from "../../common/utils";
import { CatalogCategoryRegistry } from "./catalog-category-registry";
import type { CatalogEntitySpec, CatalogEntityStatus } from "../../common/catalog";
import { cloneDeep } from "lodash";
export class CatalogEntityRegistry {
protected sources = observable.map<string, IComputedValue<CatalogEntity[]>>();
type SpecFromEntity<Entity> = Entity extends CatalogEntity<any, infer Spec> ? Spec : never;
constructor(private categoryRegistry: CatalogCategoryRegistry) {
makeObservable(this);
}
@action addObservableSource(id: string, source: IObservableArray<CatalogEntity>) {
this.sources.set(id, computed(() => source));
}
@action addComputedSource(id: string, source: IComputedValue<CatalogEntity[]>) {
this.sources.set(id, source);
}
@action removeSource(id: string) {
this.sources.delete(id);
}
@computed get items(): CatalogEntity[] {
const allItems = Array.from(iter.flatMap(this.sources.values(), source => source.get()));
return allItems.filter((entity) => this.categoryRegistry.getCategoryForEntity(entity) !== undefined);
}
getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] {
const items = this.items.filter((item) => item.apiVersion === apiVersion && item.kind === kind);
return items as T[];
}
interface EntityEnhancers {
status: IComputedValue<CatalogEntityStatus>,
spec: IComputedValue<Partial<SpecFromEntity<CatalogEntity>>>[];
}
export const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
export class CatalogEntityRegistry extends Singleton {
protected sources = observable.map<string, IComputedValue<CatalogEntity[]>>([], { deep: true });
protected computedEnhancers = new ExtendedObservableMap<string, EntityEnhancers>();
addObservableSource(id: string, source: IObservableArray<CatalogEntity>): Disposer {
return this.addComputedSource(id, computed(() => source));
}
addComputedSource(id: string, source: IComputedValue<CatalogEntity[]>): Disposer {
this.sources.set(id, source);
return () => this.sources.delete(id);
}
@computed private get rawItems() {
const allItems = Array.from(iter.flatMap(this.sources.values(), source => source.get()));
const res: CatalogEntity[] = [];
for (const entity of allItems) {
const enhancers = CatalogCategoryRegistry.getInstance().getEnhancerForEntity(entity);
if (!enhancers) {
continue;
}
this.computedEnhancers.getOrInsert(entity.metadata.uid, () => ({
status: enhancers.status(entity),
spec: enhancers.spec.map(enhancer => enhancer(entity)),
}));
}
return res;
}
@computed get items(): CatalogEntityComputed[] {
const res: CatalogEntityComputed[] = [];
for (const { spec, ...entity } of this.rawItems) {
const enhancers = this.computedEnhancers.get(entity.metadata.uid);
res.push({
status: enhancers.status.get(),
spec: this.foldSpecs(spec, enhancers.spec),
...entity
});
}
return res;
}
private foldSpecs(spec: CatalogEntitySpec, enhancers: IComputedValue<Partial<CatalogEntitySpec>>[]): CatalogEntitySpec {
const res = cloneDeep(spec);
for (const enhancer of enhancers) {
Object.assign(res, enhancer.get());
}
return res;
}
}

View File

@ -0,0 +1,40 @@
/**
* 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 type { CatalogEntityKindData, CatalogEntityMetadata, CatalogEntitySpec, CatalogEntityStatus } from "../../common/catalog";
export type { CatalogEntityKindData } from "../../common/catalog";
export interface CatalogEntity<
Metadata extends CatalogEntityMetadata = CatalogEntityMetadata,
Spec extends CatalogEntitySpec = CatalogEntitySpec,
> extends CatalogEntityKindData {
readonly metadata: Metadata;
readonly spec: Spec;
}
export interface CatalogEntityComputed<
Metadata extends CatalogEntityMetadata = CatalogEntityMetadata,
Spec extends CatalogEntitySpec = CatalogEntitySpec,
Status extends CatalogEntityStatus = CatalogEntityStatus,
> extends CatalogEntity<Metadata, Spec> {
readonly status: Status;
}

View File

@ -20,3 +20,5 @@
*/
export * from "./catalog-entity-registry";
export * from "./catalog-category-registry";
export * from "./catalog-entity";

View File

@ -22,180 +22,153 @@
import "../common/cluster-ipc";
import type http from "http";
import { ipcMain } from "electron";
import { action, autorun, makeObservable, reaction } from "mobx";
import { ClusterStore, getClusterIdFromHost } from "../common/cluster-store";
import type { Cluster } from "./cluster";
import { computed, makeObservable, observable } from "mobx";
import { ClusterModel, ClusterPreferencesStore, getClusterIdFromHost } from "../common/cluster-store";
import { Cluster } from "./cluster";
import logger from "./logger";
import { apiKubePrefix } from "../common/vars";
import { Singleton } from "../common/utils";
import { catalogEntityRegistry } from "./catalog";
import { KubernetesCluster, KubernetesClusterPrometheusMetrics } from "../common/catalog-entities/kubernetes-cluster";
import { noop, Singleton } from "../common/utils";
import { CatalogCategoryRegistry, CatalogEntity } from "./catalog";
import type { KubernetesClusterSpec } from "../common/catalog-entities/kubernetes-cluster";
import type { CatalogEntityMetadata } from "../common/catalog";
export class ClusterManager extends Singleton {
private store = ClusterStore.getInstance();
protected clusters = observable.map<string, Cluster>();
constructor() {
super();
makeObservable(this);
this.bindEvents();
}
private bindEvents() {
// reacting to every cluster's state change and total amount of items
reaction(
() => this.store.clustersList.map(c => c.getState()),
() => this.updateCatalog(this.store.clustersList),
{ fireImmediately: true, }
);
CatalogCategoryRegistry.getInstance().add({
apiVersion: "catalog.k8slens.dev/v1alpha1",
kind: "CatalogCategory",
metadata: {
name: "Kubernetes Clusters",
},
spec: {
group: "entity.k8slens.dev",
versions: [
{
version: "v1alpha1",
getStatus: (entity: CatalogEntity<CatalogEntityMetadata, KubernetesClusterSpec>) => {
const cluster = new Cluster({
id: entity.metadata.uid,
preferences: {
clusterName: entity.metadata.name
},
kubeConfigPath: entity.spec.kubeconfigPath,
contextName: entity.spec.kubeconfigContext
});
reaction(() => catalogEntityRegistry.getItemsForApiKind<KubernetesCluster>("entity.k8slens.dev/v1alpha1", "KubernetesCluster"), (entities) => {
this.syncClustersFromCatalog(entities);
});
this.clusters.set(entity.metadata.uid, cluster);
// auto-stop removed clusters
autorun(() => {
const removedClusters = Array.from(this.store.removedClusters.values());
if (removedClusters.length > 0) {
const meta = removedClusters.map(cluster => cluster.getMeta());
logger.info(`[CLUSTER-MANAGER]: removing clusters`, meta);
removedClusters.forEach(cluster => cluster.disconnect());
this.store.removedClusters.clear();
return computed(() => ({
phase: cluster.disconnected ? "disconnected" : "connected",
active: !cluster.disconnected,
}));
},
},
],
names: {
kind: "KubernetesCluster"
}
}
}, {
delay: 250
});
CatalogCategoryRegistry.getInstance().registerSpecEnhancer(
"entity.k8slens.dev/v1alpha1",
"KubernetesCluster",
(entity: CatalogEntity<CatalogEntityMetadata, KubernetesClusterSpec>) => {
if (entity.spec.metrics) {
return computed(() => ({}));
}
const preferences = ClusterPreferencesStore.getInstance().getById(entity.metadata.uid);
return computed(() => ({
metrics: {
source: "local",
prometheus: {
type: preferences.prometheusProvider?.type,
address: preferences.prometheus,
},
}
}));
}
);
ipcMain.on("network:offline", this.onNetworkOffline);
ipcMain.on("network:online", this.onNetworkOnline);
}
@action
protected updateCatalog(clusters: Cluster[]) {
for (const cluster of clusters) {
const index = catalogEntityRegistry.items.findIndex((entity) => entity.metadata.uid === cluster.id);
if (index !== -1) {
const entity = catalogEntityRegistry.items[index] as KubernetesCluster;
entity.status.phase = cluster.disconnected ? "disconnected" : "connected";
entity.status.active = !cluster.disconnected;
if (cluster.preferences?.clusterName) {
entity.metadata.name = cluster.preferences.clusterName;
}
entity.spec.metrics ||= { source: "local" };
if (entity.spec.metrics.source === "local") {
const prometheus: KubernetesClusterPrometheusMetrics = entity.spec?.metrics?.prometheus || {};
prometheus.type = cluster.preferences.prometheusProvider?.type;
prometheus.address = cluster.preferences.prometheus;
entity.spec.metrics.prometheus = prometheus;
}
catalogEntityRegistry.items.splice(index, 1, entity);
}
}
}
@action syncClustersFromCatalog(entities: KubernetesCluster[]) {
for (const entity of entities) {
const cluster = this.store.getById(entity.metadata.uid);
if (!cluster) {
this.store.addCluster({
id: entity.metadata.uid,
preferences: {
clusterName: entity.metadata.name
},
kubeConfigPath: entity.spec.kubeconfigPath,
contextName: entity.spec.kubeconfigContext
});
} else {
cluster.kubeConfigPath = entity.spec.kubeconfigPath;
cluster.contextName = entity.spec.kubeconfigContext;
entity.status = {
phase: cluster.disconnected ? "disconnected" : "connected",
active: !cluster.disconnected
};
}
}
}
protected onNetworkOffline = () => {
logger.info("[CLUSTER-MANAGER]: network is offline");
this.store.clustersList.forEach((cluster) => {
for (const cluster of this.clusters.values()) {
if (!cluster.disconnected) {
cluster.online = false;
cluster.accessible = false;
cluster.refreshConnectionStatus().catch((e) => e);
cluster.refreshConnectionStatus().catch(noop);
}
});
}
};
protected onNetworkOnline = () => {
logger.info("[CLUSTER-MANAGER]: network is online");
this.store.clustersList.forEach((cluster) => {
for (const cluster of this.clusters.values()) {
if (!cluster.disconnected) {
cluster.refreshConnectionStatus().catch((e) => e);
cluster.refreshConnectionStatus().catch(noop);
}
});
}
};
stop() {
this.store.clusters.forEach((cluster: Cluster) => {
for (const cluster of this.clusters.values()) {
cluster.disconnect();
});
}
}
getClusterForRequest(req: http.IncomingMessage): Cluster {
let cluster: Cluster = null;
// lens-server is connecting to 127.0.0.1:<port>/<uid>
if (req.headers.host.startsWith("127.0.0.1")) {
const clusterId = req.url.split("/")[1];
cluster = this.store.getById(clusterId);
const cluster = this.clusters.get(req.url.split("/")[1]);
if (cluster) {
// 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(`/${cluster.id}`, apiKubePrefix);
}
} else if (req.headers["x-cluster-id"]) {
cluster = this.store.getById(req.headers["x-cluster-id"].toString());
} else {
const clusterId = getClusterIdFromHost(req.headers.host);
cluster = this.store.getById(clusterId);
return cluster;
}
return cluster;
if (req.headers["x-cluster-id"]) {
return this.clusters.get(req.headers["x-cluster-id"].toString());
}
return this.clusters.get(getClusterIdFromHost(req.headers.host));
}
getById(id: string): Cluster {
return this.clusters.get(id);
}
}
export function catalogEntityFromCluster(cluster: Cluster) {
return new KubernetesCluster({
export function catalogEntityFromCluster(cluster: ClusterModel): CatalogEntity<CatalogEntityMetadata, KubernetesClusterSpec> {
return {
apiVersion: "entity.k8slens.dev/v1alpha1",
kind: "KubernetesCluster",
metadata: {
uid: cluster.id,
name: cluster.name,
name: cluster.contextName,
source: "local",
labels: {
distro: cluster.distribution,
distro: cluster.metadata.distribution?.toString() || "unknown",
}
},
spec: {
kubeconfigPath: cluster.kubeConfigPath,
kubeconfigContext: cluster.contextName
},
status: {
phase: cluster.disconnected ? "disconnected" : "connected",
reason: "",
message: "",
active: !cluster.disconnected
}
});
};
}

View File

@ -20,8 +20,8 @@
*/
import { ipcMain } from "electron";
import type { ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel } from "../common/cluster-store";
import { action, comparer, computed, makeObservable, observable, reaction, when } from "mobx";
import type { ClusterId, ClusterMetadata, ClusterModel, ClusterPrometheusPreferences, UpdateClusterModel } from "../common/cluster-store";
import { broadcastMessage, ClusterListNamespaceForbiddenChannel } from "../common/ipc";
import { ContextHandler } from "./context-handler";
import { AuthorizationV1Api, CoreV1Api, HttpError, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node";
@ -167,12 +167,6 @@ export class Cluster implements ClusterModel, ClusterState {
* @observable
*/
@observable isGlobalWatchEnabled = false;
/**
* Preferences
*
* @observable
*/
@observable preferences: ClusterPreferences = {};
/**
* Metadata
*

View File

@ -35,7 +35,7 @@ import { shellSync } from "./shell-sync";
import { mangleProxyEnv } from "./proxy-env";
import { registerFileProtocol } from "../common/register-protocol";
import logger from "./logger";
import { ClusterStore } from "../common/cluster-store";
import { ClusterPreferencesStore } from "../common/cluster-store";
import { UserStore } from "../common/user-store";
import { appEventBus } from "../common/event-bus";
import { ExtensionLoader } from "../extensions/extension-loader";
@ -50,12 +50,13 @@ import { bindBroadcastHandlers } from "../common/ipc";
import { startUpdateChecking } from "./app-updater";
import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
import { pushCatalogToRenderer } from "./catalog-pusher";
import { catalogEntityRegistry } from "./catalog";
import { CatalogCategoryRegistry, CatalogEntityRegistry } from "./catalog";
import { HotbarStore } from "../common/hotbar-store";
import { HelmRepoManager } from "./helm/helm-repo-manager";
import { KubeconfigSyncManager } from "./catalog-sources";
import { handleWsUpgrade } from "./proxy/ws-upgrade";
import configurePackages from "../common/configure-packages";
import { initCatalogCategories } from "./initializers/catalog-categories";
const workingDir = path.join(app.getPath("appData"), appName);
const cleanup = disposer();
@ -112,6 +113,10 @@ app.on("second-instance", (event, argv) => {
});
app.on("ready", async () => {
CatalogCategoryRegistry.createInstance();
initCatalogCategories();
CatalogEntityRegistry.createInstance();
logger.info(`🚀 Starting ${productName} from "${workingDir}"`);
logger.info("🐚 Syncing shell environment");
await shellSync();
@ -125,7 +130,7 @@ app.on("ready", async () => {
registerFileProtocol("static", __static);
const userStore = UserStore.createInstance();
const clusterStore = ClusterStore.createInstance();
const clusterStore = ClusterPreferencesStore.createInstance();
const hotbarStore = HotbarStore.createInstance();
const extensionsStore = ExtensionsStore.createInstance();
const filesystemStore = FilesystemProvisionerStore.createInstance();
@ -190,7 +195,7 @@ app.on("ready", async () => {
}
ipcMain.on(IpcRendererNavigationEvents.LOADED, () => {
cleanup.push(pushCatalogToRenderer(catalogEntityRegistry));
cleanup.push(pushCatalogToRenderer());
KubeconfigSyncManager.getInstance().startSync();
startUpdateChecking();
LensProtocolRouterMain.getInstance().rendererLoaded = true;

View File

@ -0,0 +1,62 @@
/**
* 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 { computed } from "mobx";
import URLParse from "url-parse";
import type { CatalogEntityMetadata } from "../../common/catalog";
import type { WebLinkSpec } from "../../common/catalog-entities";
import { CatalogCategoryRegistry, CatalogEntity } from "../catalog";
function isValid(url: string): boolean {
try {
new URLParse(url);
return true;
} catch {
return false;
}
}
export function initCatalogCategories() {
// KubernetesCluster is done in "cluster-manager.ts"
CatalogCategoryRegistry.getInstance().add({
apiVersion: "catalog.k8slens.dev/v1alpha1",
kind: "WebLink",
metadata: {
name: "Web Links",
},
spec: {
group: "entity.k8slens.dev",
versions: [
{
version: "v1alpha1",
getStatus: (entity: CatalogEntity<CatalogEntityMetadata, WebLinkSpec>) => computed(() => ({
phase: isValid(entity.spec.url) ? "valid" : "invalid",
})),
}
],
names: {
kind: "WebLink"
}
}
});
}

View File

@ -0,0 +1,22 @@
/**
* 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.
*/
export * from "./catalog-categories";

View File

@ -26,14 +26,14 @@ import path from "path";
import { app, remote } from "electron";
import { migration } from "../migration-wrapper";
import fse from "fs-extra";
import { ClusterModel, ClusterStore } from "../../common/cluster-store";
import { ClusterModel, ClusterPreferencesStore } from "../../common/cluster-store";
import { loadConfig } from "../../common/kube-helpers";
export default migration({
version: "3.6.0-beta.1",
run(store, printLog) {
const userDataPath = (app || remote.app).getPath("userData");
const kubeConfigBase = ClusterStore.getCustomKubeConfigPath("");
const kubeConfigBase = ClusterPreferencesStore.getCustomKubeConfigPath("");
const storedClusters: ClusterModel[] = store.get("clusters") || [];
if (!storedClusters.length) return;
@ -47,7 +47,7 @@ export default migration({
*/
try {
// take the embedded kubeconfig and dump it into a file
cluster.kubeConfigPath = ClusterStore.embedCustomKubeConfig(cluster.id, cluster.kubeConfig);
cluster.kubeConfigPath = ClusterPreferencesStore.embedCustomKubeConfig(cluster.id, cluster.kubeConfig);
cluster.contextName = loadConfig(cluster.kubeConfigPath).getCurrentContext();
delete cluster.kubeConfig;

View File

@ -21,7 +21,7 @@
// Cleans up a store that had the state related data stored
import type { Hotbar } from "../../common/hotbar-store";
import { ClusterStore } from "../../common/cluster-store";
import { ClusterPreferencesStore } from "../../common/cluster-store";
import { migration } from "../migration-wrapper";
import { v4 as uuid } from "uuid";
@ -30,7 +30,7 @@ export default migration({
run(store) {
const hotbars: Hotbar[] = [];
ClusterStore.getInstance().clustersList.forEach((cluster: any) => {
ClusterPreferencesStore.getInstance().clustersList.forEach((cluster: any) => {
const name = cluster.workspace;
if (!name) return;

View File

@ -20,8 +20,8 @@
*/
import type { Hotbar } from "../../common/hotbar-store";
import { CatalogEntityRegistry } from "../../renderer/catalog";
import { migration } from "../migration-wrapper";
import { catalogEntityRegistry } from "../../renderer/api/catalog-entity-registry";
export default migration({
version: "5.0.0-beta.5",
@ -30,7 +30,7 @@ export default migration({
hotbars.forEach((hotbar, hotbarIndex) => {
hotbar.items.forEach((item, itemIndex) => {
const entity = catalogEntityRegistry.items.find((entity) => entity.metadata.uid === item?.entity.uid);
const entity = CatalogEntityRegistry.getInstance().items.find((entity) => entity.metadata.uid === item?.entity.uid);
if (!entity) {
// Clear disabled item

View File

@ -32,7 +32,7 @@ import { render, unmountComponentAtNode } from "react-dom";
import { delay } from "../common/utils";
import { isMac, isDevelopment } from "../common/vars";
import { HotbarStore } from "../common/hotbar-store";
import { ClusterStore } from "../common/cluster-store";
import { ClusterPreferencesStore } from "../common/cluster-store";
import { UserStore } from "../common/user-store";
import { ExtensionDiscovery } from "../extensions/extension-discovery";
import { ExtensionLoader } from "../extensions/extension-loader";
@ -48,11 +48,11 @@ import configurePackages from "../common/configure-packages";
configurePackages();
/**
* If this is a development buid, wait a second to attach
* Chrome Debugger to renderer process
* https://stackoverflow.com/questions/52844870/debugging-electron-renderer-process-with-vscode
*/
async function attachChromeDebugger() {
if (isDevelopment) {
await delay(1000);
@ -73,7 +73,7 @@ export async function bootstrap(App: AppComponent) {
ExtensionDiscovery.createInstance().init();
const userStore = UserStore.createInstance();
const clusterStore = ClusterStore.createInstance();
const clusterStore = ClusterPreferencesStore.createInstance();
const extensionsStore = ExtensionsStore.createInstance();
const filesystemStore = FilesystemProvisionerStore.createInstance();
const themeStore = ThemeStore.createInstance();
@ -102,7 +102,7 @@ export async function bootstrap(App: AppComponent) {
window.addEventListener("message", (ev: MessageEvent) => {
if (ev.data === "teardown") {
UserStore.getInstance(false)?.unregisterIpcListener();
ClusterStore.getInstance(false)?.unregisterIpcListener();
ClusterPreferencesStore.getInstance(false)?.unregisterIpcListener();
unmountComponentAtNode(rootElem);
window.location.href = "about:blank";
}

View File

@ -0,0 +1,22 @@
/**
* 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.
*/
export * from "./kubernetes-cluster";

View File

@ -0,0 +1,43 @@
/**
* 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 type { CatalogEntityMetadata } from "../../common/catalog";
import type { KubernetesClusterSpec, KubernetesClusterStatus } from "../../common/catalog-entities";
import { clusterActivateHandler, clusterDisconnectHandler } from "../../common/cluster-ipc";
import { requestMain } from "../../common/ipc";
import { CatalogEntity, CatalogEntityActionContext } from "../catalog/catalog-entity";
export class KubernetesCluster extends CatalogEntity<CatalogEntityMetadata, KubernetesClusterStatus, KubernetesClusterSpec> {
public readonly apiVersion = "entity.k8slens.dev/v1alpha1";
public readonly kind = "KubernetesCluster";
async connect(): Promise<void> {
return requestMain(clusterActivateHandler, this.metadata.uid, false);
}
async disconnect(): Promise<void> {
return requestMain(clusterDisconnectHandler, this.metadata.uid, false);
}
onRun = (context: CatalogEntityActionContext) => {
context.navigate(`/cluster/${this.metadata.uid}`);
};
}

View File

@ -19,17 +19,15 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { CatalogEntityMetadata } from "../../common/catalog";
import type { WebLinkSpec, WebLinkStatus } from "../../common/catalog-entities";
import { CatalogEntity } from "../catalog";
import type { CatalogEntity } from "../../common/catalog";
import { catalogEntityRegistry as registry } from "../../main/catalog";
export class WebLink extends CatalogEntity<CatalogEntityMetadata, WebLinkStatus, WebLinkSpec> {
public readonly apiVersion = "entity.k8slens.dev/v1alpha1";
public readonly kind = "WebLink";
export { catalogCategoryRegistry as catalogCategories } from "../../common/catalog/catalog-category-registry";
export * from "../../common/catalog-entities";
export class CatalogEntityRegistry {
getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] {
return registry.getItemsForApiKind<T>(apiVersion, kind);
}
onRun = () => {
window.open(this.spec.url, "_blank");
};
}
export const catalogEntities = new CatalogEntityRegistry();

View File

@ -21,20 +21,32 @@
import { CatalogEntityRegistry } from "../catalog-entity-registry";
import "../../../common/catalog-entities";
import { catalogCategoryRegistry } from "../../../common/catalog/catalog-category-registry";
import type { CatalogEntityData, CatalogEntityKindData } from "../catalog-entity";
import { CatalogCategoryRegistry } from "../catalog-category-registry";
import type { CatalogEntity } from "../catalog-entity";
class TestCatalogEntityRegistry extends CatalogEntityRegistry {
replaceItems(items: Array<CatalogEntityData & CatalogEntityKindData>) {
replaceItems(items: CatalogEntity[]) {
this.rawItems.replace(items);
}
}
describe("CatalogEntityRegistry", () => {
beforeEach(() => {
CatalogCategoryRegistry.createInstance();
TestCatalogEntityRegistry.createInstance();
});
afterEach(() => {
TestCatalogEntityRegistry.resetInstance();
CatalogCategoryRegistry.resetInstance();
});
describe("updateItems", () => {
it("adds new catalog item", () => {
const catalog = new TestCatalogEntityRegistry(catalogCategoryRegistry);
const catalog = TestCatalogEntityRegistry.getInstance();
const items = [{
id: "123",
name: "foobar",
apiVersion: "entity.k8slens.dev/v1alpha1",
kind: "KubernetesCluster",
metadata: {
@ -53,6 +65,8 @@ describe("CatalogEntityRegistry", () => {
expect(catalog.items.length).toEqual(1);
items.push({
id: "456",
name: "barbaz",
apiVersion: "entity.k8slens.dev/v1alpha1",
kind: "KubernetesCluster",
metadata: {
@ -72,10 +86,12 @@ describe("CatalogEntityRegistry", () => {
});
it("updates existing items", () => {
const catalog = new TestCatalogEntityRegistry(catalogCategoryRegistry);
const catalog = TestCatalogEntityRegistry.getInstance();
const items = [{
id: "123",
apiVersion: "entity.k8slens.dev/v1alpha1",
kind: "KubernetesCluster",
name: "foobar",
metadata: {
uid: "123",
name: "foobar",
@ -100,11 +116,13 @@ describe("CatalogEntityRegistry", () => {
});
it("removes deleted items", () => {
const catalog = new TestCatalogEntityRegistry(catalogCategoryRegistry);
const catalog = TestCatalogEntityRegistry.getInstance();
const items = [
{
apiVersion: "entity.k8slens.dev/v1alpha1",
kind: "KubernetesCluster",
id: "123",
name: "foobar",
metadata: {
uid: "123",
name: "foobar",
@ -119,6 +137,8 @@ describe("CatalogEntityRegistry", () => {
{
apiVersion: "entity.k8slens.dev/v1alpha1",
kind: "KubernetesCluster",
id: "456",
name: "barbaz",
metadata: {
uid: "456",
name: "barbaz",
@ -142,11 +162,13 @@ describe("CatalogEntityRegistry", () => {
describe("items", () => {
it("does not return items without matching category", () => {
const catalog = new TestCatalogEntityRegistry(catalogCategoryRegistry);
const catalog = TestCatalogEntityRegistry.getInstance();
const items = [
{
apiVersion: "entity.k8slens.dev/v1alpha1",
kind: "KubernetesCluster",
id: "123",
name: "foobar",
metadata: {
uid: "123",
name: "foobar",
@ -161,6 +183,8 @@ describe("CatalogEntityRegistry", () => {
{
apiVersion: "entity.k8slens.dev/v1alpha1",
kind: "FooBar",
id: "456",
name: "barbaz",
metadata: {
uid: "456",
name: "barbaz",

View File

@ -0,0 +1,96 @@
/**
* 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 type { ObservableSet } from "mobx";
import type { CatalogCategoryRegistration as CommonCatalogCategoryRegistration, CatalogCategorySpecVersion as CommonCatalogCategorySpecVersion, CatalogEntityMetadata, CatalogEntitySpec, CatalogEntityStatus, CategoryMetadata as CommonCategoryMetadata } from "../../common/catalog";
import type { OnContextMenuOpen, OnAddMenuOpen, OnSettingsOpen, CategoryHandler, CatalogEntity } from "./catalog-entity";
import type { Rest } from "../../common/ipc";
import type { navigate } from "../navigation";
type KeysMatching<T, V> = { [K in keyof T]-?: T[K] extends V ? K : never }[keyof T];
type KeysNotMatching<T, V> = { [K in keyof T]-?: T[K] extends V ? never : K }[keyof T];
export type CategoryHandlers = {
[HandlerName in KeysMatching<Handlers, ObservableSet<any>>]?: Handlers[HandlerName] extends ObservableSet<infer Handler> ? Handler : never;
};
export type CategoryHandlerNames = keyof CategoryHandlers;
export type CatalogHandler<Name extends CategoryHandlerNames> = CategoryHandlers[Name];
export type EntityContextHandlers = keyof EntityContextGetters;
export type GlobalContextHandlers = keyof GlobalContextGetters;
type EntityContextGetters = {
[HandlerName in KeysMatching<CategoryHandlers, CategoryHandler<(...args: any) => any>>]: () => Rest<Parameters<CategoryHandlers[HandlerName]>>;
};
type GlobalContextGetters = {
[HandlerName in KeysNotMatching<CategoryHandlers, CategoryHandler<(...args: any) => any>>]: () => Parameters<CategoryHandlers[HandlerName]>;
};
export const EntityContexts: EntityContextGetters = {
onContextMenuOpen: () => [{ navigate }],
onSettingsOpen: () => [{ navigate }],
};
export const GlobalContexts: GlobalContextGetters = {
onCatalogAddMenu: () => [{ navigate }],
};
export interface CategoryMetadata extends CommonCategoryMetadata {
icon: string;
}
type ExtractEntityMetadataType<Entity> = Entity extends CatalogEntity<infer Metadata> ? Metadata : never;
type ExtractEntityStatusType<Entity> = Entity extends CatalogEntity<any, infer Status> ? Status : never;
type ExtractEntitySpecType<Entity> = Entity extends CatalogEntity<any, any, infer Spec> ? Spec : never;
export interface CatalogEntityData<
Metadata extends CatalogEntityMetadata = CatalogEntityMetadata,
Status extends CatalogEntityStatus = CatalogEntityStatus,
Spec extends CatalogEntitySpec = CatalogEntitySpec,
> {
metadata: Metadata;
status: Status;
spec: Spec;
}
export type CatalogEntityConstructor<Entity extends CatalogEntity> = (
(new (data: CatalogEntityData<
ExtractEntityMetadataType<Entity>,
ExtractEntityStatusType<Entity>,
ExtractEntitySpecType<Entity>
>) => Entity)
);
export interface CatalogCategorySpecVersion extends CommonCatalogCategorySpecVersion {
entityConstructor: CatalogEntityConstructor<CatalogEntity>,
}
export type CatalogCategoryRegistration = CommonCatalogCategoryRegistration<CategoryMetadata, CatalogCategorySpecVersion>;
export interface Handlers {
onContextMenuOpen: ObservableSet<CategoryHandler<OnContextMenuOpen>>;
onSettingsOpen: ObservableSet<CategoryHandler<OnSettingsOpen>>;
onCatalogAddMenu: ObservableSet<OnAddMenuOpen>;
}
export type Filtered<Handler> = Handler extends ((...args: any[]) => (infer T)[]) ? (...args: Parameters<Handler>) => Omit<T, "onlyVisibleForSource">[] : Handler;

View File

@ -0,0 +1,165 @@
/**
* 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, when } from "mobx";
import { disposer, Disposer } from "../../common/utils";
import { parseApiVersion, CatalogCategoryRegistry as CommonCatalogCategoryRegistry } from "../../common/catalog";
import type { AddMenuEntry, CatalogEntity, MenuEntry, SettingsMenu } from "./catalog-entity";
import { CatalogCategoryRegistration, CatalogHandler, CategoryHandlerNames, CategoryHandlers, EntityContextHandlers, EntityContexts, Filtered, GlobalContextHandlers, GlobalContexts, Handlers } from "./catalog-categories";
import { once } from "lodash";
import { ConfirmDialog } from "../components/confirm-dialog";
export interface CatalogCategory extends CatalogCategoryRegistration {
handlers: Handlers,
}
export type TransformedMenuItem = ReturnType<typeof tranformations["onContextMenuOpen"]>;
export type TransformedSettingsMenu = ReturnType<typeof tranformations["onSettingsOpen"]>;
function getOnClick(raw: Omit<MenuEntry, "onlyVisibleForSource">): () => void {
if (raw.confirm) {
return () => ConfirmDialog.open({
okButtonProps: {
primary: false,
accent: true,
},
ok: raw.onClick,
message: raw.confirm.message
});
}
return raw.onClick;
}
const tranformations = {
onContextMenuOpen: (entity: CatalogEntity, raw: MenuEntry) => {
if (raw.onlyVisibleForSource && raw.onlyVisibleForSource === entity.metadata.source) {
return null;
}
return {
title: raw.title,
onClick: getOnClick(raw),
};
},
onSettingsOpen: (entity: CatalogEntity, raw: SettingsMenu) => raw,
onCatalogAddMenu: (raw: AddMenuEntry) => ({
title: raw.title,
onClick: getOnClick(raw),
})
};
export class CatalogCategoryRegistry extends CommonCatalogCategoryRegistry<CatalogCategoryRegistration, CatalogCategory> {
protected register(registration: CatalogCategoryRegistration): CatalogCategory {
return {
handlers: {
onCatalogAddMenu: observable.set(),
onContextMenuOpen: observable.set(),
onSettingsOpen: observable.set(),
},
...registration
};
}
/**
* 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.hasForGroupKind(group, version, kind),
() => {
this.groupVersionKinds
.get(group)
.get(version)
.get(kind)
.handlers[handlerName].add(handler as any);
},
),
once(() => this.groupVersionKinds.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<typeof tranformations[typeof handlerName]>[];
runEntityHandlersFor(entity: CatalogEntity, handlerName: "onSettingsOpen"): ReturnType<typeof tranformations[typeof handlerName]>[];
runEntityHandlersFor(entity: CatalogEntity, handlerName: EntityContextHandlers): ReturnType<Filtered<CategoryHandlers[typeof handlerName]>> {
const category = this.getRegistered(entity.apiVersion, entity.kind);
const res = [];
for (const handler of category.handlers[handlerName].values()) {
const items = (handler as any)(entity, ...EntityContexts[handlerName]());
for (const item of items) {
if (!item) {
continue;
}
const transformed = tranformations[handlerName](entity, item);
if (transformed) {
continue;
}
res.push(transformed as any);
}
}
return res;
}
runGlobalHandlersFor({ spec }: CatalogCategoryRegistration, handlerName: "onCatalogAddMenu"): ReturnType<CategoryHandlers[typeof handlerName]>;
runGlobalHandlersFor({ spec }: CatalogCategoryRegistration, handlerName: GlobalContextHandlers): ReturnType<CategoryHandlers[typeof handlerName]> {
const category = this.getRegistered(spec.group, spec.names.kind);
const res: ReturnType<Filtered<CategoryHandlers[typeof handlerName]>> = [];
for (const handler of category.handlers[handlerName].values()) {
const items = (handler as any)(...GlobalContexts[handlerName]());
for (const item of items) {
if (!item) {
continue;
}
const transformed = tranformations[handlerName](item);
if (transformed) {
continue;
}
res.push(transformed as any);
}
}
return res;
}
}

View File

@ -21,20 +21,22 @@
import { computed, observable, makeObservable } from "mobx";
import { subscribeToBroadcast } from "../../common/ipc";
import { CatalogCategory, CatalogEntity, CatalogEntityData, catalogCategoryRegistry, CatalogCategoryRegistry, CatalogEntityKindData } from "../../common/catalog";
import "../../common/catalog-entities";
import { iter } from "../utils";
import { iter, Singleton } from "../utils";
import type { CatalogEntity } from "./catalog-entity";
import { CatalogCategoryRegistry } from "./catalog-category-registry";
import type { CatalogCategoryRegistration } from "./catalog-categories";
export class CatalogEntityRegistry {
protected rawItems = observable.array<CatalogEntityData & CatalogEntityKindData>([], { deep: true });
export class CatalogEntityRegistry extends Singleton {
protected rawItems = observable.array<CatalogEntity>([], { deep: true });
@observable protected _activeEntity: CatalogEntity;
constructor(private categoryRegistry: CatalogCategoryRegistry) {
constructor() {
super();
makeObservable(this);
}
init() {
subscribeToBroadcast("catalog:items", (ev, items: (CatalogEntityData & CatalogEntityKindData)[]) => {
subscribeToBroadcast("catalog:items", (ev, items: CatalogEntity[]) => {
this.rawItems.replace(items);
});
}
@ -48,11 +50,11 @@ export class CatalogEntityRegistry {
}
@computed get items() {
return Array.from(iter.filterMap(this.rawItems, rawItem => this.categoryRegistry.getEntityForData(rawItem)));
return Array.from(iter.filter(this.rawItems, item => CatalogCategoryRegistry.getInstance().getCategoryForEntity(item)));
}
@computed get entities(): Map<string, CatalogEntity> {
return new Map(this.items.map(item => [item.metadata.uid, item]));
return new Map(this.items.map(item => [item.id, item]));
}
getById(id: string) {
@ -65,12 +67,10 @@ export class CatalogEntityRegistry {
return items as T[];
}
getItemsForCategory<T extends CatalogEntity>(category: CatalogCategory): T[] {
const supportedVersions = category.spec.versions.map((v) => `${category.spec.group}/${v.name}`);
getItemsForCategory<T extends CatalogEntity>(category: CatalogCategoryRegistration): T[] {
const supportedVersions = category.spec.versions.map(({ version }) => `${category.spec.group}/${version}`);
const items = this.items.filter((item) => supportedVersions.includes(item.apiVersion) && item.kind === category.spec.names.kind);
return items as T[];
}
}
export const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);

View File

@ -0,0 +1,122 @@
/**
* 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 { navigate } from "../navigation";
import { commandRegistry } from "../../extensions/registries";
import type { CatalogEntityKindData, CatalogEntityMetadata, CatalogEntitySpec, CatalogEntityStatus } from "../../common/catalog";
import type { CatalogEntityData } from "./catalog-categories";
export type { CatalogEntityKindData } from "../../common/catalog";
export const catalogEntityRunContext = {
navigate: (url: string) => navigate(url),
setCommandPaletteContext: (entity?: CatalogEntity) => {
commandRegistry.activeEntity = entity;
}
};
export interface CatalogEntityActionContext {
navigate: (url: string) => void;
setCommandPaletteContext: (context?: CatalogEntity) => void;
}
export interface MenuEntry {
title: string;
onlyVisibleForSource?: string; // show only if empty or if matches with entity source
onClick: () => void | Promise<void>;
confirm?: {
message: string;
}
}
export interface AddMenuEntry extends Omit<MenuEntry, "onlyVisibleForSource"> {
icon: string;
}
export interface CatalogEntitySettingsMenu {
group?: string;
title: string;
components: {
View: React.ComponentType<any>
};
}
export interface MenuContext {
navigate: (url: string) => void;
}
export type OnContextMenuOpen = (ctx: MenuContext) => MenuEntry[];
export type OnAddMenuOpen = (ctx: MenuContext) => AddMenuEntry[];
export type CategoryHandler<EntityHandler extends (...args: any[]) => any> = (entity: CatalogEntity, ...args: Parameters<EntityHandler>) => ReturnType<EntityHandler>;
export interface SettingsContext {
}
export interface SettingsMenu {
group?: string;
title: string;
components: {
View: React.ComponentType<any>
};
}
export type OnSettingsOpen = (ctx: SettingsContext) => SettingsMenu[];
function deepFreeze(o: any) {
Object.freeze(o);
Object.getOwnPropertyNames(o).forEach(function (prop) {
if (o.hasOwnProperty(prop)
&& o[prop] !== null
&& (typeof o[prop] === "object" || typeof o[prop] === "function")
&& !Object.isFrozen(o[prop])) {
deepFreeze(o[prop]);
}
});
return o;
}
export class CatalogEntity<
Metadata extends CatalogEntityMetadata = CatalogEntityMetadata,
Status extends CatalogEntityStatus = CatalogEntityStatus,
Spec extends CatalogEntitySpec = CatalogEntitySpec,
> implements CatalogEntityKindData {
readonly metadata: Metadata;
readonly status: Status;
readonly spec: Spec;
readonly id: string;
readonly name: string;
readonly apiVersion: string;
readonly kind: string;
constructor(data: CatalogEntityData<Metadata, Status, Spec>) {
// This is done to prevent users from mistaking that they can overright these values to "save" them
this.metadata = deepFreeze(data.metadata);
this.status = deepFreeze(data.status);
this.spec = deepFreeze(data.spec);
this.id = this.metadata.uid;
this.name = this.metadata.name;
}
onRun?(context: CatalogEntityActionContext): void;
}

View File

@ -0,0 +1,25 @@
/**
* 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.
*/
export * from "./catalog-category-registry";
export * from "./catalog-entity-registry";
export * from "./catalog-entity";
export * from "./catalog-categories";

View File

@ -27,7 +27,7 @@ import { KubeConfig } from "@kubernetes/client-node";
import { AceEditor } from "../ace-editor";
import { Button } from "../button";
import { loadConfig, splitConfig, validateKubeConfig } from "../../../common/kube-helpers";
import { ClusterStore } from "../../../common/cluster-store";
import { ClusterPreferencesStore } from "../../../common/cluster-store";
import { v4 as uuid } from "uuid";
import { navigate } from "../../navigation";
import { UserStore } from "../../../common/user-store";
@ -112,7 +112,7 @@ export class AddCluster extends React.Component {
}).map(context => {
const clusterId = uuid();
const kubeConfig = this.kubeContexts.get(context);
const kubeConfigPath = ClusterStore.embedCustomKubeConfig(clusterId, kubeConfig); // save in app-files folder
const kubeConfigPath = ClusterPreferencesStore.embedCustomKubeConfig(clusterId, kubeConfig); // save in app-files folder
return {
id: clusterId,
@ -126,7 +126,7 @@ export class AddCluster extends React.Component {
});
runInAction(() => {
ClusterStore.getInstance().addClusters(...newClusters);
// ClusterPreferencesStore.getInstance().addClusters(...newClusters);
Notifications.ok(
<>Successfully imported <b>{newClusters.length}</b> cluster(s)</>

View File

@ -26,18 +26,16 @@ import { Icon } from "../icon";
import { disposeOnUnmount, observer } from "mobx-react";
import { observable, reaction, makeObservable } from "mobx";
import { boundMethod } from "../../../common/utils";
import type { CatalogCategory, CatalogEntityAddMenuContext, CatalogEntityAddMenu } from "../../api/catalog-entity";
import { EventEmitter } from "events";
import { navigate } from "../../navigation";
import { AddMenuEntry, CatalogCategoryRegistration, CatalogCategoryRegistry } from "../../catalog";
export type CatalogAddButtonProps = {
category: CatalogCategory
category: CatalogCategoryRegistration
};
@observer
export class CatalogAddButton extends React.Component<CatalogAddButtonProps> {
@observable protected isOpen = false;
protected menuItems = observable.array<CatalogEntityAddMenu>([]);
protected menuItems = observable.array<AddMenuEntry>([]);
constructor(props: CatalogAddButtonProps) {
super(props);
@ -48,15 +46,7 @@ export class CatalogAddButton extends React.Component<CatalogAddButtonProps> {
disposeOnUnmount(this, [
reaction(() => this.props.category, (category) => {
this.menuItems.clear();
if (category && category instanceof EventEmitter) {
const context: CatalogEntityAddMenuContext = {
navigate: (url: string) => navigate(url),
menuItems: this.menuItems
};
category.emit("onCatalogAddMenu", context);
}
this.menuItems.replace(CatalogCategoryRegistry.getInstance().runGlobalHandlersFor(category, "onCatalogAddMenu"));
}, { fireImmediately: true })
]);
}

View File

@ -19,33 +19,33 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from "mobx";
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
import type { CatalogEntity, CatalogEntityActionContext } from "../../api/catalog-entity";
import { computed, IReactionDisposer, makeObservable, observable, reaction } from "mobx";
import { ItemObject, ItemStore } from "../../item.store";
import { CatalogCategory } from "../../../common/catalog";
import { autoBind } from "../../../common/utils";
import { autoBind } from "../../utils";
import { CatalogEntityRegistry } from "../../catalog/catalog-entity-registry";
import type { CatalogEntity, CatalogEntityActionContext } from "../../catalog/catalog-entity";
import { CatalogCategoryRegistration, CatalogCategoryRegistry } from "../../catalog";
export class CatalogEntityItem implements ItemObject {
constructor(public entity: CatalogEntity) {}
get name() {
return this.entity.metadata.name;
return this.entity.name;
}
getName() {
return this.entity.metadata.name;
return this.entity.name;
}
get id() {
return this.entity.metadata.uid;
return this.entity.id;
}
getId() {
return this.id;
}
@computed get phase() {
get phase() {
return this.entity.status.phase;
}
@ -78,9 +78,8 @@ export class CatalogEntityItem implements ItemObject {
this.entity.onRun(ctx);
}
@action
async onContextMenuOpen(ctx: any) {
return this.entity.onContextMenuOpen(ctx);
onContextMenuOpen() {
return CatalogCategoryRegistry.getInstance().runEntityHandlersFor(this.entity, "onContextMenuOpen");
}
}
@ -91,14 +90,14 @@ export class CatalogEntityStore extends ItemStore<CatalogEntityItem> {
autoBind(this);
}
@observable activeCategory?: CatalogCategory;
@observable activeCategory?: CatalogCategoryRegistration;
@computed get entities() {
if (!this.activeCategory) {
return catalogEntityRegistry.items.map(entity => new CatalogEntityItem(entity));
return CatalogEntityRegistry.getInstance().items.map(entity => new CatalogEntityItem(entity));
}
return catalogEntityRegistry.getItemsForCategory(this.activeCategory).map(entity => new CatalogEntityItem(entity));
return CatalogEntityRegistry.getInstance().getItemsForCategory(this.activeCategory).map(entity => new CatalogEntityItem(entity));
}
watch() {

View File

@ -25,21 +25,19 @@ import { disposeOnUnmount, observer } from "mobx-react";
import { ItemListLayout } from "../item-object-list";
import { action, makeObservable, observable, reaction, when } from "mobx";
import { CatalogEntityItem, CatalogEntityStore } from "./catalog-entity.store";
import { navigate } from "../../navigation";
import { kebabCase } from "lodash";
import { PageLayout } from "../layout/page-layout";
import { MenuItem, MenuActions } from "../menu";
import { CatalogEntityContextMenu, CatalogEntityContextMenuContext, catalogEntityRunContext } from "../../api/catalog-entity";
import { catalogEntityRunContext, CatalogCategoryRegistry, TransformedMenuItem } from "../../catalog";
import { Badge } from "../badge";
import { HotbarStore } from "../../../common/hotbar-store";
import { ConfirmDialog } from "../confirm-dialog";
import { Tab, Tabs } from "../tabs";
import { catalogCategoryRegistry } from "../../../common/catalog";
import { CatalogAddButton } from "./catalog-add-button";
import type { RouteComponentProps } from "react-router";
import type { ICatalogViewRouteParam } from "./catalog.route";
import { Notifications } from "../notifications";
import { Avatar } from "../avatar/avatar";
import { boundMethod } from "autobind-decorator";
enum sortBy {
name = "name",
@ -52,8 +50,8 @@ interface Props extends RouteComponentProps<ICatalogViewRouteParam> {}
@observer
export class Catalog extends React.Component<Props> {
@observable private catalogEntityStore?: CatalogEntityStore;
@observable private contextMenu: CatalogEntityContextMenuContext;
@observable activeTab?: string;
menuItems = observable.array<TransformedMenuItem>();
constructor(props: Props) {
super(props);
@ -71,15 +69,11 @@ export class Catalog extends React.Component<Props> {
}
async componentDidMount() {
this.contextMenu = {
menuItems: [],
navigate: (url: string) => navigate(url)
};
this.catalogEntityStore = new CatalogEntityStore();
disposeOnUnmount(this, [
this.catalogEntityStore.watch(),
when(() => catalogCategoryRegistry.items.length > 0, () => {
const item = catalogCategoryRegistry.items.find(i => i.getId() === this.routeActiveTab);
when(() => CatalogCategoryRegistry.getInstance().items.length > 0, () => {
const item = CatalogCategoryRegistry.getInstance().items.find(i => i.id === this.routeActiveTab);
if (item || this.routeActiveTab === undefined) {
this.activeTab = this.routeActiveTab;
@ -88,9 +82,9 @@ export class Catalog extends React.Component<Props> {
Notifications.error(<p>Unknown category: {this.routeActiveTab}</p>);
}
}),
reaction(() => catalogCategoryRegistry.items, (items) => {
reaction(() => CatalogCategoryRegistry.getInstance().items, (items) => {
if (!this.activeTab && items.length > 0) {
this.activeTab = items[0].getId();
this.activeTab = items[0].id;
this.catalogEntityStore.activeCategory = items[0];
}
}),
@ -105,33 +99,16 @@ export class Catalog extends React.Component<Props> {
item.onRun(catalogEntityRunContext);
}
onMenuItemClick(menuItem: CatalogEntityContextMenu) {
if (menuItem.confirm) {
ConfirmDialog.open({
okButtonProps: {
primary: false,
accent: true,
},
ok: () => {
menuItem.onClick();
},
message: menuItem.confirm.message
});
} else {
menuItem.onClick();
}
}
get categories() {
return catalogCategoryRegistry.items;
return CatalogCategoryRegistry.getInstance().items;
}
@action
onTabChange = (tabId: string | null) => {
const activeCategory = this.categories.find(category => category.getId() === tabId);
const activeCategory = this.categories.find(category => category.id === tabId);
this.catalogEntityStore.activeCategory = activeCategory;
this.activeTab = activeCategory?.getId();
this.activeTab = activeCategory?.id;
};
renderNavigation() {
@ -148,10 +125,10 @@ export class Catalog extends React.Component<Props> {
{
this.categories.map(category => (
<Tab
value={category.getId()}
key={category.getId()}
value={category.id}
key={category.id}
label={category.metadata.name}
data-testid={`${category.getId()}-tab`}
data-testid={`${category.id}-tab`}
/>
))
}
@ -160,14 +137,13 @@ export class Catalog extends React.Component<Props> {
);
}
renderItemMenu = (item: CatalogEntityItem) => {
const menuItems = this.contextMenu.menuItems.filter((menuItem) => !menuItem.onlyVisibleForSource || menuItem.onlyVisibleForSource === item.entity.metadata.source);
@boundMethod
renderItemMenu(item: CatalogEntityItem) {
return (
<MenuActions onOpen={() => item.onContextMenuOpen(this.contextMenu)}>
<MenuActions onOpen={() => this.menuItems.replace(item.onContextMenuOpen())}>
{
menuItems.map((menuItem, index) => (
<MenuItem key={index} onClick={() => this.onMenuItemClick(menuItem)}>
this.menuItems.map((menuItem, index) => (
<MenuItem key={index} onClick={menuItem.onClick}>
{menuItem.title}
</MenuItem>
))
@ -177,10 +153,10 @@ export class Catalog extends React.Component<Props> {
</MenuItem>
</MenuActions>
);
};
}
renderIcon(item: CatalogEntityItem) {
const category = catalogCategoryRegistry.getCategoryForEntity(item.entity);
const category = CatalogCategoryRegistry.getInstance().getCategoryForEntity(item.entity);
if (!category) {
return null;

View File

@ -26,7 +26,7 @@ import { reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import { nodesStore } from "../+nodes/nodes.store";
import { podsStore } from "../+workloads-pods/pods.store";
import { ClusterStore, getHostedCluster } from "../../../common/cluster-store";
import { getHostedCluster } from "../../../common/cluster-store";
import { interval } from "../../utils";
import { TabLayout } from "../layout/tab-layout";
import { Spinner } from "../spinner";
@ -87,7 +87,7 @@ export class ClusterOverview extends React.Component {
render() {
const isLoaded = nodesStore.isLoaded && podsStore.isLoaded;
const isMetricsHidden = ClusterStore.getInstance().isMetricHidden(ResourceType.Cluster);
const isMetricsHidden = getHostedCluster().isMetricHidden(ResourceType.Cluster);
return (
<TabLayout>

View File

@ -28,8 +28,8 @@ import { observer } from "mobx-react";
import { PageLayout } from "../layout/page-layout";
import { navigation } from "../../navigation";
import { Tabs, Tab } from "../tabs";
import type { CatalogEntity } from "../../api/catalog-entity";
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
import type { CatalogEntity } from "../../catalog";
import { CatalogEntityRegistry } from "../../catalog";
import { entitySettingRegistry } from "../../../extensions/registries";
import type { EntitySettingsRouteParams } from "./entity-settings.route";
import { groupBy } from "lodash";
@ -51,7 +51,7 @@ export class EntitySettings extends React.Component<Props> {
}
get entity(): CatalogEntity {
return catalogEntityRegistry.getById(this.entityId);
return CatalogEntityRegistry.getInstance().getById(this.entityId);
}
get menuItems() {

View File

@ -36,7 +36,7 @@ import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
import { getBackendServiceNamePort } from "../../api/endpoints/ingress.api";
import { ResourceType } from "../cluster-settings/components/cluster-metrics-setting";
import { ClusterStore } from "../../../common/cluster-store";
import { ClusterPreferencesStore } from "../../../common/cluster-store";
interface Props extends KubeObjectDetailsProps<Ingress> {
}
@ -130,7 +130,7 @@ export class IngressDetails extends React.Component<Props> {
"Network",
"Duration",
];
const isMetricHidden = ClusterStore.getInstance().isMetricHidden(ResourceType.Ingress);
const isMetricHidden = ClusterPreferencesStore.getInstance().isMetricHidden(ResourceType.Ingress);
const { serviceName, servicePort } = ingress.getServiceNamePort();
return (

View File

@ -39,7 +39,7 @@ import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { KubeEventDetails } from "../+events/kube-event-details";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
import { ResourceType } from "../cluster-settings/components/cluster-metrics-setting";
import { ClusterStore } from "../../../common/cluster-store";
import { ClusterPreferencesStore } from "../../../common/cluster-store";
interface Props extends KubeObjectDetailsProps<Node> {
}
@ -75,7 +75,7 @@ export class NodeDetails extends React.Component<Props> {
"Disk",
"Pods",
];
const isMetricHidden = ClusterStore.getInstance().isMetricHidden(ResourceType.Node);
const isMetricHidden = ClusterPreferencesStore.getInstance().isMetricHidden(ResourceType.Node);
return (
<div className="NodeDetails">

View File

@ -36,7 +36,7 @@ import { getDetailsUrl, KubeObjectDetailsProps, KubeObjectMeta } from "../kube-o
import type { PersistentVolumeClaim } from "../../api/endpoints";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
import { ResourceType } from "../cluster-settings/components/cluster-metrics-setting";
import { ClusterStore } from "../../../common/cluster-store";
import { ClusterPreferencesStore } from "../../../common/cluster-store";
interface Props extends KubeObjectDetailsProps<PersistentVolumeClaim> {
}
@ -64,7 +64,7 @@ export class PersistentVolumeClaimDetails extends React.Component<Props> {
const metricTabs = [
"Disk"
];
const isMetricHidden = ClusterStore.getInstance().isMetricHidden(ResourceType.VolumeClaim);
const isMetricHidden = ClusterPreferencesStore.getInstance().isMetricHidden(ResourceType.VolumeClaim);
return (
<div className="PersistentVolumeClaimDetails">

View File

@ -40,7 +40,7 @@ import { PodDetailsList } from "../+workloads-pods/pod-details-list";
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
import { ResourceType } from "../cluster-settings/components/cluster-metrics-setting";
import { ClusterStore } from "../../../common/cluster-store";
import { ClusterPreferencesStore } from "../../../common/cluster-store";
interface Props extends KubeObjectDetailsProps<DaemonSet> {
}
@ -70,7 +70,7 @@ export class DaemonSetDetails extends React.Component<Props> {
const nodeSelector = daemonSet.getNodeSelectors();
const childPods = daemonSetStore.getChildPods(daemonSet);
const metrics = daemonSetStore.metrics;
const isMetricHidden = ClusterStore.getInstance().isMetricHidden(ResourceType.DaemonSet);
const isMetricHidden = ClusterPreferencesStore.getInstance().isMetricHidden(ResourceType.DaemonSet);
return (
<div className="DaemonSetDetails">

View File

@ -41,7 +41,7 @@ import { PodDetailsList } from "../+workloads-pods/pod-details-list";
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
import { ResourceType } from "../cluster-settings/components/cluster-metrics-setting";
import { ClusterStore } from "../../../common/cluster-store";
import { ClusterPreferencesStore } from "../../../common/cluster-store";
import { replicaSetStore } from "../+workloads-replicasets/replicasets.store";
import { DeploymentReplicaSets } from "./deployment-replicasets";
@ -74,7 +74,7 @@ export class DeploymentDetails extends React.Component<Props> {
const childPods = deploymentStore.getChildPods(deployment);
const replicaSets = replicaSetStore.getReplicaSetsByOwner(deployment);
const metrics = deploymentStore.metrics;
const isMetricHidden = ClusterStore.getInstance().isMetricHidden(ResourceType.Deployment);
const isMetricHidden = ClusterPreferencesStore.getInstance().isMetricHidden(ResourceType.Deployment);
return (
<div className="DeploymentDetails">

View File

@ -34,7 +34,7 @@ import type { IMetrics } from "../../api/endpoints/metrics.api";
import { ContainerCharts } from "./container-charts";
import { ResourceType } from "../cluster-settings/components/cluster-metrics-setting";
import { LocaleDate } from "../locale-date";
import { ClusterStore } from "../../../common/cluster-store";
import { ClusterPreferencesStore } from "../../../common/cluster-store";
interface Props {
pod: Pod;
@ -89,7 +89,7 @@ export class PodDetailsContainer extends React.Component<Props> {
"Memory",
"Filesystem",
];
const isMetricHidden = ClusterStore.getInstance().isMetricHidden(ResourceType.Container);
const isMetricHidden = ClusterPreferencesStore.getInstance().isMetricHidden(ResourceType.Container);
return (
<div className="PodDetailsContainer">

View File

@ -44,7 +44,7 @@ import { PodCharts, podMetricTabs } from "./pod-charts";
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
import { ResourceType } from "../cluster-settings/components/cluster-metrics-setting";
import { ClusterStore } from "../../../common/cluster-store";
import { ClusterPreferencesStore } from "../../../common/cluster-store";
interface Props extends KubeObjectDetailsProps<Pod> {
}
@ -94,7 +94,7 @@ export class PodDetails extends React.Component<Props> {
const nodeSelector = pod.getNodeSelectors();
const volumes = pod.getVolumes();
const metrics = podsStore.metrics;
const isMetricHidden = ClusterStore.getInstance().isMetricHidden(ResourceType.Pod);
const isMetricHidden = ClusterPreferencesStore.getInstance().isMetricHidden(ResourceType.Pod);
return (
<div className="PodDetails">

View File

@ -39,7 +39,7 @@ import { PodDetailsList } from "../+workloads-pods/pod-details-list";
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
import { ResourceType } from "../cluster-settings/components/cluster-metrics-setting";
import { ClusterStore } from "../../../common/cluster-store";
import { ClusterPreferencesStore } from "../../../common/cluster-store";
interface Props extends KubeObjectDetailsProps<ReplicaSet> {
}
@ -70,7 +70,7 @@ export class ReplicaSetDetails extends React.Component<Props> {
const nodeSelector = replicaSet.getNodeSelectors();
const images = replicaSet.getImages();
const childPods = replicaSetStore.getChildPods(replicaSet);
const isMetricHidden = ClusterStore.getInstance().isMetricHidden(ResourceType.ReplicaSet);
const isMetricHidden = ClusterPreferencesStore.getInstance().isMetricHidden(ResourceType.ReplicaSet);
return (
<div className="ReplicaSetDetails">

View File

@ -40,7 +40,7 @@ import { PodDetailsList } from "../+workloads-pods/pod-details-list";
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
import { ResourceType } from "../cluster-settings/components/cluster-metrics-setting";
import { ClusterStore } from "../../../common/cluster-store";
import { ClusterPreferencesStore } from "../../../common/cluster-store";
interface Props extends KubeObjectDetailsProps<StatefulSet> {
}
@ -69,7 +69,7 @@ export class StatefulSetDetails extends React.Component<Props> {
const nodeSelector = statefulSet.getNodeSelectors();
const childPods = statefulSetStore.getChildPods(statefulSet);
const metrics = statefulSetStore.metrics;
const isMetricHidden = ClusterStore.getInstance().isMetricHidden(ResourceType.StatefulSet);
const isMetricHidden = ClusterPreferencesStore.getInstance().isMetricHidden(ResourceType.StatefulSet);
return (
<div className="StatefulSetDetails">

View File

@ -30,8 +30,7 @@ import { requestMain, subscribeToBroadcast } from "../../../common/ipc";
import { Icon } from "../icon";
import { Button } from "../button";
import { cssNames, IClassName } from "../../utils";
import type { Cluster } from "../../../main/cluster";
import { ClusterId, ClusterStore } from "../../../common/cluster-store";
import { ClusterId, ClusterPreferencesStore } from "../../../common/cluster-store";
import { CubeSpinner } from "../spinner";
import { clusterActivateHandler } from "../../../common/cluster-ipc";
@ -50,8 +49,8 @@ export class ClusterStatus extends React.Component<Props> {
makeObservable(this);
}
get cluster(): Cluster {
return ClusterStore.getInstance().getById(this.props.clusterId);
get cluster() {
return ClusterPreferencesStore.getInstance().getById(this.props.clusterId);
}
@computed get hasErrors(): boolean {

View File

@ -26,16 +26,19 @@ import { disposeOnUnmount, observer } from "mobx-react";
import { ClusterStatus } from "./cluster-status";
import { hasLoadedView, initView, refreshViews } from "./lens-views";
import type { Cluster } from "../../../main/cluster";
import { ClusterStore } from "../../../common/cluster-store";
import { ClusterPreferencesStore } from "../../../common/cluster-store";
import { requestMain } from "../../../common/ipc";
import { clusterActivateHandler } from "../../../common/cluster-ipc";
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
import { getMatchedClusterId, navigate } from "../../navigation";
import { catalogURL } from "../+catalog/catalog.route";
import { CatalogEntityRegistry } from "../../catalog/catalog-entity-registry";
import { catalogURL } from "../+catalog";
interface Props extends RouteComponentProps<IClusterViewRouteParams> {
}
@observer
export class ClusterView extends React.Component {
constructor(props: {}) {
export class ClusterView extends React.Component<Props> {
constructor(props: Props) {
super(props);
makeObservable(this);
}
@ -44,8 +47,8 @@ export class ClusterView extends React.Component {
return getMatchedClusterId();
}
@computed get cluster(): Cluster | undefined {
return ClusterStore.getInstance().getById(this.clusterId);
get cluster(): Cluster {
return ClusterPreferencesStore.getInstance().getById(this.clusterId);
}
@computed get isReady(): boolean {
@ -64,7 +67,7 @@ export class ClusterView extends React.Component {
refreshViews(clusterId); // refresh visibility of active cluster
initView(clusterId); // init cluster-view (iframe), requires parent container #lens-views to be in DOM
requestMain(clusterActivateHandler, clusterId, false); // activate and fetch cluster's state from main
catalogEntityRegistry.activeEntity = catalogEntityRegistry.getById(clusterId);
CatalogEntityRegistry.getInstance().activeEntity = CatalogEntityRegistry.getInstance().getById(clusterId);
}, {
fireImmediately: true,
}),

View File

@ -20,8 +20,9 @@
*/
import { observable, when } from "mobx";
import { ClusterId, ClusterStore, getClusterFrameUrl } from "../../../common/cluster-store";
import { ClusterId, ClusterPreferencesStore, getClusterFrameUrl } from "../../../common/cluster-store";
import logger from "../../../main/logger";
import { CatalogEntityRegistry } from "../../catalog";
export interface LensView {
isLoaded?: boolean
@ -36,7 +37,13 @@ export function hasLoadedView(clusterId: ClusterId): boolean {
}
export async function initView(clusterId: ClusterId) {
const cluster = ClusterStore.getInstance().getById(clusterId);
refreshViews(clusterId);
if (!clusterId || lensViews.has(clusterId)) {
return;
}
const cluster = CatalogEntityRegistry.getInstance().getById(clusterId);
if (!cluster || lensViews.has(clusterId)) {
return;
@ -46,7 +53,7 @@ export async function initView(clusterId: ClusterId) {
const parentElem = document.getElementById("lens-views");
const iframe = document.createElement("iframe");
iframe.name = cluster.contextName;
iframe.name = cluster.name;
iframe.setAttribute("src", getClusterFrameUrl(clusterId));
iframe.addEventListener("load", () => {
logger.info(`[LENS-VIEW]: loaded from ${iframe.src}`);
@ -62,7 +69,7 @@ export async function initView(clusterId: ClusterId) {
export async function autoCleanOnRemove(clusterId: ClusterId, iframe: HTMLIFrameElement) {
await when(() => {
const cluster = ClusterStore.getInstance().getById(clusterId);
const cluster = ClusterPreferencesStore.getInstance().getById(clusterId);
return !cluster || (cluster.disconnected && lensViews.get(clusterId)?.isLoaded);
});
@ -80,7 +87,7 @@ export async function autoCleanOnRemove(clusterId: ClusterId, iframe: HTMLIFrame
export function refreshViews(visibleClusterId?: string) {
logger.info(`[LENS-VIEW]: refreshing iframe views, visible cluster id=${visibleClusterId}`);
const cluster = ClusterStore.getInstance().getById(visibleClusterId);
const cluster = !visibleClusterId ? null : ClusterPreferencesStore.getInstance().getById(visibleClusterId);
lensViews.forEach(({ clusterId, view, isLoaded }) => {
const isCurrent = clusterId === cluster?.id;

View File

@ -22,7 +22,7 @@
import { navigate } from "../../navigation";
import { commandRegistry } from "../../../extensions/registries/command-registry";
import { entitySettingsURL } from "../+entity-settings";
import { ClusterStore } from "../../../common/cluster-store";
import { ClusterPreferencesStore } from "../../../common/cluster-store";
commandRegistry.add({
id: "cluster.viewCurrentClusterSettings",
@ -30,7 +30,7 @@ commandRegistry.add({
scope: "global",
action: () => navigate(entitySettingsURL({
params: {
entityId: ClusterStore.getInstance().active.id
entityId: ClusterPreferencesStore.getInstance().active.id
}
})),
isActive: (context) => !!context.entity

View File

@ -20,7 +20,7 @@
*/
import React from "react";
import { ClusterStore } from "../../../common/cluster-store";
import { ClusterPreferencesStore } from "../../../common/cluster-store";
import { ClusterProxySetting } from "./components/cluster-proxy-setting";
import { ClusterNameSetting } from "./components/cluster-name-setting";
import { ClusterHomeDirSetting } from "./components/cluster-home-dir-setting";
@ -30,11 +30,11 @@ import { ShowMetricsSetting } from "./components/show-metrics";
import { ClusterPrometheusSetting } from "./components/cluster-prometheus-setting";
import { ClusterKubeconfig } from "./components/cluster-kubeconfig";
import { entitySettingRegistry } from "../../../extensions/registries";
import type { CatalogEntity } from "../../api/catalog-entity";
import type { CatalogEntity } from "../../catalog";
function getClusterForEntity(entity: CatalogEntity) {
return ClusterStore.getInstance().getById(entity.metadata.uid);
return ClusterPreferencesStore.getInstance().getById(entity.metadata.uid);
}
entitySettingRegistry.add([

View File

@ -25,11 +25,11 @@ import { computed, makeObservable, observable } from "mobx";
import { observer } from "mobx-react";
import React from "react";
import { commandRegistry } from "../../../extensions/registries/command-registry";
import { ClusterStore } from "../../../common/cluster-store";
import { CommandOverlay } from "./command-container";
import { broadcastMessage } from "../../../common/ipc";
import { navigate } from "../../navigation";
import { clusterViewURL } from "../cluster-manager/cluster-view.route";
import { CatalogEntityRegistry } from "../../catalog";
@observer
export class CommandDialog extends React.Component {
@ -46,7 +46,7 @@ export class CommandDialog extends React.Component {
};
return commandRegistry.getItems().filter((command) => {
if (command.scope === "entity" && !ClusterStore.getInstance().active) {
if (command.scope === "entity" && !CatalogEntityRegistry.getInstance().activeEntity) {
return false;
}

View File

@ -22,11 +22,7 @@
import React, { DOMAttributes } from "react";
import { makeObservable, observable } from "mobx";
import { observer } from "mobx-react";
import type { CatalogEntity, CatalogEntityContextMenuContext } from "../../../common/catalog";
import { catalogCategoryRegistry } from "../../api/catalog-category-registry";
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
import { navigate } from "../../navigation";
import { CatalogCategoryRegistry, CatalogEntity, CatalogEntityRegistry, TransformedMenuItem } from "../../catalog";
import { cssNames, IClassName } from "../../utils";
import { Icon } from "../icon";
import { HotbarIcon } from "./hotbar-icon";
@ -43,23 +39,16 @@ interface Props extends DOMAttributes<HTMLElement> {
@observer
export class HotbarEntityIcon extends React.Component<Props> {
@observable private contextMenu: CatalogEntityContextMenuContext;
menuItems = observable.array<TransformedMenuItem>();
constructor(props: Props) {
super(props);
makeObservable(this);
}
componentDidMount() {
this.contextMenu = {
menuItems: [],
navigate: (url: string) => navigate(url)
};
}
get kindIcon() {
const className = "badge";
const category = catalogCategoryRegistry.getCategoryForEntity(this.props.entity);
const category = CatalogCategoryRegistry.getInstance().getCategoryForEntity(this.props.entity);
if (!category) {
return <Icon material="bug_report" className={className} />;
@ -67,9 +56,9 @@ export class HotbarEntityIcon extends React.Component<Props> {
if (category.metadata.icon.includes("<svg")) {
return <Icon svg={category.metadata.icon} className={className} />;
} else {
return <Icon material={category.metadata.icon} className={className} />;
}
return <Icon material={category.metadata.icon} className={className} />;
}
get ledIcon() {
@ -79,7 +68,7 @@ export class HotbarEntityIcon extends React.Component<Props> {
}
isActive(item: CatalogEntity) {
return catalogEntityRegistry.activeEntity?.metadata?.uid == item.getId();
return CatalogEntityRegistry.getInstance().activeEntity?.metadata?.uid == item.id;
}
isPersisted(entity: CatalogEntity) {
@ -87,10 +76,6 @@ export class HotbarEntityIcon extends React.Component<Props> {
}
render() {
if (!this.contextMenu) {
return null;
}
const {
entity, errorClass, add, remove,
index, children, ...elemProps
@ -100,24 +85,16 @@ export class HotbarEntityIcon extends React.Component<Props> {
active: this.isActive(entity),
disabled: !entity
});
const onOpen = async () => {
await entity.onContextMenuOpen(this.contextMenu);
};
const isActive = this.isActive(entity);
const isPersisted = this.isPersisted(entity);
const menuItems = this.contextMenu?.menuItems.filter((menuItem) => !menuItem.onlyVisibleForSource || menuItem.onlyVisibleForSource === entity.metadata.source);
if (!isPersisted) {
menuItems.unshift({
const persistAction = this.isPersisted(entity)
? ({
title: "Pin to Hotbar",
onClick: () => add(entity, index)
});
} else {
menuItems.unshift({
})
: ({
title: "Unpin from Hotbar",
onClick: () => remove(entity.metadata.uid)
});
}
return (
<HotbarIcon
@ -126,8 +103,8 @@ export class HotbarEntityIcon extends React.Component<Props> {
source={entity.metadata.source}
className={className}
active={isActive}
onMenuOpen={onOpen}
menuItems={menuItems}
onMenuOpen={() => this.menuItems.replace(CatalogCategoryRegistry.getInstance().runEntityHandlersFor(entity, "onContextMenuOpen"))}
menuItems={[...this.menuItems, persistAction]}
{...elemProps}
>
{ this.ledIcon }

View File

@ -23,13 +23,12 @@ import "./hotbar-icon.scss";
import React, { DOMAttributes, useState } from "react";
import type { CatalogEntityContextMenu } from "../../../common/catalog";
import { cssNames, IClassName } from "../../utils";
import { ConfirmDialog } from "../confirm-dialog";
import { Menu, MenuItem } from "../menu";
import { MaterialTooltip } from "../material-tooltip/material-tooltip";
import { observer } from "mobx-react";
import { Avatar } from "../avatar/avatar";
import type { TransformedMenuItem } from "../../catalog";
interface Props extends DOMAttributes<HTMLElement> {
uid: string;
@ -38,27 +37,10 @@ interface Props extends DOMAttributes<HTMLElement> {
onMenuOpen?: () => void;
className?: IClassName;
active?: boolean;
menuItems?: CatalogEntityContextMenu[];
menuItems?: TransformedMenuItem[];
disabled?: boolean;
}
function onMenuItemClick(menuItem: CatalogEntityContextMenu) {
if (menuItem.confirm) {
ConfirmDialog.open({
okButtonProps: {
primary: false,
accent: true,
},
ok: () => {
menuItem.onClick();
},
message: menuItem.confirm.message
});
} else {
menuItem.onClick();
}
}
export const HotbarIcon = observer(({menuItems = [], ...props}: Props) => {
const { uid, title, active, className, source, disabled, onMenuOpen, children, ...rest } = props;
const id = `hotbarIcon-${uid}`;
@ -95,13 +77,11 @@ export const HotbarIcon = observer(({menuItems = [], ...props}: Props) => {
toggleMenu();
}}
close={() => toggleMenu()}>
{ menuItems.map((menuItem) => {
return (
<MenuItem key={menuItem.title} onClick={() => onMenuItemClick(menuItem) }>
{menuItem.title}
</MenuItem>
);
})}
{menuItems.map((menuItem) => (
<MenuItem key={menuItem.title} onClick={menuItem.onClick}>
{menuItem.title}
</MenuItem>
))}
</Menu>
</div>
);

View File

@ -26,9 +26,8 @@ import React from "react";
import { observer } from "mobx-react";
import { HotbarEntityIcon } from "./hotbar-entity-icon";
import { cssNames, IClassName } from "../../utils";
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
import { defaultHotbarCells, HotbarItem, HotbarStore } from "../../../common/hotbar-store";
import { CatalogEntity, CatalogEntityContextMenu, catalogEntityRunContext } from "../../api/catalog-entity";
import { CatalogEntity, MenuEntry, catalogEntityRunContext, CatalogEntityRegistry } from "../../catalog";
import { DragDropContext, Draggable, Droppable, DropResult } from "react-beautiful-dnd";
import { HotbarSelector } from "./hotbar-selector";
import { HotbarCell } from "./hotbar-cell";
@ -52,7 +51,7 @@ export class HotbarMenu extends React.Component<Props> {
return null;
}
return item ? catalogEntityRegistry.items.find((entity) => entity.metadata.uid === item.entity.uid) : null;
return item ? CatalogEntityRegistry.getInstance().items.find((entity) => entity.metadata.uid === item.entity.uid) : null;
}
onDragEnd(result: DropResult) {
@ -92,7 +91,7 @@ export class HotbarMenu extends React.Component<Props> {
@computed get items() {
const items = this.hotbar.items;
const activeEntity = catalogEntityRegistry.activeEntity;
const activeEntity = CatalogEntityRegistry.getInstance().activeEntity;
if (!activeEntity) return items;
@ -111,7 +110,7 @@ export class HotbarMenu extends React.Component<Props> {
renderGrid() {
return this.items.map((item, index) => {
const entity = this.getEntity(item);
const disabledMenuItems: CatalogEntityContextMenu[] = [
const disabledMenuItems: MenuEntry[] = [
{
title: "Unpin from Hotbar",
onClick: () => this.removeItem(item.entity.uid)

View File

@ -27,7 +27,7 @@ import "@testing-library/jest-dom/extend-expect";
import { MainLayoutHeader } from "../main-layout-header";
import { Cluster } from "../../../../main/cluster";
import { ClusterStore } from "../../../../common/cluster-store";
import { ClusterPreferencesStore } from "../../../../common/cluster-store";
import mockFs from "mock-fs";
describe("<MainLayoutHeader />", () => {
@ -60,7 +60,7 @@ describe("<MainLayoutHeader />", () => {
mockFs(mockOpts);
ClusterStore.createInstance();
ClusterPreferencesStore.createInstance();
cluster = new Cluster({
id: "foo",
@ -70,7 +70,7 @@ describe("<MainLayoutHeader />", () => {
});
afterEach(() => {
ClusterStore.resetInstance();
ClusterPreferencesStore.resetInstance();
mockFs.restore();
});

View File

@ -0,0 +1,138 @@
/**
* 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 { clusterDisconnectHandler } from "../../common/cluster-ipc";
import { ClusterPreferencesStore } from "../../common/cluster-store";
import { requestMain } from "../../common/ipc";
import { CatalogCategoryRegistry, MenuContext, MenuEntry } from "../catalog";
import { KubernetesCluster } from "../catalog-entities";
import { productName } from "../../common/vars";
import { WebLink } from "../catalog-entities/web-link";
export function initCatalogCategoryHandlers() {
const registry = CatalogCategoryRegistry.getInstance();
/**
* KubernetesCluster
*/
registry.add({
apiVersion: "catalog.k8slens.dev/v1alpha1",
kind: "CatalogCategory",
metadata: {
name: "Kubernetes Clusters",
icon: require(`!!raw-loader!./catalog-icons/kubernetes.svg`).default // eslint-disable-line
},
spec: {
group: "entity.k8slens.dev",
versions: [
{
version: "v1alpha1",
entityConstructor: KubernetesCluster,
}
],
names: {
kind: "KubernetesCluster"
}
}
});
registry.registerHandler(
"entity.k8slens.dev/v1alpha1",
"KubernetesCluster",
"onCatalogAddMenu",
(ctx: MenuContext) => [
{
icon: "text_snippet",
title: "Add from kubeconfig",
onClick: () => {
ctx.navigate("/add-cluster");
}
}
]
);
registry.registerHandler(
"entity.k8slens.dev/v1alpha1",
"KubernetesCluster",
"onContextMenuOpen",
(entity: KubernetesCluster, ctx: MenuContext) => {
const res: MenuEntry[] = [
{
title: "Settings",
onlyVisibleForSource: "local",
onClick: () => ctx.navigate(`/entity/${entity.metadata.uid}/settings`)
}
];
if (entity.metadata.labels["file"]?.startsWith(ClusterPreferencesStore.storedKubeConfigFolder)) {
res.push({
title: "Delete",
onlyVisibleForSource: "local",
onClick: () => ClusterPreferencesStore.getInstance().removeById(entity.metadata.uid),
confirm: {
message: `Remove Kubernetes Cluster "${entity.metadata.name} from ${productName}?`
}
});
}
if (entity.status.phase == "connected") {
res.push({
title: "Disconnect",
onClick: () => {
ClusterPreferencesStore.getInstance().deactivate(entity.metadata.uid);
requestMain(clusterDisconnectHandler, entity.metadata.uid);
}
});
} else {
res.push({
title: "Connect",
onClick: () => {
ctx.navigate(`/cluster/${entity.metadata.uid}`);
}
});
}
return res;
}
);
/**
* WebLink
*/
registry.add({
apiVersion: "catalog.k8slens.dev/v1alpha1",
kind: "WebLink",
metadata: {
name: "Web Links",
icon: "link"
},
spec: {
group: "entity.k8slens.dev",
versions: [
{
version: "v1alpha1",
entityConstructor: WebLink,
}
],
names: {
kind: "WebLink"
}
}
});
}

View File

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -0,0 +1,22 @@
/**
* 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.
*/
export * from "./catalog-categories";

View File

@ -26,7 +26,7 @@ import { Notifications, notificationsStore } from "../components/notifications";
import { Button } from "../components/button";
import { isMac } from "../../common/vars";
import { invalidKubeconfigHandler } from "./invalid-kubeconfig-handler";
import { ClusterStore } from "../../common/cluster-store";
import { ClusterPreferencesStore } from "../../common/cluster-store";
import { navigate } from "../navigation";
import { entitySettingsURL } from "../components/+entity-settings";
@ -97,7 +97,7 @@ function ListNamespacesForbiddenHandler(event: IpcRendererEvent, ...[clusterId]:
(
<div className="flex column gaps">
<b>Add Accessible Namespaces</b>
<p>Cluster <b>{ClusterStore.getInstance().getById(clusterId).name}</b> does not have permissions to list namespaces. Please add the namespaces you have access to.</p>
<p>Cluster <b>{ClusterPreferencesStore.getInstance().getById(clusterId).name}</b> does not have permissions to list namespaces. Please add the namespaces you have access to.</p>
<div className="flex gaps row align-left box grow">
<Button active outlined label="Go to Accessible Namespaces Settings" onClick={()=> {
navigate(entitySettingsURL({ params: { entityId: clusterId }, fragment: "accessible-namespaces" }));

View File

@ -21,7 +21,7 @@
import React from "react";
import { ipcRenderer, IpcRendererEvent, shell } from "electron";
import { ClusterStore } from "../../common/cluster-store";
import { ClusterPreferencesStore } from "../../common/cluster-store";
import { InvalidKubeConfigArgs, InvalidKubeconfigChannel } from "../../common/ipc/invalid-kubeconfig";
import { Notifications, notificationsStore } from "../components/notifications";
import { Button } from "../components/button";
@ -32,13 +32,13 @@ export const invalidKubeconfigHandler = {
channel: InvalidKubeconfigChannel,
listener: InvalidKubeconfigListener,
verifier: (args: [unknown]): args is InvalidKubeConfigArgs => {
return args.length === 1 && typeof args[0] === "string" && !!ClusterStore.getInstance().getById(args[0]);
return args.length === 1 && typeof args[0] === "string" && !!ClusterPreferencesStore.getInstance().getById(args[0]);
},
};
function InvalidKubeconfigListener(event: IpcRendererEvent, ...[clusterId]: InvalidKubeConfigArgs): void {
const notificationId = `invalid-kubeconfig:${clusterId}`;
const cluster = ClusterStore.getInstance().getById(clusterId);
const cluster = ClusterPreferencesStore.getInstance().getById(clusterId);
const contextName = cluster.name !== cluster.contextName ? `(context: ${cluster.contextName})` : "";
Notifications.error(
@ -51,7 +51,7 @@ function InvalidKubeconfigListener(event: IpcRendererEvent, ...[clusterId]: Inva
<p>Do you want to remove the cluster now?</p>
<div className="flex gaps row align-left box grow">
<Button active outlined label="Remove" onClick={()=> {
ClusterStore.getInstance().removeById(clusterId);
ClusterPreferencesStore.getInstance().removeById(clusterId);
notificationsStore.remove(notificationId);
}} />
<Button active outlined label="Cancel" onClick={() => notificationsStore.remove(notificationId)} />

View File

@ -22,14 +22,14 @@ import fse from "fs-extra";
import path from "path";
import hb from "handlebars";
import { ResourceApplier } from "../../main/resource-applier";
import type { KubernetesCluster } from "../catalog-entities";
import logger from "../../main/logger";
import { app } from "electron";
import { requestMain } from "../ipc";
import { clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler } from "../cluster-ipc";
import { ClusterStore } from "../cluster-store";
import { requestMain } from "../../common/ipc";
import { clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler } from "../../common/cluster-ipc";
import yaml from "js-yaml";
import { productName } from "../vars";
import { productName } from "../../common/vars";
import type { KubernetesCluster } from "../catalog-entities";
import { ClusterManager } from "../../main/cluster-manager";
export class ResourceStack {
constructor(protected cluster: KubernetesCluster, protected name: string) {}
@ -57,7 +57,7 @@ export class ResourceStack {
}
protected async applyResources(resources: string[], extraArgs?: string[]): Promise<string> {
const clusterModel = ClusterStore.getInstance().getById(this.cluster.metadata.uid);
const clusterModel = ClusterManager.getInstance().getById(this.cluster.metadata.uid);
if (!clusterModel) {
throw new Error(`cluster not found`);
@ -81,7 +81,7 @@ export class ResourceStack {
}
protected async deleteResources(resources: string[], extraArgs?: string[]): Promise<string> {
const clusterModel = ClusterStore.getInstance().getById(this.cluster.metadata.uid);
const clusterModel = ClusterManager.getInstance().getById(this.cluster.metadata.uid);
if (!clusterModel) {
throw new Error(`cluster not found`);

View File

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

View File

@ -27,8 +27,8 @@ import { clusterViewURL } from "../components/cluster-manager/cluster-view.route
import { LensProtocolRouterRenderer } from "./router";
import { navigate } from "../navigation/helpers";
import { entitySettingsURL } from "../components/+entity-settings";
import { catalogEntityRegistry } from "../api/catalog-entity-registry";
import { ClusterStore } from "../../common/cluster-store";
import { CatalogEntityRegistry } from "../catalog/catalog-entity-registry";
import { ClusterPreferencesStore } from "../../common/cluster-store";
import { EXTENSION_NAME_MATCH, EXTENSION_PUBLISHER_MATCH, LensProtocolRouter } from "../../common/protocol-handler";
export function bindProtocolAddRouteHandlers() {
@ -54,7 +54,7 @@ export function bindProtocolAddRouteHandlers() {
navigate(addClusterURL());
})
.addInternalHandler("/entity/:entityId/settings", ({ pathname: { entityId } }) => {
const entity = catalogEntityRegistry.getById(entityId);
const entity = CatalogEntityRegistry.getInstance().getById(entityId);
if (entity) {
navigate(entitySettingsURL({ params: { entityId } }));
@ -64,7 +64,7 @@ export function bindProtocolAddRouteHandlers() {
})
// Handlers below are deprecated and only kept for backward compact purposes
.addInternalHandler("/cluster/:clusterId", ({ pathname: { clusterId } }) => {
const cluster = ClusterStore.getInstance().getById(clusterId);
const cluster = ClusterPreferencesStore.getInstance().getById(clusterId);
if (cluster) {
navigate(clusterViewURL({ params: { clusterId } }));
@ -73,7 +73,7 @@ export function bindProtocolAddRouteHandlers() {
}
})
.addInternalHandler("/cluster/:clusterId/settings", ({ pathname: { clusterId } }) => {
const cluster = ClusterStore.getInstance().getById(clusterId);
const cluster = ClusterPreferencesStore.getInstance().getById(clusterId);
if (cluster) {
navigate(entitySettingsURL({ params: { entityId: clusterId } }));

View File

@ -22,8 +22,17 @@
import { reaction } from "mobx";
import { StorageAdapter, StorageHelper } from "../storageHelper";
import { delay } from "../../../common/utils/delay";
import { ClusterPreferencesStore } from "../../../common/cluster-store";
describe("renderer/utils/StorageHelper", () => {
beforeEach(() => {
ClusterPreferencesStore.createInstance();
});
afterEach(() => {
ClusterPreferencesStore.resetInstance();
});
describe("window.localStorage might be used as StorageAdapter", () => {
type StorageModel = string;

View File

@ -26,7 +26,7 @@ import { app, remote } from "electron";
import { comparer, observable, reaction, toJS, when } from "mobx";
import fse from "fs-extra";
import { StorageHelper } from "./storageHelper";
import { ClusterStore, getHostedClusterId } from "../../common/cluster-store";
import { getHostedClusterId } from "../../common/cluster-store";
import logger from "../../main/logger";
const storage = observable({
@ -70,26 +70,18 @@ export function createStorage<T>(key: string, defaultValue: T) {
});
// remove json-file when cluster deleted
if (clusterId !== undefined) {
when(() => ClusterStore.getInstance(false)?.removedClusters.has(clusterId)).then(removeFile);
}
}
async function saveFile(state: Record<string, any> = {}) {
logger.info(`${logPrefix} saving ${filePath}`);
async function saveFile(state: Record<string, any> = {}) {
logger.info(`${logPrefix} saving ${filePath}`);
try {
await fse.ensureDir(folder, { mode: 0o755 });
await fse.writeJson(filePath, state, { spaces: 2 });
} catch (error) {
logger.error(`${logPrefix} saving failed: ${error}`, {
json: state, jsonFilePath: filePath
});
}
}
function removeFile() {
logger.debug(`${logPrefix} removing ${filePath}`);
fse.unlink(filePath).catch(Function);
try {
await fse.ensureDir(folder, { mode: 0o755 });
await fse.writeJson(filePath, state, { spaces: 2 });
} catch (error) {
logger.error(`${logPrefix} saving failed: ${error}`, {
json: state, jsonFilePath: filePath
});
}
}