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:
parent
f3b3d15e50
commit
4d05eff051
@ -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);
|
||||
});
|
||||
|
||||
@ -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": "{}" } });
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>;
|
||||
}
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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());
|
||||
}
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
59
src/extensions/core-api/main/catalog.ts
Normal file
59
src/extensions/core-api/main/catalog.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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";
|
||||
71
src/extensions/core-api/renderer/catalog.ts
Normal file
71
src/extensions/core-api/renderer/catalog.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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";
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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)();
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
108
src/main/catalog/catalog-category-registry.ts
Normal file
108
src/main/catalog/catalog-category-registry.ts
Normal 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)
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
40
src/main/catalog/catalog-entity.ts
Normal file
40
src/main/catalog/catalog-entity.ts
Normal 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;
|
||||
}
|
||||
@ -20,3 +20,5 @@
|
||||
*/
|
||||
|
||||
export * from "./catalog-entity-registry";
|
||||
export * from "./catalog-category-registry";
|
||||
export * from "./catalog-entity";
|
||||
|
||||
@ -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
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@ -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
|
||||
*
|
||||
|
||||
@ -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;
|
||||
|
||||
62
src/main/initializers/catalog-categories.ts
Normal file
62
src/main/initializers/catalog-categories.ts
Normal 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"
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
22
src/main/initializers/index.ts
Normal file
22
src/main/initializers/index.ts
Normal 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";
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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";
|
||||
}
|
||||
|
||||
22
src/renderer/catalog-entities/index.ts
Normal file
22
src/renderer/catalog-entities/index.ts
Normal 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";
|
||||
43
src/renderer/catalog-entities/kubernetes-cluster.ts
Normal file
43
src/renderer/catalog-entities/kubernetes-cluster.ts
Normal 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}`);
|
||||
};
|
||||
}
|
||||
@ -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();
|
||||
@ -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",
|
||||
96
src/renderer/catalog/catalog-categories.ts
Normal file
96
src/renderer/catalog/catalog-categories.ts
Normal 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;
|
||||
165
src/renderer/catalog/catalog-category-registry.ts
Normal file
165
src/renderer/catalog/catalog-category-registry.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
122
src/renderer/catalog/catalog-entity.ts
Normal file
122
src/renderer/catalog/catalog-entity.ts
Normal 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;
|
||||
}
|
||||
25
src/renderer/catalog/index.ts
Normal file
25
src/renderer/catalog/index.ts
Normal 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";
|
||||
@ -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)</>
|
||||
|
||||
@ -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 })
|
||||
]);
|
||||
}
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
}),
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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([
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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 }
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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();
|
||||
});
|
||||
|
||||
|
||||
138
src/renderer/initializers/catalog-categories.ts
Normal file
138
src/renderer/initializers/catalog-categories.ts
Normal 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"
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
22
src/renderer/initializers/index.ts
Normal file
22
src/renderer/initializers/index.ts
Normal 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";
|
||||
@ -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" }));
|
||||
|
||||
@ -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)} />
|
||||
|
||||
@ -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`);
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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 } }));
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user