mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
wip: catalog
Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
parent
4856fdda2f
commit
8a0cf91ab4
@ -17,7 +17,7 @@
|
|||||||
"dev-run": "nodemon --watch static/build/main.js --exec \"electron --remote-debugging-port=9223 --inspect .\"",
|
"dev-run": "nodemon --watch static/build/main.js --exec \"electron --remote-debugging-port=9223 --inspect .\"",
|
||||||
"dev:main": "yarn run compile:main --watch",
|
"dev:main": "yarn run compile:main --watch",
|
||||||
"dev:renderer": "yarn run webpack-dev-server --config webpack.renderer.ts",
|
"dev:renderer": "yarn run webpack-dev-server --config webpack.renderer.ts",
|
||||||
"dev:extension-types": "yarn run compile:extension-types --watch --progress",
|
"dev:extension-types": "yarn run compile:extension-types --watch",
|
||||||
"compile": "env NODE_ENV=production concurrently yarn:compile:*",
|
"compile": "env NODE_ENV=production concurrently yarn:compile:*",
|
||||||
"compile:main": "yarn run webpack --config webpack.main.ts",
|
"compile:main": "yarn run webpack --config webpack.main.ts",
|
||||||
"compile:renderer": "yarn run webpack --config webpack.renderer.ts",
|
"compile:renderer": "yarn run webpack --config webpack.renderer.ts",
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import mockFs from "mock-fs";
|
|||||||
import yaml from "js-yaml";
|
import yaml from "js-yaml";
|
||||||
import { Cluster } from "../../main/cluster";
|
import { Cluster } from "../../main/cluster";
|
||||||
import { ClusterStore, getClusterIdFromHost } from "../cluster-store";
|
import { ClusterStore, getClusterIdFromHost } from "../cluster-store";
|
||||||
import { workspaceStore } from "../workspace-store";
|
|
||||||
|
|
||||||
const testDataIcon = fs.readFileSync("test-data/cluster-store-migration-icon.png");
|
const testDataIcon = fs.readFileSync("test-data/cluster-store-migration-icon.png");
|
||||||
const kubeconfig = `
|
const kubeconfig = `
|
||||||
@ -76,8 +75,7 @@ describe("empty config", () => {
|
|||||||
icon: "data:image/jpeg;base64, iVBORw0KGgoAAAANSUhEUgAAA1wAAAKoCAYAAABjkf5",
|
icon: "data:image/jpeg;base64, iVBORw0KGgoAAAANSUhEUgAAA1wAAAKoCAYAAABjkf5",
|
||||||
clusterName: "minikube"
|
clusterName: "minikube"
|
||||||
},
|
},
|
||||||
kubeConfigPath: ClusterStore.embedCustomKubeConfig("foo", kubeconfig),
|
kubeConfigPath: ClusterStore.embedCustomKubeConfig("foo", kubeconfig)
|
||||||
workspace: workspaceStore.currentWorkspaceId
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -91,12 +89,6 @@ describe("empty config", () => {
|
|||||||
expect(storedCluster.enabled).toBe(true);
|
expect(storedCluster.enabled).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("adds cluster to default workspace", () => {
|
|
||||||
const storedCluster = clusterStore.getById("foo");
|
|
||||||
|
|
||||||
expect(storedCluster.workspace).toBe("default");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("removes cluster from store", async () => {
|
it("removes cluster from store", async () => {
|
||||||
await clusterStore.removeById("foo");
|
await clusterStore.removeById("foo");
|
||||||
expect(clusterStore.getById("foo")).toBeUndefined();
|
expect(clusterStore.getById("foo")).toBeUndefined();
|
||||||
@ -105,7 +97,6 @@ describe("empty config", () => {
|
|||||||
it("sets active cluster", () => {
|
it("sets active cluster", () => {
|
||||||
clusterStore.setActive("foo");
|
clusterStore.setActive("foo");
|
||||||
expect(clusterStore.active.id).toBe("foo");
|
expect(clusterStore.active.id).toBe("foo");
|
||||||
expect(workspaceStore.currentWorkspace.lastActiveClusterId).toBe("foo");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -118,8 +109,7 @@ describe("empty config", () => {
|
|||||||
preferences: {
|
preferences: {
|
||||||
clusterName: "prod"
|
clusterName: "prod"
|
||||||
},
|
},
|
||||||
kubeConfigPath: ClusterStore.embedCustomKubeConfig("prod", kubeconfig),
|
kubeConfigPath: ClusterStore.embedCustomKubeConfig("prod", kubeconfig)
|
||||||
workspace: "workstation"
|
|
||||||
}),
|
}),
|
||||||
new Cluster({
|
new Cluster({
|
||||||
id: "dev",
|
id: "dev",
|
||||||
@ -127,8 +117,7 @@ describe("empty config", () => {
|
|||||||
preferences: {
|
preferences: {
|
||||||
clusterName: "dev"
|
clusterName: "dev"
|
||||||
},
|
},
|
||||||
kubeConfigPath: ClusterStore.embedCustomKubeConfig("dev", kubeconfig),
|
kubeConfigPath: ClusterStore.embedCustomKubeConfig("dev", kubeconfig)
|
||||||
workspace: "workstation"
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -138,51 +127,11 @@ describe("empty config", () => {
|
|||||||
expect(clusterStore.clusters.size).toBe(2);
|
expect(clusterStore.clusters.size).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("gets clusters by workspaces", () => {
|
|
||||||
const wsClusters = clusterStore.getByWorkspaceId("workstation");
|
|
||||||
const defaultClusters = clusterStore.getByWorkspaceId("default");
|
|
||||||
|
|
||||||
expect(defaultClusters.length).toBe(0);
|
|
||||||
expect(wsClusters.length).toBe(2);
|
|
||||||
expect(wsClusters[0].id).toBe("prod");
|
|
||||||
expect(wsClusters[1].id).toBe("dev");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("check if cluster's kubeconfig file saved", () => {
|
it("check if cluster's kubeconfig file saved", () => {
|
||||||
const file = ClusterStore.embedCustomKubeConfig("boo", "kubeconfig");
|
const file = ClusterStore.embedCustomKubeConfig("boo", "kubeconfig");
|
||||||
|
|
||||||
expect(fs.readFileSync(file, "utf8")).toBe("kubeconfig");
|
expect(fs.readFileSync(file, "utf8")).toBe("kubeconfig");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("check if reorderring works for same from and to", () => {
|
|
||||||
clusterStore.swapIconOrders("workstation", 1, 1);
|
|
||||||
|
|
||||||
const clusters = clusterStore.getByWorkspaceId("workstation");
|
|
||||||
|
|
||||||
expect(clusters[0].id).toBe("prod");
|
|
||||||
expect(clusters[0].preferences.iconOrder).toBe(0);
|
|
||||||
expect(clusters[1].id).toBe("dev");
|
|
||||||
expect(clusters[1].preferences.iconOrder).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("check if reorderring works for different from and to", () => {
|
|
||||||
clusterStore.swapIconOrders("workstation", 0, 1);
|
|
||||||
|
|
||||||
const clusters = clusterStore.getByWorkspaceId("workstation");
|
|
||||||
|
|
||||||
expect(clusters[0].id).toBe("dev");
|
|
||||||
expect(clusters[0].preferences.iconOrder).toBe(0);
|
|
||||||
expect(clusters[1].id).toBe("prod");
|
|
||||||
expect(clusters[1].preferences.iconOrder).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("check if after icon reordering, changing workspaces still works", () => {
|
|
||||||
clusterStore.swapIconOrders("workstation", 1, 1);
|
|
||||||
clusterStore.getById("prod").workspace = "default";
|
|
||||||
|
|
||||||
expect(clusterStore.getByWorkspaceId("workstation").length).toBe(1);
|
|
||||||
expect(clusterStore.getByWorkspaceId("default").length).toBe(1);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -485,12 +434,6 @@ describe("for a pre 2.7.0-beta.0 config without a workspace", () => {
|
|||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
mockFs.restore();
|
mockFs.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("adds cluster to default workspace", async () => {
|
|
||||||
const storedClusterData = clusterStore.clustersList[0];
|
|
||||||
|
|
||||||
expect(storedClusterData.workspace).toBe("default");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("pre 3.6.0-beta.1 config with an existing cluster", () => {
|
describe("pre 3.6.0-beta.1 config with an existing cluster", () => {
|
||||||
|
|||||||
@ -1,188 +0,0 @@
|
|||||||
import mockFs from "mock-fs";
|
|
||||||
|
|
||||||
jest.mock("electron", () => {
|
|
||||||
return {
|
|
||||||
app: {
|
|
||||||
getVersion: () => "99.99.99",
|
|
||||||
getPath: () => "tmp",
|
|
||||||
getLocale: () => "en"
|
|
||||||
},
|
|
||||||
ipcMain: {
|
|
||||||
handle: jest.fn(),
|
|
||||||
on: jest.fn()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
import { Workspace, WorkspaceStore } from "../workspace-store";
|
|
||||||
|
|
||||||
describe("workspace store tests", () => {
|
|
||||||
describe("for an empty config", () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
WorkspaceStore.resetInstance();
|
|
||||||
mockFs({ tmp: { "lens-workspace-store.json": "{}" } });
|
|
||||||
|
|
||||||
await WorkspaceStore.getInstance<WorkspaceStore>().load();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
mockFs.restore();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("default workspace should always exist", () => {
|
|
||||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
|
||||||
|
|
||||||
expect(ws.workspaces.size).toBe(1);
|
|
||||||
expect(ws.getById(WorkspaceStore.defaultId)).not.toBe(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("default workspace should be enabled", () => {
|
|
||||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
|
||||||
|
|
||||||
expect(ws.workspaces.size).toBe(1);
|
|
||||||
expect(ws.getById(WorkspaceStore.defaultId).enabled).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cannot remove the default workspace", () => {
|
|
||||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
|
||||||
|
|
||||||
expect(() => ws.removeWorkspaceById(WorkspaceStore.defaultId)).toThrowError("Cannot remove");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can update workspace description", () => {
|
|
||||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
|
||||||
const workspace = ws.addWorkspace(new Workspace({
|
|
||||||
id: "foobar",
|
|
||||||
name: "foobar",
|
|
||||||
}));
|
|
||||||
|
|
||||||
workspace.description = "Foobar description";
|
|
||||||
ws.updateWorkspace(workspace);
|
|
||||||
|
|
||||||
expect(ws.getById("foobar").description).toBe("Foobar description");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can add workspaces", () => {
|
|
||||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
|
||||||
|
|
||||||
ws.addWorkspace(new Workspace({
|
|
||||||
id: "123",
|
|
||||||
name: "foobar",
|
|
||||||
}));
|
|
||||||
|
|
||||||
const workspace = ws.getById("123");
|
|
||||||
|
|
||||||
expect(workspace.name).toBe("foobar");
|
|
||||||
expect(workspace.enabled).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cannot set a non-existent workspace to be active", () => {
|
|
||||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
|
||||||
|
|
||||||
expect(() => ws.setActive("abc")).toThrow("doesn't exist");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can set a existent workspace to be active", () => {
|
|
||||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
|
||||||
|
|
||||||
ws.addWorkspace(new Workspace({
|
|
||||||
id: "abc",
|
|
||||||
name: "foobar",
|
|
||||||
}));
|
|
||||||
|
|
||||||
expect(() => ws.setActive("abc")).not.toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can remove a workspace", () => {
|
|
||||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
|
||||||
|
|
||||||
ws.addWorkspace(new Workspace({
|
|
||||||
id: "123",
|
|
||||||
name: "foobar",
|
|
||||||
}));
|
|
||||||
ws.addWorkspace(new Workspace({
|
|
||||||
id: "1234",
|
|
||||||
name: "foobar 1",
|
|
||||||
}));
|
|
||||||
ws.removeWorkspaceById("123");
|
|
||||||
|
|
||||||
expect(ws.workspaces.size).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cannot create workspace with existent name", () => {
|
|
||||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
|
||||||
|
|
||||||
ws.addWorkspace(new Workspace({
|
|
||||||
id: "someid",
|
|
||||||
name: "default",
|
|
||||||
}));
|
|
||||||
|
|
||||||
expect(ws.workspacesList.length).toBe(1); // default workspace only
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cannot create workspace with empty name", () => {
|
|
||||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
|
||||||
|
|
||||||
ws.addWorkspace(new Workspace({
|
|
||||||
id: "random",
|
|
||||||
name: "",
|
|
||||||
}));
|
|
||||||
|
|
||||||
expect(ws.workspacesList.length).toBe(1); // default workspace only
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cannot create workspace with ' ' name", () => {
|
|
||||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
|
||||||
|
|
||||||
ws.addWorkspace(new Workspace({
|
|
||||||
id: "random",
|
|
||||||
name: " ",
|
|
||||||
}));
|
|
||||||
|
|
||||||
expect(ws.workspacesList.length).toBe(1); // default workspace only
|
|
||||||
});
|
|
||||||
|
|
||||||
it("trim workspace name", () => {
|
|
||||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
|
||||||
|
|
||||||
ws.addWorkspace(new Workspace({
|
|
||||||
id: "random",
|
|
||||||
name: "default ",
|
|
||||||
}));
|
|
||||||
|
|
||||||
expect(ws.workspacesList.length).toBe(1); // default workspace only
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("for a non-empty config", () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
WorkspaceStore.resetInstance();
|
|
||||||
mockFs({
|
|
||||||
tmp: {
|
|
||||||
"lens-workspace-store.json": JSON.stringify({
|
|
||||||
currentWorkspace: "abc",
|
|
||||||
workspaces: [{
|
|
||||||
id: "abc",
|
|
||||||
name: "test"
|
|
||||||
}, {
|
|
||||||
id: "default",
|
|
||||||
name: "default"
|
|
||||||
}]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await WorkspaceStore.getInstance<WorkspaceStore>().load();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
mockFs.restore();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("doesn't revert to default workspace", async () => {
|
|
||||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
|
||||||
|
|
||||||
expect(ws.currentWorkspaceId).toBe("abc");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
42
src/common/catalog-category-registry.ts
Normal file
42
src/common/catalog-category-registry.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { action, computed, observable, toJS } from "mobx";
|
||||||
|
import { CatalogCategory, CatalogEntityData } from "./catalog-entity";
|
||||||
|
|
||||||
|
export class CatalogCategoryRegistry {
|
||||||
|
@observable protected categories: CatalogCategory[] = [];
|
||||||
|
|
||||||
|
@action add(category: CatalogCategory) {
|
||||||
|
this.categories.push(category);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action remove(category: CatalogCategory) {
|
||||||
|
this.categories = this.categories.filter((cat) => cat.apiVersion !== category.apiVersion && cat.kind !== category.kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed get items() {
|
||||||
|
return toJS(this.categories);
|
||||||
|
}
|
||||||
|
|
||||||
|
getEntityForData(data: CatalogEntityData) {
|
||||||
|
const splitApiVersion = data.apiVersion.split("/");
|
||||||
|
const group = splitApiVersion[0];
|
||||||
|
const version = splitApiVersion[1];
|
||||||
|
|
||||||
|
const category = this.categories.find((category) => {
|
||||||
|
return category.spec.group === group && category.spec.names.kind === data.kind;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!category) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const specVersion = category.spec.versions.find((v) => v.name === version);
|
||||||
|
|
||||||
|
if (!specVersion) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new specVersion.entityClass(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const catalogCategoryRegistry = new CatalogCategoryRegistry();
|
||||||
1
src/common/catalog-entities/index.ts
Normal file
1
src/common/catalog-entities/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./kubernetes-cluster";
|
||||||
73
src/common/catalog-entities/kubernetes-cluster.ts
Normal file
73
src/common/catalog-entities/kubernetes-cluster.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import { observable } from "mobx";
|
||||||
|
import { catalogCategoryRegistry } from "../catalog-category-registry";
|
||||||
|
import { CatalogCategory, CatalogEntity, CatalogEntityActionContext, CatalogEntityData, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog-entity";
|
||||||
|
import { clusterStore } from "../cluster-store";
|
||||||
|
|
||||||
|
export type KubernetesClusterSpec = {
|
||||||
|
kubeconfigPath: string;
|
||||||
|
kubeconfigContext: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface KubernetesClusterStatus extends CatalogEntityStatus {
|
||||||
|
phase: "connected" | "disconnected";
|
||||||
|
}
|
||||||
|
|
||||||
|
export class KubernetesCluster implements CatalogEntity {
|
||||||
|
public readonly apiVersion = "entity.k8slens.dev/v1alpha1";
|
||||||
|
public readonly kind = "KubernetesCluster";
|
||||||
|
@observable public metadata: CatalogEntityMetadata;
|
||||||
|
@observable public status: KubernetesClusterStatus;
|
||||||
|
@observable public spec: KubernetesClusterSpec;
|
||||||
|
|
||||||
|
constructor(data: CatalogEntityData) {
|
||||||
|
this.metadata = data.metadata;
|
||||||
|
this.status = data.status as KubernetesClusterStatus;
|
||||||
|
this.spec = data.spec as KubernetesClusterSpec;
|
||||||
|
}
|
||||||
|
|
||||||
|
getId() {
|
||||||
|
return this.metadata.uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
getName() {
|
||||||
|
return this.metadata.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
get cluster() {
|
||||||
|
return clusterStore.getById(this.metadata.uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onRun(context: CatalogEntityActionContext) {
|
||||||
|
context.navigate(`/cluster/${this.metadata.uid}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onDetailsOpen() {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
async onContextMenuOpen() {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class KubernetesClusterCategory implements CatalogCategory {
|
||||||
|
public readonly apiVersion = "catalog.k8slens.dev/v1alpha1";
|
||||||
|
public readonly kind = "CatalogCategory";
|
||||||
|
public metadata = {
|
||||||
|
name: "Kubernetes Clusters"
|
||||||
|
};
|
||||||
|
public spec = {
|
||||||
|
group: "entity.k8slens.dev",
|
||||||
|
versions: [
|
||||||
|
{
|
||||||
|
name: "v1alpha1",
|
||||||
|
entityClass: KubernetesCluster
|
||||||
|
}
|
||||||
|
],
|
||||||
|
names: {
|
||||||
|
kind: "KubernetesCluster"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
catalogCategoryRegistry.add(new KubernetesClusterCategory());
|
||||||
32
src/common/catalog-entity-registry.ts
Normal file
32
src/common/catalog-entity-registry.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { action, computed, observable } from "mobx";
|
||||||
|
import { CatalogEntity } from "./catalog-entity";
|
||||||
|
|
||||||
|
export class CatalogEntityRegistry {
|
||||||
|
@observable.deep protected sources: Map<string, CatalogEntity[]> = observable.map(new Map(), { deep: true });
|
||||||
|
|
||||||
|
@action addSource(id: string, source: CatalogEntity[]) {
|
||||||
|
this.sources.set(id, source);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action removeSource(id: string) {
|
||||||
|
this.sources.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed get items() {
|
||||||
|
const catalogItems: CatalogEntity[] = [];
|
||||||
|
|
||||||
|
for (const [id, items] of this.sources) {
|
||||||
|
items.forEach((item) => catalogItems.push(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
return catalogItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
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[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const catalogEntityRegistry = new CatalogEntityRegistry();
|
||||||
63
src/common/catalog-entity.ts
Normal file
63
src/common/catalog-entity.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
export interface CatalogCategoryVersion {
|
||||||
|
name: string;
|
||||||
|
entityClass: { new(data: CatalogEntityData): CatalogEntity };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CatalogCategory {
|
||||||
|
apiVersion: string;
|
||||||
|
kind: string;
|
||||||
|
metadata: {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
spec: {
|
||||||
|
group: string;
|
||||||
|
versions: CatalogCategoryVersion[];
|
||||||
|
names: {
|
||||||
|
kind: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CatalogEntityMetadata = {
|
||||||
|
uid: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
source?: string;
|
||||||
|
labels: {
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
[key: string]: string | object;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CatalogEntityStatus = {
|
||||||
|
phase: string;
|
||||||
|
reason?: string;
|
||||||
|
message?: string;
|
||||||
|
active?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface CatalogEntityActionContext {
|
||||||
|
navigate: (url: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CatalogEntityContextMenuContext extends CatalogEntityActionContext {
|
||||||
|
menu: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CatalogEntityData = {
|
||||||
|
apiVersion: string;
|
||||||
|
kind: string;
|
||||||
|
metadata: CatalogEntityMetadata;
|
||||||
|
status: CatalogEntityStatus;
|
||||||
|
spec: {
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface CatalogEntity extends CatalogEntityData {
|
||||||
|
getId: () => string;
|
||||||
|
getName: () => string;
|
||||||
|
onRun: (context: CatalogEntityActionContext) => Promise<void>;
|
||||||
|
onDetailsOpen: (context: CatalogEntityActionContext) => Promise<void>;
|
||||||
|
onContextMenuOpen: (context: CatalogEntityContextMenuContext) => Promise<void>;
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@ import { appEventBus } from "./event-bus";
|
|||||||
import { ResourceApplier } from "../main/resource-applier";
|
import { ResourceApplier } from "../main/resource-applier";
|
||||||
import { ipcMain, IpcMainInvokeEvent } from "electron";
|
import { ipcMain, IpcMainInvokeEvent } from "electron";
|
||||||
import { clusterFrameMap } from "./cluster-frames";
|
import { clusterFrameMap } from "./cluster-frames";
|
||||||
|
import { catalogEntityRegistry } from "./catalog-entity-registry";
|
||||||
|
|
||||||
export const clusterActivateHandler = "cluster:activate";
|
export const clusterActivateHandler = "cluster:activate";
|
||||||
export const clusterSetFrameIdHandler = "cluster:set-frame-id";
|
export const clusterSetFrameIdHandler = "cluster:set-frame-id";
|
||||||
@ -17,6 +18,10 @@ if (ipcMain) {
|
|||||||
const cluster = clusterStore.getById(clusterId);
|
const cluster = clusterStore.getById(clusterId);
|
||||||
|
|
||||||
if (cluster) {
|
if (cluster) {
|
||||||
|
catalogEntityRegistry.getItemsForApiKind("entity.k8slens.dev/v1alpha1", "KubernetesCluster").forEach((item) => {
|
||||||
|
if (item.metadata.uid === cluster.id) item.status.active = true;
|
||||||
|
});
|
||||||
|
|
||||||
return cluster.activate(force);
|
return cluster.activate(force);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -43,6 +48,9 @@ if (ipcMain) {
|
|||||||
|
|
||||||
if (cluster) {
|
if (cluster) {
|
||||||
cluster.disconnect();
|
cluster.disconnect();
|
||||||
|
catalogEntityRegistry.getItemsForApiKind("entity.k8slens.dev/v1alpha1", "KubernetesCluster").forEach((item) => {
|
||||||
|
if (item.metadata.uid === cluster.id) item.status.active = false;
|
||||||
|
});
|
||||||
clusterFrameMap.delete(cluster.id);
|
clusterFrameMap.delete(cluster.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { workspaceStore } from "./workspace-store";
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { app, ipcRenderer, remote, webFrame } from "electron";
|
import { app, ipcRenderer, remote, webFrame } from "electron";
|
||||||
import { unlink } from "fs-extra";
|
import { unlink } from "fs-extra";
|
||||||
@ -12,9 +11,7 @@ import { dumpConfigYaml } from "./kube-helpers";
|
|||||||
import { saveToAppFiles } from "./utils/saveToAppFiles";
|
import { saveToAppFiles } from "./utils/saveToAppFiles";
|
||||||
import { KubeConfig } from "@kubernetes/client-node";
|
import { KubeConfig } from "@kubernetes/client-node";
|
||||||
import { handleRequest, requestMain, subscribeToBroadcast, unsubscribeAllFromBroadcast } from "./ipc";
|
import { handleRequest, requestMain, subscribeToBroadcast, unsubscribeAllFromBroadcast } from "./ipc";
|
||||||
import _ from "lodash";
|
|
||||||
import move from "array-move";
|
import move from "array-move";
|
||||||
import type { WorkspaceId } from "./workspace-store";
|
|
||||||
import { ResourceType } from "../renderer/components/+cluster-settings/components/cluster-metrics-setting";
|
import { ResourceType } from "../renderer/components/+cluster-settings/components/cluster-metrics-setting";
|
||||||
|
|
||||||
export interface ClusterIconUpload {
|
export interface ClusterIconUpload {
|
||||||
@ -47,9 +44,6 @@ export interface ClusterModel {
|
|||||||
/** Path to cluster kubeconfig */
|
/** Path to cluster kubeconfig */
|
||||||
kubeConfigPath: string;
|
kubeConfigPath: string;
|
||||||
|
|
||||||
/** Workspace id */
|
|
||||||
workspace?: WorkspaceId;
|
|
||||||
|
|
||||||
/** User context in kubeconfig */
|
/** User context in kubeconfig */
|
||||||
contextName?: string;
|
contextName?: string;
|
||||||
|
|
||||||
@ -110,6 +104,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
@observable removedClusters = observable.map<ClusterId, Cluster>();
|
@observable removedClusters = observable.map<ClusterId, Cluster>();
|
||||||
@observable clusters = observable.map<ClusterId, Cluster>();
|
@observable clusters = observable.map<ClusterId, Cluster>();
|
||||||
|
|
||||||
|
|
||||||
private static stateRequestChannel = "cluster:states";
|
private static stateRequestChannel = "cluster:states";
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
@ -222,7 +217,6 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
const clusterId = this.clusters.has(id) ? id : null;
|
const clusterId = this.clusters.has(id) ? id : null;
|
||||||
|
|
||||||
this.activeCluster = clusterId;
|
this.activeCluster = clusterId;
|
||||||
workspaceStore.setLastActiveClusterId(clusterId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deactivate(id: ClusterId) {
|
deactivate(id: ClusterId) {
|
||||||
@ -232,8 +226,8 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
swapIconOrders(workspace: WorkspaceId, from: number, to: number) {
|
swapIconOrders(from: number, to: number) {
|
||||||
const clusters = this.getByWorkspaceId(workspace);
|
const clusters = this.enabledClustersList;
|
||||||
|
|
||||||
if (from < 0 || to < 0 || from >= clusters.length || to >= clusters.length || isNaN(from) || isNaN(to)) {
|
if (from < 0 || to < 0 || from >= clusters.length || to >= clusters.length || isNaN(from) || isNaN(to)) {
|
||||||
throw new Error(`invalid from<->to arguments`);
|
throw new Error(`invalid from<->to arguments`);
|
||||||
@ -255,13 +249,6 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
return this.clusters.get(id);
|
return this.clusters.get(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
getByWorkspaceId(workspaceId: string): Cluster[] {
|
|
||||||
const clusters = Array.from(this.clusters.values())
|
|
||||||
.filter(cluster => cluster.workspace === workspaceId);
|
|
||||||
|
|
||||||
return _.sortBy(clusters, cluster => cluster.preferences.iconOrder);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
addClusters(...models: ClusterModel[]): Cluster[] {
|
addClusters(...models: ClusterModel[]): Cluster[] {
|
||||||
const clusters: Cluster[] = [];
|
const clusters: Cluster[] = [];
|
||||||
@ -313,13 +300,6 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
|
||||||
removeByWorkspaceId(workspaceId: string) {
|
|
||||||
this.getByWorkspaceId(workspaceId).forEach(cluster => {
|
|
||||||
this.removeById(cluster.id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
protected fromStore({ activeCluster, clusters = [] }: ClusterStoreModel = {}) {
|
protected fromStore({ activeCluster, clusters = [] }: ClusterStoreModel = {}) {
|
||||||
const currentClusters = this.clusters.toJS();
|
const currentClusters = this.clusters.toJS();
|
||||||
|
|||||||
@ -1,345 +0,0 @@
|
|||||||
import { ipcRenderer } from "electron";
|
|
||||||
import { action, computed, observable, toJS, reaction } from "mobx";
|
|
||||||
import { BaseStore } from "./base-store";
|
|
||||||
import { clusterStore } from "./cluster-store";
|
|
||||||
import { appEventBus } from "./event-bus";
|
|
||||||
import { broadcastMessage, handleRequest, requestMain } from "../common/ipc";
|
|
||||||
import logger from "../main/logger";
|
|
||||||
import type { ClusterId } from "./cluster-store";
|
|
||||||
|
|
||||||
export type WorkspaceId = string;
|
|
||||||
|
|
||||||
export interface WorkspaceStoreModel {
|
|
||||||
workspaces: WorkspaceModel[];
|
|
||||||
currentWorkspace?: WorkspaceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WorkspaceModel {
|
|
||||||
id: WorkspaceId;
|
|
||||||
name: string;
|
|
||||||
description?: string;
|
|
||||||
ownerRef?: string;
|
|
||||||
lastActiveClusterId?: ClusterId;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WorkspaceState {
|
|
||||||
enabled: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateFromModel = Symbol("updateFromModel");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Workspace
|
|
||||||
*
|
|
||||||
* @beta
|
|
||||||
*/
|
|
||||||
export class Workspace implements WorkspaceModel, WorkspaceState {
|
|
||||||
/**
|
|
||||||
* Unique id for workspace
|
|
||||||
*
|
|
||||||
* @observable
|
|
||||||
*/
|
|
||||||
@observable id: WorkspaceId;
|
|
||||||
/**
|
|
||||||
* Workspace name
|
|
||||||
*
|
|
||||||
* @observable
|
|
||||||
*/
|
|
||||||
@observable name: string;
|
|
||||||
/**
|
|
||||||
* Workspace description
|
|
||||||
*
|
|
||||||
* @observable
|
|
||||||
*/
|
|
||||||
@observable description?: string;
|
|
||||||
/**
|
|
||||||
* Workspace owner reference
|
|
||||||
*
|
|
||||||
* If extension sets ownerRef then it needs to explicitly mark workspace as enabled onActivate (or when workspace is saved)
|
|
||||||
*
|
|
||||||
* @observable
|
|
||||||
*/
|
|
||||||
@observable ownerRef?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Last active cluster id
|
|
||||||
*
|
|
||||||
* @observable
|
|
||||||
*/
|
|
||||||
@observable lastActiveClusterId?: ClusterId;
|
|
||||||
|
|
||||||
|
|
||||||
@observable private _enabled: boolean;
|
|
||||||
|
|
||||||
constructor(data: WorkspaceModel) {
|
|
||||||
Object.assign(this, data);
|
|
||||||
|
|
||||||
if (!ipcRenderer) {
|
|
||||||
reaction(() => this.getState(), () => {
|
|
||||||
this.pushState();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Is workspace enabled
|
|
||||||
*
|
|
||||||
* Workspaces that don't have ownerRef will be enabled by default. Workspaces with ownerRef need to explicitly enable a workspace.
|
|
||||||
*
|
|
||||||
* @observable
|
|
||||||
*/
|
|
||||||
get enabled(): boolean {
|
|
||||||
return !this.isManaged || this._enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
set enabled(enabled: boolean) {
|
|
||||||
this._enabled = enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Is workspace managed by an extension
|
|
||||||
*/
|
|
||||||
get isManaged(): boolean {
|
|
||||||
return !!this.ownerRef;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get workspace state
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
getState(): WorkspaceState {
|
|
||||||
return toJS({
|
|
||||||
enabled: this.enabled
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Push state
|
|
||||||
*
|
|
||||||
* @internal
|
|
||||||
* @param state workspace state
|
|
||||||
*/
|
|
||||||
pushState(state = this.getState()) {
|
|
||||||
logger.silly("[WORKSPACE] pushing state", {...state, id: this.id});
|
|
||||||
broadcastMessage("workspace:state", this.id, toJS(state));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param state workspace state
|
|
||||||
*/
|
|
||||||
@action setState(state: WorkspaceState) {
|
|
||||||
Object.assign(this, state);
|
|
||||||
}
|
|
||||||
|
|
||||||
[updateFromModel] = action((model: WorkspaceModel) => {
|
|
||||||
Object.assign(this, model);
|
|
||||||
});
|
|
||||||
|
|
||||||
toJSON(): WorkspaceModel {
|
|
||||||
return toJS({
|
|
||||||
id: this.id,
|
|
||||||
name: this.name,
|
|
||||||
description: this.description,
|
|
||||||
ownerRef: this.ownerRef,
|
|
||||||
lastActiveClusterId: this.lastActiveClusterId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
|
|
||||||
static readonly defaultId: WorkspaceId = "default";
|
|
||||||
private static stateRequestChannel = "workspace:states";
|
|
||||||
|
|
||||||
@observable currentWorkspaceId = WorkspaceStore.defaultId;
|
|
||||||
@observable workspaces = observable.map<WorkspaceId, Workspace>();
|
|
||||||
|
|
||||||
private constructor() {
|
|
||||||
super({
|
|
||||||
configName: "lens-workspace-store",
|
|
||||||
});
|
|
||||||
|
|
||||||
this.workspaces.set(WorkspaceStore.defaultId, new Workspace({
|
|
||||||
id: WorkspaceStore.defaultId,
|
|
||||||
name: "default"
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
async load() {
|
|
||||||
await super.load();
|
|
||||||
type workspaceStateSync = {
|
|
||||||
id: string;
|
|
||||||
state: WorkspaceState;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (ipcRenderer) {
|
|
||||||
logger.info("[WORKSPACE-STORE] requesting initial state sync");
|
|
||||||
const workspaceStates: workspaceStateSync[] = await requestMain(WorkspaceStore.stateRequestChannel);
|
|
||||||
|
|
||||||
workspaceStates.forEach((workspaceState) => {
|
|
||||||
const workspace = this.getById(workspaceState.id);
|
|
||||||
|
|
||||||
if (workspace) {
|
|
||||||
workspace.setState(workspaceState.state);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
handleRequest(WorkspaceStore.stateRequestChannel, (): workspaceStateSync[] => {
|
|
||||||
const states: workspaceStateSync[] = [];
|
|
||||||
|
|
||||||
this.workspacesList.forEach((workspace) => {
|
|
||||||
states.push({
|
|
||||||
state: workspace.getState(),
|
|
||||||
id: workspace.id
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return states;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
registerIpcListener() {
|
|
||||||
logger.info("[WORKSPACE-STORE] starting to listen state events");
|
|
||||||
ipcRenderer.on("workspace:state", (event, workspaceId: string, state: WorkspaceState) => {
|
|
||||||
this.getById(workspaceId)?.setState(state);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
unregisterIpcListener() {
|
|
||||||
super.unregisterIpcListener();
|
|
||||||
ipcRenderer.removeAllListeners("workspace:state");
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get currentWorkspace(): Workspace {
|
|
||||||
return this.getById(this.currentWorkspaceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get workspacesList() {
|
|
||||||
return Array.from(this.workspaces.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get enabledWorkspacesList() {
|
|
||||||
return this.workspacesList.filter((w) => w.enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
pushState() {
|
|
||||||
this.workspaces.forEach((w) => {
|
|
||||||
w.pushState();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
isDefault(id: WorkspaceId) {
|
|
||||||
return id === WorkspaceStore.defaultId;
|
|
||||||
}
|
|
||||||
|
|
||||||
getById(id: WorkspaceId): Workspace {
|
|
||||||
return this.workspaces.get(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
getByName(name: string): Workspace {
|
|
||||||
return this.workspacesList.find(workspace => workspace.name === name);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
setActive(id = WorkspaceStore.defaultId) {
|
|
||||||
if (id === this.currentWorkspaceId) return;
|
|
||||||
|
|
||||||
if (!this.getById(id)) {
|
|
||||||
throw new Error(`workspace ${id} doesn't exist`);
|
|
||||||
}
|
|
||||||
this.currentWorkspaceId = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
addWorkspace(workspace: Workspace) {
|
|
||||||
const { id, name } = workspace;
|
|
||||||
|
|
||||||
if (!name.trim() || this.getByName(name.trim())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.workspaces.set(id, workspace);
|
|
||||||
|
|
||||||
if (!workspace.isManaged) {
|
|
||||||
workspace.enabled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
appEventBus.emit({name: "workspace", action: "add"});
|
|
||||||
|
|
||||||
return workspace;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
updateWorkspace(workspace: Workspace) {
|
|
||||||
this.workspaces.set(workspace.id, workspace);
|
|
||||||
appEventBus.emit({name: "workspace", action: "update"});
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
removeWorkspace(workspace: Workspace) {
|
|
||||||
this.removeWorkspaceById(workspace.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
removeWorkspaceById(id: WorkspaceId) {
|
|
||||||
const workspace = this.getById(id);
|
|
||||||
|
|
||||||
if (!workspace) return;
|
|
||||||
|
|
||||||
if (this.isDefault(id)) {
|
|
||||||
throw new Error("Cannot remove default workspace");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.currentWorkspaceId === id) {
|
|
||||||
this.currentWorkspaceId = WorkspaceStore.defaultId; // reset to default
|
|
||||||
}
|
|
||||||
this.workspaces.delete(id);
|
|
||||||
appEventBus.emit({name: "workspace", action: "remove"});
|
|
||||||
clusterStore.removeByWorkspaceId(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
setLastActiveClusterId(clusterId?: ClusterId, workspaceId = this.currentWorkspaceId) {
|
|
||||||
this.getById(workspaceId).lastActiveClusterId = clusterId;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
protected fromStore({ currentWorkspace, workspaces = [] }: WorkspaceStoreModel) {
|
|
||||||
if (currentWorkspace) {
|
|
||||||
this.currentWorkspaceId = currentWorkspace;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentWorkspaces = this.workspaces.toJS();
|
|
||||||
const newWorkspaceIds = new Set<WorkspaceId>([WorkspaceStore.defaultId]); // never delete default
|
|
||||||
|
|
||||||
for (const workspaceModel of workspaces) {
|
|
||||||
const oldWorkspace = this.workspaces.get(workspaceModel.id);
|
|
||||||
|
|
||||||
if (oldWorkspace) {
|
|
||||||
oldWorkspace[updateFromModel](workspaceModel);
|
|
||||||
} else {
|
|
||||||
this.workspaces.set(workspaceModel.id, new Workspace(workspaceModel));
|
|
||||||
}
|
|
||||||
|
|
||||||
newWorkspaceIds.add(workspaceModel.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove deleted workspaces
|
|
||||||
for (const workspaceId of currentWorkspaces.keys()) {
|
|
||||||
if (!newWorkspaceIds.has(workspaceId)) {
|
|
||||||
this.workspaces.delete(workspaceId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toJSON(): WorkspaceStoreModel {
|
|
||||||
return toJS({
|
|
||||||
currentWorkspace: this.currentWorkspaceId,
|
|
||||||
workspaces: this.workspacesList.map((w) => w.toJSON()),
|
|
||||||
}, {
|
|
||||||
recurseEverything: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const workspaceStore = WorkspaceStore.getInstance<WorkspaceStore>();
|
|
||||||
0
src/extensions/core-api/catalog.ts
Normal file
0
src/extensions/core-api/catalog.ts
Normal file
@ -1,8 +1 @@
|
|||||||
export { ExtensionStore } from "../extension-store";
|
export { ExtensionStore } from "../extension-store";
|
||||||
|
|
||||||
export { clusterStore, Cluster, ClusterStore } from "../stores/cluster-store";
|
|
||||||
export type { ClusterModel, ClusterId } from "../stores/cluster-store";
|
|
||||||
|
|
||||||
export { workspaceStore, Workspace, WorkspaceStore } from "../stores/workspace-store";
|
|
||||||
export type { WorkspaceId, WorkspaceModel } from "../stores/workspace-store";
|
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,8 @@ import type { MenuRegistration } from "./registries/menu-registry";
|
|||||||
import { LensExtension } from "./lens-extension";
|
import { LensExtension } from "./lens-extension";
|
||||||
import { WindowManager } from "../main/window-manager";
|
import { WindowManager } from "../main/window-manager";
|
||||||
import { getExtensionPageUrl } from "./registries/page-registry";
|
import { getExtensionPageUrl } from "./registries/page-registry";
|
||||||
|
import { catalogEntityRegistry } from "../common/catalog-entity-registry";
|
||||||
|
import { CatalogEntity } from "../common/catalog-entity";
|
||||||
|
|
||||||
export class LensMainExtension extends LensExtension {
|
export class LensMainExtension extends LensExtension {
|
||||||
appMenus: MenuRegistration[] = [];
|
appMenus: MenuRegistration[] = [];
|
||||||
@ -16,4 +18,12 @@ export class LensMainExtension extends LensExtension {
|
|||||||
|
|
||||||
await windowManager.navigate(pageUrl, frameId);
|
await windowManager.navigate(pageUrl, frameId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addCatalogSource(id: string, source: CatalogEntity[]) {
|
||||||
|
catalogEntityRegistry.addSource(`${this.name}:${id}`, source);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeCatalogSorce(id: string) {
|
||||||
|
catalogEntityRegistry.removeSource(`${this.name}:${id}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,12 @@
|
|||||||
// Extensions API -> Commands
|
// Extensions API -> Commands
|
||||||
|
|
||||||
import type { Cluster } from "../../main/cluster";
|
import type { Cluster } from "../../main/cluster";
|
||||||
import type { Workspace } from "../../common/workspace-store";
|
|
||||||
import { BaseRegistry } from "./base-registry";
|
import { BaseRegistry } from "./base-registry";
|
||||||
import { action } from "mobx";
|
import { action } from "mobx";
|
||||||
import { LensExtension } from "../lens-extension";
|
import { LensExtension } from "../lens-extension";
|
||||||
|
|
||||||
export type CommandContext = {
|
export type CommandContext = {
|
||||||
cluster?: Cluster;
|
cluster?: Cluster;
|
||||||
workspace?: Workspace;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface CommandRegistration {
|
export interface CommandRegistration {
|
||||||
|
|||||||
@ -1,128 +0,0 @@
|
|||||||
import { clusterStore as internalClusterStore, ClusterId } from "../../common/cluster-store";
|
|
||||||
import type { ClusterModel } from "../../common/cluster-store";
|
|
||||||
import { Cluster } from "../../main/cluster";
|
|
||||||
import { Singleton } from "../core-api/utils";
|
|
||||||
import { ObservableMap } from "mobx";
|
|
||||||
|
|
||||||
export { Cluster } from "../../main/cluster";
|
|
||||||
export type { ClusterModel, ClusterId } from "../../common/cluster-store";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Store for all added clusters
|
|
||||||
*
|
|
||||||
* @beta
|
|
||||||
*/
|
|
||||||
export class ClusterStore extends Singleton {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Active cluster id
|
|
||||||
*/
|
|
||||||
get activeClusterId(): string {
|
|
||||||
return internalClusterStore.activeCluster;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set active cluster id
|
|
||||||
*/
|
|
||||||
set activeClusterId(id : ClusterId) {
|
|
||||||
internalClusterStore.activeCluster = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map of all clusters
|
|
||||||
*/
|
|
||||||
get clusters(): ObservableMap<string, Cluster> {
|
|
||||||
return internalClusterStore.clusters;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get active cluster (a cluster which is currently visible)
|
|
||||||
*/
|
|
||||||
get activeCluster(): Cluster {
|
|
||||||
if (!this.activeClusterId) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.getById(this.activeClusterId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Array of all clusters
|
|
||||||
*/
|
|
||||||
get clustersList(): Cluster[] {
|
|
||||||
return internalClusterStore.clustersList;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Array of all enabled clusters
|
|
||||||
*/
|
|
||||||
get enabledClustersList(): Cluster[] {
|
|
||||||
return internalClusterStore.enabledClustersList;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Array of all clusters that have active connection to a Kubernetes cluster
|
|
||||||
*/
|
|
||||||
get connectedClustersList(): Cluster[] {
|
|
||||||
return internalClusterStore.connectedClustersList;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get cluster object by cluster id
|
|
||||||
* @param id cluster id
|
|
||||||
*/
|
|
||||||
getById(id: ClusterId): Cluster {
|
|
||||||
return internalClusterStore.getById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all clusters belonging to a workspace
|
|
||||||
* @param workspaceId workspace id
|
|
||||||
*/
|
|
||||||
getByWorkspaceId(workspaceId: string): Cluster[] {
|
|
||||||
return internalClusterStore.getByWorkspaceId(workspaceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add clusters to store
|
|
||||||
* @param models list of cluster models
|
|
||||||
*/
|
|
||||||
addClusters(...models: ClusterModel[]): Cluster[] {
|
|
||||||
return internalClusterStore.addClusters(...models);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a cluster to store
|
|
||||||
* @param model cluster
|
|
||||||
*/
|
|
||||||
addCluster(model: ClusterModel | Cluster): Cluster {
|
|
||||||
return internalClusterStore.addCluster(model);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a cluster from store
|
|
||||||
* @param model cluster
|
|
||||||
*/
|
|
||||||
async removeCluster(model: ClusterModel) {
|
|
||||||
return this.removeById(model.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a cluster from store by id
|
|
||||||
* @param clusterId cluster id
|
|
||||||
*/
|
|
||||||
async removeById(clusterId: ClusterId) {
|
|
||||||
return internalClusterStore.removeById(clusterId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove all clusters belonging to a workspaces
|
|
||||||
* @param workspaceId workspace id
|
|
||||||
*/
|
|
||||||
removeByWorkspaceId(workspaceId: string) {
|
|
||||||
return internalClusterStore.removeByWorkspaceId(workspaceId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const clusterStore = ClusterStore.getInstance<ClusterStore>();
|
|
||||||
@ -1,118 +0,0 @@
|
|||||||
import { Singleton } from "../core-api/utils";
|
|
||||||
import { workspaceStore as internalWorkspaceStore, WorkspaceStore as InternalWorkspaceStore, Workspace, WorkspaceId } from "../../common/workspace-store";
|
|
||||||
import { ObservableMap } from "mobx";
|
|
||||||
|
|
||||||
export { Workspace } from "../../common/workspace-store";
|
|
||||||
export type { WorkspaceId, WorkspaceModel } from "../../common/workspace-store";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stores all workspaces
|
|
||||||
*
|
|
||||||
* @beta
|
|
||||||
*/
|
|
||||||
export class WorkspaceStore extends Singleton {
|
|
||||||
/**
|
|
||||||
* Default workspace id, this workspace is always present
|
|
||||||
*/
|
|
||||||
static readonly defaultId: WorkspaceId = InternalWorkspaceStore.defaultId;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Currently active workspace id
|
|
||||||
*/
|
|
||||||
get currentWorkspaceId(): string {
|
|
||||||
return internalWorkspaceStore.currentWorkspaceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set active workspace id
|
|
||||||
*/
|
|
||||||
set currentWorkspaceId(id: string) {
|
|
||||||
internalWorkspaceStore.currentWorkspaceId = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map of all workspaces
|
|
||||||
*/
|
|
||||||
get workspaces(): ObservableMap<string, Workspace> {
|
|
||||||
return internalWorkspaceStore.workspaces;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Currently active workspace
|
|
||||||
*/
|
|
||||||
get currentWorkspace(): Workspace {
|
|
||||||
return internalWorkspaceStore.currentWorkspace;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Array of all workspaces
|
|
||||||
*/
|
|
||||||
get workspacesList(): Workspace[] {
|
|
||||||
return internalWorkspaceStore.workspacesList;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Array of all enabled (visible) workspaces
|
|
||||||
*/
|
|
||||||
get enabledWorkspacesList(): Workspace[] {
|
|
||||||
return internalWorkspaceStore.enabledWorkspacesList;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get workspace by id
|
|
||||||
* @param id workspace id
|
|
||||||
*/
|
|
||||||
getById(id: WorkspaceId): Workspace {
|
|
||||||
return internalWorkspaceStore.getById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get workspace by name
|
|
||||||
* @param name workspace name
|
|
||||||
*/
|
|
||||||
getByName(name: string): Workspace {
|
|
||||||
return internalWorkspaceStore.getByName(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set active workspace
|
|
||||||
* @param id workspace id
|
|
||||||
*/
|
|
||||||
setActive(id = WorkspaceStore.defaultId) {
|
|
||||||
return internalWorkspaceStore.setActive(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a workspace to store
|
|
||||||
* @param workspace workspace
|
|
||||||
*/
|
|
||||||
addWorkspace(workspace: Workspace) {
|
|
||||||
return internalWorkspaceStore.addWorkspace(workspace);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update a workspace in store
|
|
||||||
* @param workspace workspace
|
|
||||||
*/
|
|
||||||
updateWorkspace(workspace: Workspace) {
|
|
||||||
return internalWorkspaceStore.updateWorkspace(workspace);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove workspace from store
|
|
||||||
* @param workspace workspace
|
|
||||||
*/
|
|
||||||
removeWorkspace(workspace: Workspace) {
|
|
||||||
return internalWorkspaceStore.removeWorkspace(workspace);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove workspace by id
|
|
||||||
* @param id workspace
|
|
||||||
*/
|
|
||||||
removeWorkspaceById(id: WorkspaceId) {
|
|
||||||
return internalWorkspaceStore.removeWorkspaceById(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const workspaceStore = WorkspaceStore.getInstance<WorkspaceStore>();
|
|
||||||
@ -31,7 +31,6 @@ jest.mock("request-promise-native");
|
|||||||
|
|
||||||
import { Console } from "console";
|
import { Console } from "console";
|
||||||
import mockFs from "mock-fs";
|
import mockFs from "mock-fs";
|
||||||
import { workspaceStore } from "../../common/workspace-store";
|
|
||||||
import { Cluster } from "../cluster";
|
import { Cluster } from "../cluster";
|
||||||
import { ContextHandler } from "../context-handler";
|
import { ContextHandler } from "../context-handler";
|
||||||
import { getFreePort } from "../port";
|
import { getFreePort } from "../port";
|
||||||
@ -81,8 +80,7 @@ describe("create clusters", () => {
|
|||||||
c = new Cluster({
|
c = new Cluster({
|
||||||
id: "foo",
|
id: "foo",
|
||||||
contextName: "minikube",
|
contextName: "minikube",
|
||||||
kubeConfigPath: "minikube-config.yml",
|
kubeConfigPath: "minikube-config.yml"
|
||||||
workspace: workspaceStore.currentWorkspaceId
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -162,8 +160,7 @@ describe("create clusters", () => {
|
|||||||
}({
|
}({
|
||||||
id: "foo",
|
id: "foo",
|
||||||
contextName: "minikube",
|
contextName: "minikube",
|
||||||
kubeConfigPath: "minikube-config.yml",
|
kubeConfigPath: "minikube-config.yml"
|
||||||
workspace: workspaceStore.currentWorkspaceId
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await c.init(port);
|
await c.init(port);
|
||||||
|
|||||||
@ -26,7 +26,6 @@ jest.mock("winston", () => ({
|
|||||||
import { KubeconfigManager } from "../kubeconfig-manager";
|
import { KubeconfigManager } from "../kubeconfig-manager";
|
||||||
import mockFs from "mock-fs";
|
import mockFs from "mock-fs";
|
||||||
import { Cluster } from "../cluster";
|
import { Cluster } from "../cluster";
|
||||||
import { workspaceStore } from "../../common/workspace-store";
|
|
||||||
import { ContextHandler } from "../context-handler";
|
import { ContextHandler } from "../context-handler";
|
||||||
import { getFreePort } from "../port";
|
import { getFreePort } from "../port";
|
||||||
import fse from "fs-extra";
|
import fse from "fs-extra";
|
||||||
@ -77,8 +76,7 @@ describe("kubeconfig manager tests", () => {
|
|||||||
const cluster = new Cluster({
|
const cluster = new Cluster({
|
||||||
id: "foo",
|
id: "foo",
|
||||||
contextName: "minikube",
|
contextName: "minikube",
|
||||||
kubeConfigPath: "minikube-config.yml",
|
kubeConfigPath: "minikube-config.yml"
|
||||||
workspace: workspaceStore.currentWorkspaceId
|
|
||||||
});
|
});
|
||||||
const contextHandler = new ContextHandler(cluster);
|
const contextHandler = new ContextHandler(cluster);
|
||||||
const port = await getFreePort();
|
const port = await getFreePort();
|
||||||
@ -98,8 +96,7 @@ describe("kubeconfig manager tests", () => {
|
|||||||
const cluster = new Cluster({
|
const cluster = new Cluster({
|
||||||
id: "foo",
|
id: "foo",
|
||||||
contextName: "minikube",
|
contextName: "minikube",
|
||||||
kubeConfigPath: "minikube-config.yml",
|
kubeConfigPath: "minikube-config.yml"
|
||||||
workspace: workspaceStore.currentWorkspaceId
|
|
||||||
});
|
});
|
||||||
const contextHandler = new ContextHandler(cluster);
|
const contextHandler = new ContextHandler(cluster);
|
||||||
const port = await getFreePort();
|
const port = await getFreePort();
|
||||||
|
|||||||
26
src/main/catalog-pusher.ts
Normal file
26
src/main/catalog-pusher.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { autorun, toJS } from "mobx";
|
||||||
|
import { broadcastMessage, subscribeToBroadcast } from "../common/ipc";
|
||||||
|
import { CatalogEntityRegistry} from "../common/catalog-entity-registry";
|
||||||
|
import "../common/catalog-entities/kubernetes-cluster";
|
||||||
|
|
||||||
|
export class CatalogPusher {
|
||||||
|
static init(catalog: CatalogEntityRegistry) {
|
||||||
|
new CatalogPusher(catalog).init();
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(private catalog: CatalogEntityRegistry) {}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
autorun(() => {
|
||||||
|
this.broadcast();
|
||||||
|
});
|
||||||
|
|
||||||
|
subscribeToBroadcast("catalog:broadcast", () => {
|
||||||
|
this.broadcast();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcast() {
|
||||||
|
broadcastMessage("catalog:items", toJS(this.catalog.items, { recurseEverything: true }));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,16 +1,23 @@
|
|||||||
import "../common/cluster-ipc";
|
import "../common/cluster-ipc";
|
||||||
import type http from "http";
|
import type http from "http";
|
||||||
import { ipcMain } from "electron";
|
import { ipcMain } from "electron";
|
||||||
import { autorun, reaction } from "mobx";
|
import { action, autorun, observable, reaction, toJS } from "mobx";
|
||||||
import { clusterStore, getClusterIdFromHost } from "../common/cluster-store";
|
import { clusterStore, getClusterIdFromHost } from "../common/cluster-store";
|
||||||
import { Cluster } from "./cluster";
|
import { Cluster } from "./cluster";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
import { apiKubePrefix } from "../common/vars";
|
import { apiKubePrefix } from "../common/vars";
|
||||||
import { Singleton } from "../common/utils";
|
import { Singleton } from "../common/utils";
|
||||||
|
import { CatalogEntity } from "../common/catalog-entity";
|
||||||
|
import { KubernetesCluster } from "../common/catalog-entities/kubernetes-cluster";
|
||||||
|
import { catalogEntityRegistry } from "../common/catalog-entity-registry";
|
||||||
|
|
||||||
export class ClusterManager extends Singleton {
|
export class ClusterManager extends Singleton {
|
||||||
|
@observable.deep catalogSource: CatalogEntity[] = [];
|
||||||
|
|
||||||
constructor(public readonly port: number) {
|
constructor(public readonly port: number) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
catalogEntityRegistry.addSource("lens:kubernetes-clusters", this.catalogSource);
|
||||||
// auto-init clusters
|
// auto-init clusters
|
||||||
reaction(() => clusterStore.enabledClustersList, (clusters) => {
|
reaction(() => clusterStore.enabledClustersList, (clusters) => {
|
||||||
clusters.forEach((cluster) => {
|
clusters.forEach((cluster) => {
|
||||||
@ -19,8 +26,14 @@ export class ClusterManager extends Singleton {
|
|||||||
cluster.init(port);
|
cluster.init(port);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
}, { fireImmediately: true });
|
}, { fireImmediately: true });
|
||||||
|
|
||||||
|
reaction(() => toJS(clusterStore.enabledClustersList, { recurseEverything: true }), () => {
|
||||||
|
this.updateCatalogSource(clusterStore.enabledClustersList);
|
||||||
|
}, { fireImmediately: true });
|
||||||
|
|
||||||
|
|
||||||
// auto-stop removed clusters
|
// auto-stop removed clusters
|
||||||
autorun(() => {
|
autorun(() => {
|
||||||
const removedClusters = Array.from(clusterStore.removedClusters.values());
|
const removedClusters = Array.from(clusterStore.removedClusters.values());
|
||||||
@ -40,6 +53,55 @@ export class ClusterManager extends Singleton {
|
|||||||
ipcMain.on("network:online", () => { this.onNetworkOnline(); });
|
ipcMain.on("network:online", () => { this.onNetworkOnline(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action protected updateCatalogSource(clusters: Cluster[]) {
|
||||||
|
this.catalogSource.forEach((entity, index) => {
|
||||||
|
const clusterIndex = clusters.findIndex((cluster) => entity.metadata.uid === cluster.id);
|
||||||
|
|
||||||
|
if (clusterIndex === -1) {
|
||||||
|
this.catalogSource.splice(index, 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
clusters.forEach((cluster) => {
|
||||||
|
const entityIndex = this.catalogSource.findIndex((entity) => entity.metadata.uid === cluster.id);
|
||||||
|
const newEntity = this.catalogEntityFromCluster(cluster);
|
||||||
|
|
||||||
|
if (entityIndex === -1) {
|
||||||
|
this.catalogSource.push(newEntity);
|
||||||
|
} else {
|
||||||
|
const entity = this.catalogSource[entityIndex] as KubernetesCluster;
|
||||||
|
|
||||||
|
newEntity.status.active = entity.status.active;
|
||||||
|
this.catalogSource.splice(entityIndex, 1, newEntity);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected catalogEntityFromCluster(cluster: Cluster) {
|
||||||
|
return new KubernetesCluster(toJS({
|
||||||
|
apiVersion: "entity.k8slens.dev/v1alpha1",
|
||||||
|
kind: "KubernetesCluster",
|
||||||
|
metadata: {
|
||||||
|
uid: cluster.id,
|
||||||
|
name: cluster.name,
|
||||||
|
source: "local",
|
||||||
|
labels: {
|
||||||
|
"distro": cluster.metadata["distro"]?.toString()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
kubeconfigPath: cluster.kubeConfigPath,
|
||||||
|
kubeconfigContext: cluster.contextName
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
phase: cluster.disconnected ? "disconnected" : "connected",
|
||||||
|
reason: "",
|
||||||
|
message: "",
|
||||||
|
active: !cluster.disconnected
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
protected onNetworkOffline() {
|
protected onNetworkOffline() {
|
||||||
logger.info("[CLUSTER-MANAGER]: network is offline");
|
logger.info("[CLUSTER-MANAGER]: network is offline");
|
||||||
clusterStore.enabledClustersList.forEach((cluster) => {
|
clusterStore.enabledClustersList.forEach((cluster) => {
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { ipcMain } from "electron";
|
import { ipcMain } from "electron";
|
||||||
import type { ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences } from "../common/cluster-store";
|
import type { ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences } from "../common/cluster-store";
|
||||||
import type { IMetricsReqParams } from "../renderer/api/endpoints/metrics.api";
|
import type { IMetricsReqParams } from "../renderer/api/endpoints/metrics.api";
|
||||||
import type { WorkspaceId } from "../common/workspace-store";
|
|
||||||
import { action, comparer, computed, observable, reaction, toJS, when } from "mobx";
|
import { action, comparer, computed, observable, reaction, toJS, when } from "mobx";
|
||||||
import { apiKubePrefix } from "../common/vars";
|
import { apiKubePrefix } from "../common/vars";
|
||||||
import { broadcastMessage, InvalidKubeconfigChannel, ClusterListNamespaceForbiddenChannel } from "../common/ipc";
|
import { broadcastMessage, InvalidKubeconfigChannel, ClusterListNamespaceForbiddenChannel } from "../common/ipc";
|
||||||
@ -104,12 +103,6 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
* @observable
|
* @observable
|
||||||
*/
|
*/
|
||||||
@observable contextName: string;
|
@observable contextName: string;
|
||||||
/**
|
|
||||||
* Workspace id
|
|
||||||
*
|
|
||||||
* @observable
|
|
||||||
*/
|
|
||||||
@observable workspace: WorkspaceId;
|
|
||||||
/**
|
/**
|
||||||
* Path to kubeconfig
|
* Path to kubeconfig
|
||||||
*
|
*
|
||||||
@ -611,7 +604,6 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
id: this.id,
|
id: this.id,
|
||||||
contextName: this.contextName,
|
contextName: this.contextName,
|
||||||
kubeConfigPath: this.kubeConfigPath,
|
kubeConfigPath: this.kubeConfigPath,
|
||||||
workspace: this.workspace,
|
|
||||||
preferences: this.preferences,
|
preferences: this.preferences,
|
||||||
metadata: this.metadata,
|
metadata: this.metadata,
|
||||||
ownerRef: this.ownerRef,
|
ownerRef: this.ownerRef,
|
||||||
|
|||||||
@ -17,7 +17,6 @@ import { registerFileProtocol } from "../common/register-protocol";
|
|||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
import { clusterStore } from "../common/cluster-store";
|
import { clusterStore } from "../common/cluster-store";
|
||||||
import { userStore } from "../common/user-store";
|
import { userStore } from "../common/user-store";
|
||||||
import { workspaceStore } from "../common/workspace-store";
|
|
||||||
import { appEventBus } from "../common/event-bus";
|
import { appEventBus } from "../common/event-bus";
|
||||||
import { extensionLoader } from "../extensions/extension-loader";
|
import { extensionLoader } from "../extensions/extension-loader";
|
||||||
import { extensionsStore } from "../extensions/extensions-store";
|
import { extensionsStore } from "../extensions/extensions-store";
|
||||||
@ -30,6 +29,8 @@ import { getAppVersion, getAppVersionFromProxyServer } from "../common/utils";
|
|||||||
import { bindBroadcastHandlers } from "../common/ipc";
|
import { bindBroadcastHandlers } from "../common/ipc";
|
||||||
import { startUpdateChecking } from "./app-updater";
|
import { startUpdateChecking } from "./app-updater";
|
||||||
import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
|
import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
|
||||||
|
import { CatalogPusher } from "./catalog-pusher";
|
||||||
|
import { catalogEntityRegistry } from "../common/catalog-entity-registry";
|
||||||
|
|
||||||
const workingDir = path.join(app.getPath("appData"), appName);
|
const workingDir = path.join(app.getPath("appData"), appName);
|
||||||
let proxyPort: number;
|
let proxyPort: number;
|
||||||
@ -107,7 +108,6 @@ app.on("ready", async () => {
|
|||||||
await Promise.all([
|
await Promise.all([
|
||||||
userStore.load(),
|
userStore.load(),
|
||||||
clusterStore.load(),
|
clusterStore.load(),
|
||||||
workspaceStore.load(),
|
|
||||||
extensionsStore.load(),
|
extensionsStore.load(),
|
||||||
filesystemProvisionerStore.load(),
|
filesystemProvisionerStore.load(),
|
||||||
]);
|
]);
|
||||||
@ -164,6 +164,7 @@ app.on("ready", async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ipcMain.on(IpcRendererNavigationEvents.LOADED, () => {
|
ipcMain.on(IpcRendererNavigationEvents.LOADED, () => {
|
||||||
|
CatalogPusher.init(catalogEntityRegistry);
|
||||||
startUpdateChecking();
|
startUpdateChecking();
|
||||||
LensProtocolRouterMain
|
LensProtocolRouterMain
|
||||||
.getInstance<LensProtocolRouterMain>()
|
.getInstance<LensProtocolRouterMain>()
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { preferencesURL } from "../renderer/components/+preferences/preferences.
|
|||||||
import { whatsNewURL } from "../renderer/components/+whats-new/whats-new.route";
|
import { whatsNewURL } from "../renderer/components/+whats-new/whats-new.route";
|
||||||
import { clusterSettingsURL } from "../renderer/components/+cluster-settings/cluster-settings.route";
|
import { clusterSettingsURL } from "../renderer/components/+cluster-settings/cluster-settings.route";
|
||||||
import { extensionsURL } from "../renderer/components/+extensions/extensions.route";
|
import { extensionsURL } from "../renderer/components/+extensions/extensions.route";
|
||||||
|
import { landingURL } from "../renderer/components/+landing-page/landing-page.route";
|
||||||
import { menuRegistry } from "../extensions/registries/menu-registry";
|
import { menuRegistry } from "../extensions/registries/menu-registry";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
import { exitApp } from "./exit-app";
|
import { exitApp } from "./exit-app";
|
||||||
@ -175,6 +176,13 @@ export function buildMenu(windowManager: WindowManager) {
|
|||||||
const viewMenu: MenuItemConstructorOptions = {
|
const viewMenu: MenuItemConstructorOptions = {
|
||||||
label: "View",
|
label: "View",
|
||||||
submenu: [
|
submenu: [
|
||||||
|
{
|
||||||
|
label: "Catalog",
|
||||||
|
accelerator: "Shift+CmdOrCtrl+C",
|
||||||
|
click() {
|
||||||
|
navigate(landingURL());
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "Command Palette...",
|
label: "Command Palette...",
|
||||||
accelerator: "Shift+CmdOrCtrl+P",
|
accelerator: "Shift+CmdOrCtrl+P",
|
||||||
|
|||||||
@ -5,10 +5,7 @@ import { autorun } from "mobx";
|
|||||||
import { showAbout } from "./menu";
|
import { showAbout } from "./menu";
|
||||||
import { checkForUpdates } from "./app-updater";
|
import { checkForUpdates } from "./app-updater";
|
||||||
import { WindowManager } from "./window-manager";
|
import { WindowManager } from "./window-manager";
|
||||||
import { clusterStore } from "../common/cluster-store";
|
|
||||||
import { workspaceStore } from "../common/workspace-store";
|
|
||||||
import { preferencesURL } from "../renderer/components/+preferences/preferences.route";
|
import { preferencesURL } from "../renderer/components/+preferences/preferences.route";
|
||||||
import { clusterViewURL } from "../renderer/components/cluster-manager/cluster-view.route";
|
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
import { isDevelopment, isWindows } from "../common/vars";
|
import { isDevelopment, isWindows } from "../common/vars";
|
||||||
import { exitApp } from "./exit-app";
|
import { exitApp } from "./exit-app";
|
||||||
@ -74,31 +71,6 @@ function createTrayMenu(windowManager: WindowManager): Menu {
|
|||||||
windowManager.navigate(preferencesURL());
|
windowManager.navigate(preferencesURL());
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: "Clusters",
|
|
||||||
submenu: workspaceStore.enabledWorkspacesList
|
|
||||||
.filter(workspace => clusterStore.getByWorkspaceId(workspace.id).length > 0) // hide empty workspaces
|
|
||||||
.map(workspace => {
|
|
||||||
const clusters = clusterStore.getByWorkspaceId(workspace.id);
|
|
||||||
|
|
||||||
return {
|
|
||||||
label: workspace.name,
|
|
||||||
toolTip: workspace.description,
|
|
||||||
submenu: clusters.map(cluster => {
|
|
||||||
const { id: clusterId, name: label, online, workspace } = cluster;
|
|
||||||
|
|
||||||
return {
|
|
||||||
label: `${online ? "✓" : "\x20".repeat(3)/*offset*/}${label}`,
|
|
||||||
toolTip: clusterId,
|
|
||||||
async click() {
|
|
||||||
workspaceStore.setActive(workspace);
|
|
||||||
windowManager.navigate(clusterViewURL({ params: { clusterId } }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: "Check for updates",
|
label: "Check for updates",
|
||||||
async click() {
|
async click() {
|
||||||
|
|||||||
135
src/renderer/api/__tests__/catalog-entity-registry.test.ts
Normal file
135
src/renderer/api/__tests__/catalog-entity-registry.test.ts
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import { CatalogEntityRegistry } from "../catalog-entity-registry";
|
||||||
|
import "../../../common/catalog-entities";
|
||||||
|
import { catalogCategoryRegistry } from "../../../common/catalog-category-registry";
|
||||||
|
|
||||||
|
describe("CatalogEntityRegistry", () => {
|
||||||
|
describe("updateItems", () => {
|
||||||
|
it("adds new catalog item", () => {
|
||||||
|
const catalog = new CatalogEntityRegistry(catalogCategoryRegistry);
|
||||||
|
const items = [{
|
||||||
|
apiVersion: "entity.k8slens.dev/v1alpha1",
|
||||||
|
kind: "KubernetesCluster",
|
||||||
|
metadata: {
|
||||||
|
uid: "123",
|
||||||
|
name: "foobar",
|
||||||
|
source: "test",
|
||||||
|
labels: {}
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
phase: "disconnected"
|
||||||
|
},
|
||||||
|
spec: {}
|
||||||
|
}];
|
||||||
|
|
||||||
|
catalog.updateItems(items);
|
||||||
|
expect(catalog.items.length).toEqual(1);
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
apiVersion: "entity.k8slens.dev/v1alpha1",
|
||||||
|
kind: "KubernetesCluster",
|
||||||
|
metadata: {
|
||||||
|
uid: "456",
|
||||||
|
name: "barbaz",
|
||||||
|
source: "test",
|
||||||
|
labels: {}
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
phase: "disconnected"
|
||||||
|
},
|
||||||
|
spec: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
catalog.updateItems(items);
|
||||||
|
expect(catalog.items.length).toEqual(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores unknown items", () => {
|
||||||
|
const catalog = new CatalogEntityRegistry(catalogCategoryRegistry);
|
||||||
|
const items = [{
|
||||||
|
apiVersion: "entity.k8slens.dev/v1alpha1",
|
||||||
|
kind: "FooBar",
|
||||||
|
metadata: {
|
||||||
|
uid: "123",
|
||||||
|
name: "foobar",
|
||||||
|
source: "test",
|
||||||
|
labels: {}
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
phase: "disconnected"
|
||||||
|
},
|
||||||
|
spec: {}
|
||||||
|
}];
|
||||||
|
|
||||||
|
catalog.updateItems(items);
|
||||||
|
expect(catalog.items.length).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates existing items", () => {
|
||||||
|
const catalog = new CatalogEntityRegistry(catalogCategoryRegistry);
|
||||||
|
const items = [{
|
||||||
|
apiVersion: "entity.k8slens.dev/v1alpha1",
|
||||||
|
kind: "KubernetesCluster",
|
||||||
|
metadata: {
|
||||||
|
uid: "123",
|
||||||
|
name: "foobar",
|
||||||
|
source: "test",
|
||||||
|
labels: {}
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
phase: "disconnected"
|
||||||
|
},
|
||||||
|
spec: {}
|
||||||
|
}];
|
||||||
|
|
||||||
|
catalog.updateItems(items);
|
||||||
|
expect(catalog.items.length).toEqual(1);
|
||||||
|
expect(catalog.items[0].status.phase).toEqual("disconnected");
|
||||||
|
|
||||||
|
items[0].status.phase = "connected";
|
||||||
|
|
||||||
|
catalog.updateItems(items);
|
||||||
|
expect(catalog.items.length).toEqual(1);
|
||||||
|
expect(catalog.items[0].status.phase).toEqual("connected");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("removes deleted items", () => {
|
||||||
|
const catalog = new CatalogEntityRegistry(catalogCategoryRegistry);
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
apiVersion: "entity.k8slens.dev/v1alpha1",
|
||||||
|
kind: "KubernetesCluster",
|
||||||
|
metadata: {
|
||||||
|
uid: "123",
|
||||||
|
name: "foobar",
|
||||||
|
source: "test",
|
||||||
|
labels: {}
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
phase: "disconnected"
|
||||||
|
},
|
||||||
|
spec: {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
apiVersion: "entity.k8slens.dev/v1alpha1",
|
||||||
|
kind: "KubernetesCluster",
|
||||||
|
metadata: {
|
||||||
|
uid: "456",
|
||||||
|
name: "barbaz",
|
||||||
|
source: "test",
|
||||||
|
labels: {}
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
phase: "disconnected"
|
||||||
|
},
|
||||||
|
spec: {}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
catalog.updateItems(items);
|
||||||
|
items.splice(0, 1);
|
||||||
|
catalog.updateItems(items);
|
||||||
|
expect(catalog.items.length).toEqual(1);
|
||||||
|
expect(catalog.items[0].metadata.uid).toEqual("456");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
56
src/renderer/api/catalog-entity-registry.ts
Normal file
56
src/renderer/api/catalog-entity-registry.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { action, observable } from "mobx";
|
||||||
|
import { broadcastMessage, subscribeToBroadcast } from "../../common/ipc";
|
||||||
|
import { CatalogEntity, CatalogEntityData } from "../../common/catalog-entity";
|
||||||
|
import { catalogCategoryRegistry, CatalogCategoryRegistry } from "../../common/catalog-category-registry";
|
||||||
|
import "../../common/catalog-entities";
|
||||||
|
|
||||||
|
export { CatalogEntity, CatalogEntityData } from "../../common/catalog-entity";
|
||||||
|
|
||||||
|
export class CatalogEntityRegistry {
|
||||||
|
@observable protected _items: CatalogEntity[] = observable.array([], { deep: true });
|
||||||
|
|
||||||
|
constructor(private categoryRegistry: CatalogCategoryRegistry) {}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
subscribeToBroadcast("catalog:items", (ev, items: CatalogEntityData[]) => {
|
||||||
|
this.updateItems(items);
|
||||||
|
});
|
||||||
|
broadcastMessage("catalog:broadcast");
|
||||||
|
}
|
||||||
|
|
||||||
|
@action updateItems(items: CatalogEntityData[]) {
|
||||||
|
this._items.forEach((item, index) => {
|
||||||
|
const foundIndex = items.findIndex((i) => i.apiVersion === item.apiVersion && i.kind === item.kind && i.metadata.uid === item.metadata.uid);
|
||||||
|
|
||||||
|
if (foundIndex === -1) {
|
||||||
|
this._items.splice(index, 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
items.forEach((data) => {
|
||||||
|
const item = this.categoryRegistry.getEntityForData(data);
|
||||||
|
|
||||||
|
if (!item) return; // invalid data
|
||||||
|
|
||||||
|
const index = this._items.findIndex((i) => i.apiVersion === item.apiVersion && i.kind === item.kind && i.metadata.uid === item.metadata.uid);
|
||||||
|
|
||||||
|
if (index === -1) {
|
||||||
|
this._items.push(item);
|
||||||
|
} else {
|
||||||
|
this._items.splice(index, 1, item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get items() {
|
||||||
|
return this._items;
|
||||||
|
}
|
||||||
|
|
||||||
|
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[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
|
||||||
@ -10,7 +10,6 @@ import { clusterStore } from "../common/cluster-store";
|
|||||||
import { userStore } from "../common/user-store";
|
import { userStore } from "../common/user-store";
|
||||||
import { delay } from "../common/utils";
|
import { delay } from "../common/utils";
|
||||||
import { isMac, isDevelopment } from "../common/vars";
|
import { isMac, isDevelopment } from "../common/vars";
|
||||||
import { workspaceStore } from "../common/workspace-store";
|
|
||||||
import * as LensExtensions from "../extensions/extension-api";
|
import * as LensExtensions from "../extensions/extension-api";
|
||||||
import { extensionDiscovery } from "../extensions/extension-discovery";
|
import { extensionDiscovery } from "../extensions/extension-discovery";
|
||||||
import { extensionLoader } from "../extensions/extension-loader";
|
import { extensionLoader } from "../extensions/extension-loader";
|
||||||
@ -56,7 +55,6 @@ export async function bootstrap(App: AppComponent) {
|
|||||||
// preload common stores
|
// preload common stores
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
userStore.load(),
|
userStore.load(),
|
||||||
workspaceStore.load(),
|
|
||||||
clusterStore.load(),
|
clusterStore.load(),
|
||||||
extensionsStore.load(),
|
extensionsStore.load(),
|
||||||
filesystemProvisionerStore.load(),
|
filesystemProvisionerStore.load(),
|
||||||
@ -65,7 +63,6 @@ export async function bootstrap(App: AppComponent) {
|
|||||||
|
|
||||||
// Register additional store listeners
|
// Register additional store listeners
|
||||||
clusterStore.registerIpcListener();
|
clusterStore.registerIpcListener();
|
||||||
workspaceStore.registerIpcListener();
|
|
||||||
|
|
||||||
// init app's dependencies if any
|
// init app's dependencies if any
|
||||||
if (App.init) {
|
if (App.init) {
|
||||||
@ -74,7 +71,6 @@ export async function bootstrap(App: AppComponent) {
|
|||||||
window.addEventListener("message", (ev: MessageEvent) => {
|
window.addEventListener("message", (ev: MessageEvent) => {
|
||||||
if (ev.data === "teardown") {
|
if (ev.data === "teardown") {
|
||||||
userStore.unregisterIpcListener();
|
userStore.unregisterIpcListener();
|
||||||
workspaceStore.unregisterIpcListener();
|
|
||||||
clusterStore.unregisterIpcListener();
|
clusterStore.unregisterIpcListener();
|
||||||
unmountComponentAtNode(rootElem);
|
unmountComponentAtNode(rootElem);
|
||||||
window.location.href = "about:blank";
|
window.location.href = "about:blank";
|
||||||
|
|||||||
@ -12,7 +12,6 @@ import { Button } from "../button";
|
|||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { kubeConfigDefaultPath, loadConfig, splitConfig, validateConfig, validateKubeConfig } from "../../../common/kube-helpers";
|
import { kubeConfigDefaultPath, loadConfig, splitConfig, validateConfig, validateKubeConfig } from "../../../common/kube-helpers";
|
||||||
import { ClusterModel, ClusterStore, clusterStore } from "../../../common/cluster-store";
|
import { ClusterModel, ClusterStore, clusterStore } from "../../../common/cluster-store";
|
||||||
import { workspaceStore } from "../../../common/workspace-store";
|
|
||||||
import { v4 as uuid } from "uuid";
|
import { v4 as uuid } from "uuid";
|
||||||
import { navigate } from "../../navigation";
|
import { navigate } from "../../navigation";
|
||||||
import { userStore } from "../../../common/user-store";
|
import { userStore } from "../../../common/user-store";
|
||||||
@ -171,7 +170,6 @@ export class AddCluster extends React.Component {
|
|||||||
return {
|
return {
|
||||||
id: clusterId,
|
id: clusterId,
|
||||||
kubeConfigPath,
|
kubeConfigPath,
|
||||||
workspace: workspaceStore.currentWorkspaceId,
|
|
||||||
contextName: kubeConfig.currentContext,
|
contextName: kubeConfig.currentContext,
|
||||||
preferences: {
|
preferences: {
|
||||||
clusterName: kubeConfig.currentContext,
|
clusterName: kubeConfig.currentContext,
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import { Removal } from "./removal";
|
|||||||
import { Status } from "./status";
|
import { Status } from "./status";
|
||||||
import { General } from "./general";
|
import { General } from "./general";
|
||||||
import { Cluster } from "../../../main/cluster";
|
import { Cluster } from "../../../main/cluster";
|
||||||
import { ClusterIcon } from "../cluster-icon";
|
|
||||||
import { IClusterSettingsRouteParams } from "./cluster-settings.route";
|
import { IClusterSettingsRouteParams } from "./cluster-settings.route";
|
||||||
import { clusterStore } from "../../../common/cluster-store";
|
import { clusterStore } from "../../../common/cluster-store";
|
||||||
import { PageLayout } from "../layout/page-layout";
|
import { PageLayout } from "../layout/page-layout";
|
||||||
@ -58,7 +57,6 @@ export class ClusterSettings extends React.Component<Props> {
|
|||||||
if (!cluster) return null;
|
if (!cluster) return null;
|
||||||
const header = (
|
const header = (
|
||||||
<>
|
<>
|
||||||
<ClusterIcon cluster={cluster} showErrors={false} showTooltip={false}/>
|
|
||||||
<h2>{cluster.preferences.clusterName}</h2>
|
<h2>{cluster.preferences.clusterName}</h2>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,79 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { Cluster } from "../../../../main/cluster";
|
|
||||||
import { FilePicker, OverSizeLimitStyle } from "../../file-picker";
|
|
||||||
import { autobind } from "../../../utils";
|
|
||||||
import { Button } from "../../button";
|
|
||||||
import { observable } from "mobx";
|
|
||||||
import { observer } from "mobx-react";
|
|
||||||
import { SubTitle } from "../../layout/sub-title";
|
|
||||||
import { ClusterIcon } from "../../cluster-icon";
|
|
||||||
|
|
||||||
enum GeneralInputStatus {
|
|
||||||
CLEAN = "clean",
|
|
||||||
ERROR = "error",
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
cluster: Cluster;
|
|
||||||
}
|
|
||||||
|
|
||||||
@observer
|
|
||||||
export class ClusterIconSetting extends React.Component<Props> {
|
|
||||||
@observable status = GeneralInputStatus.CLEAN;
|
|
||||||
@observable errorText?: string;
|
|
||||||
|
|
||||||
@autobind()
|
|
||||||
async onIconPick([file]: File[]) {
|
|
||||||
const { cluster } = this.props;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (file) {
|
|
||||||
const buf = Buffer.from(await file.arrayBuffer());
|
|
||||||
|
|
||||||
cluster.preferences.icon = `data:${file.type};base64,${buf.toString("base64")}`;
|
|
||||||
} else {
|
|
||||||
// this has to be done as a seperate branch (and not always) because `cluster`
|
|
||||||
// is observable and triggers an update loop.
|
|
||||||
cluster.preferences.icon = undefined;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
this.errorText = e.toString();
|
|
||||||
this.status = GeneralInputStatus.ERROR;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getClearButton() {
|
|
||||||
if (this.props.cluster.preferences.icon) {
|
|
||||||
return <Button tooltip="Revert back to default icon" accent onClick={() => this.onIconPick([])}>Clear</Button>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const label = (
|
|
||||||
<>
|
|
||||||
<ClusterIcon
|
|
||||||
cluster={this.props.cluster}
|
|
||||||
showErrors={false}
|
|
||||||
showTooltip={false}
|
|
||||||
/>
|
|
||||||
{"Browse for new icon..."}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<SubTitle title="Cluster Icon" />
|
|
||||||
<p>Define cluster icon. By default automatically generated.</p>
|
|
||||||
<div className="file-loader">
|
|
||||||
<FilePicker
|
|
||||||
accept="image/*"
|
|
||||||
label={label}
|
|
||||||
onOverSizeLimit={OverSizeLimitStyle.FILTER}
|
|
||||||
handler={this.onIconPick}
|
|
||||||
/>
|
|
||||||
{this.getClearButton()}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { observer } from "mobx-react";
|
|
||||||
import { workspaceStore } from "../../../../common/workspace-store";
|
|
||||||
import { Cluster } from "../../../../main/cluster";
|
|
||||||
import { Select } from "../../../components/select";
|
|
||||||
import { SubTitle } from "../../layout/sub-title";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
cluster: Cluster;
|
|
||||||
}
|
|
||||||
|
|
||||||
@observer
|
|
||||||
export class ClusterWorkspaceSetting extends React.Component<Props> {
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<SubTitle title="Cluster Workspace"/>
|
|
||||||
<p>
|
|
||||||
Define cluster workspace.
|
|
||||||
</p>
|
|
||||||
<Select
|
|
||||||
value={this.props.cluster.workspace}
|
|
||||||
onChange={({value}) => this.props.cluster.workspace = value}
|
|
||||||
options={workspaceStore.enabledWorkspacesList.map(w =>
|
|
||||||
({value: w.id, label: w.name})
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,8 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Cluster } from "../../../main/cluster";
|
import { Cluster } from "../../../main/cluster";
|
||||||
import { ClusterNameSetting } from "./components/cluster-name-setting";
|
import { ClusterNameSetting } from "./components/cluster-name-setting";
|
||||||
import { ClusterWorkspaceSetting } from "./components/cluster-workspace-setting";
|
|
||||||
import { ClusterIconSetting } from "./components/cluster-icon-setting";
|
|
||||||
import { ClusterProxySetting } from "./components/cluster-proxy-setting";
|
import { ClusterProxySetting } from "./components/cluster-proxy-setting";
|
||||||
import { ClusterPrometheusSetting } from "./components/cluster-prometheus-setting";
|
import { ClusterPrometheusSetting } from "./components/cluster-prometheus-setting";
|
||||||
import { ClusterHomeDirSetting } from "./components/cluster-home-dir-setting";
|
import { ClusterHomeDirSetting } from "./components/cluster-home-dir-setting";
|
||||||
@ -19,8 +17,6 @@ export class General extends React.Component<Props> {
|
|||||||
return <div>
|
return <div>
|
||||||
<h2>General</h2>
|
<h2>General</h2>
|
||||||
<ClusterNameSetting cluster={this.props.cluster} />
|
<ClusterNameSetting cluster={this.props.cluster} />
|
||||||
<ClusterWorkspaceSetting cluster={this.props.cluster} />
|
|
||||||
<ClusterIconSetting cluster={this.props.cluster} />
|
|
||||||
<ClusterProxySetting cluster={this.props.cluster} />
|
<ClusterProxySetting cluster={this.props.cluster} />
|
||||||
<ClusterPrometheusSetting cluster={this.props.cluster} />
|
<ClusterPrometheusSetting cluster={this.props.cluster} />
|
||||||
<ClusterHomeDirSetting cluster={this.props.cluster} />
|
<ClusterHomeDirSetting cluster={this.props.cluster} />
|
||||||
|
|||||||
@ -0,0 +1,48 @@
|
|||||||
|
import { computed, reaction } from "mobx";
|
||||||
|
import { CatalogEntity, catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||||
|
import { ItemObject, ItemStore } from "../../item.store";
|
||||||
|
import { autobind } from "../../utils";
|
||||||
|
|
||||||
|
export class CatalogEntityItem implements ItemObject {
|
||||||
|
constructor(public entity: CatalogEntity) {}
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return this.entity.metadata.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
getName() {
|
||||||
|
return this.entity.metadata.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
get id() {
|
||||||
|
return this.entity.metadata.uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
getId() {
|
||||||
|
return this.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
get phase() {
|
||||||
|
return this.entity.status.phase;
|
||||||
|
}
|
||||||
|
|
||||||
|
onRun(ctx: any) {
|
||||||
|
this.entity.onRun(ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@autobind()
|
||||||
|
export class CatalogEntityStore extends ItemStore<CatalogEntityItem> {
|
||||||
|
|
||||||
|
@computed get entities() {
|
||||||
|
return catalogEntityRegistry.items.map(entity => new CatalogEntityItem(entity));
|
||||||
|
}
|
||||||
|
|
||||||
|
watch() {
|
||||||
|
return reaction(() => this.entities, () => this.loadAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
loadAll() {
|
||||||
|
return this.loadItems(() => this.entities);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,12 +1,8 @@
|
|||||||
.PageLayout.LandingOverview {
|
.LandingPage {
|
||||||
--width: 100%;
|
--width: 100%;
|
||||||
--height: 100%;
|
--height: 100%;
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.content-wrapper {
|
.content-wrapper {
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
margin: unset;
|
margin: unset;
|
||||||
max-width: unset;
|
max-width: unset;
|
||||||
@ -14,3 +10,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.TableCell.status {
|
||||||
|
&.connected {
|
||||||
|
color: var(--colorSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disconnected {
|
||||||
|
color: var(--halfGray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,42 +1,60 @@
|
|||||||
import "./landing-page.scss";
|
import "./landing-page.scss";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { computed, observable } from "mobx";
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { clusterStore } from "../../../common/cluster-store";
|
import { ItemListLayout } from "../item-object-list";
|
||||||
import { workspaceStore } from "../../../common/workspace-store";
|
import { IReactionDisposer, observable } from "mobx";
|
||||||
import { WorkspaceOverview } from "./workspace-overview";
|
import { CatalogEntityItem, CatalogEntityStore } from "./catalog-entity.store";
|
||||||
|
import { navigate } from "../../navigation";
|
||||||
|
import { kebabCase } from "lodash";
|
||||||
import { PageLayout } from "../layout/page-layout";
|
import { PageLayout } from "../layout/page-layout";
|
||||||
import { Notifications } from "../notifications";
|
|
||||||
import { Icon } from "../icon";
|
enum sortBy {
|
||||||
|
name = "name",
|
||||||
|
status = "status"
|
||||||
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class LandingPage extends React.Component {
|
export class LandingPage extends React.Component {
|
||||||
@observable showHint = true;
|
@observable private catalogEntityStore?: CatalogEntityStore;
|
||||||
|
private disposers: IReactionDisposer[] = [];
|
||||||
@computed
|
|
||||||
get clusters() {
|
|
||||||
return clusterStore.getByWorkspaceId(workspaceStore.currentWorkspaceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const noClustersInScope = !this.clusters.length;
|
this.catalogEntityStore = new CatalogEntityStore();
|
||||||
const showStartupHint = this.showHint;
|
this.disposers.push(this.catalogEntityStore.watch());
|
||||||
|
}
|
||||||
|
|
||||||
if (showStartupHint && noClustersInScope) {
|
componentWillUnmount() {
|
||||||
Notifications.info(<><b>Welcome!</b><p>Get started by associating one or more clusters to Lens</p></>, {
|
this.disposers.forEach((d) => d());
|
||||||
timeout: 30_000,
|
|
||||||
id: "landing-welcome"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const showBackButton = this.clusters.length > 0;
|
if (!this.catalogEntityStore) {
|
||||||
const header = <><Icon svg="logo-lens" big /> <h2>{workspaceStore.currentWorkspace.name}</h2></>;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout className="LandingOverview flex" header={header} provideBackButtonNavigation={showBackButton} showOnTop={true}>
|
<PageLayout className="LandingPage">
|
||||||
<WorkspaceOverview />
|
<ItemListLayout
|
||||||
|
renderHeaderTitle="Catalog"
|
||||||
|
isClusterScoped
|
||||||
|
isSearchable={true}
|
||||||
|
isSelectable={false}
|
||||||
|
className="CatalogItemList"
|
||||||
|
store={this.catalogEntityStore}
|
||||||
|
sortingCallbacks={{
|
||||||
|
[sortBy.name]: (item: CatalogEntityItem) => item.name,
|
||||||
|
[sortBy.status]: (item: CatalogEntityItem) => item.phase,
|
||||||
|
}}
|
||||||
|
renderTableHeader={[
|
||||||
|
{ title: "Name", className: "name", sortBy: sortBy.name },
|
||||||
|
{ title: "Status", className: "status", sortBy: sortBy.status },
|
||||||
|
]}
|
||||||
|
renderTableContents={(item: CatalogEntityItem) => [
|
||||||
|
item.name,
|
||||||
|
{ title: item.phase, className: kebabCase(item.phase) }
|
||||||
|
]}
|
||||||
|
onDetails={(item: CatalogEntityItem) => item.onRun({ navigate: (url: string) => navigate(url)})}
|
||||||
|
/>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,74 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { ClusterItem, WorkspaceClusterStore } from "./workspace-cluster.store";
|
|
||||||
import { autobind, cssNames } from "../../utils";
|
|
||||||
import { MenuActions, MenuActionsProps } from "../menu/menu-actions";
|
|
||||||
import { MenuItem } from "../menu";
|
|
||||||
import { Icon } from "../icon";
|
|
||||||
import { Workspace } from "../../../common/workspace-store";
|
|
||||||
import { clusterSettingsURL } from "../+cluster-settings";
|
|
||||||
import { navigate } from "../../navigation";
|
|
||||||
|
|
||||||
interface Props extends MenuActionsProps {
|
|
||||||
clusterItem: ClusterItem;
|
|
||||||
workspace: Workspace;
|
|
||||||
workspaceClusterStore: WorkspaceClusterStore;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class WorkspaceClusterMenu extends React.Component<Props> {
|
|
||||||
|
|
||||||
@autobind()
|
|
||||||
remove() {
|
|
||||||
const { clusterItem, workspaceClusterStore } = this.props;
|
|
||||||
|
|
||||||
return workspaceClusterStore.remove(clusterItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
@autobind()
|
|
||||||
gotoSettings() {
|
|
||||||
const { clusterItem } = this.props;
|
|
||||||
|
|
||||||
navigate(clusterSettingsURL({
|
|
||||||
params: {
|
|
||||||
clusterId: clusterItem.id
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
@autobind()
|
|
||||||
renderRemoveMessage() {
|
|
||||||
const { clusterItem, workspace } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<p>Remove cluster <b>{clusterItem.name}</b> from workspace <b>{workspace.name}</b>?</p>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
renderContent() {
|
|
||||||
const { toolbar } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<MenuItem onClick={this.gotoSettings}>
|
|
||||||
<Icon material="settings" interactive={toolbar} title="Settings"/>
|
|
||||||
<span className="title">Settings</span>
|
|
||||||
</MenuItem>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { clusterItem: { cluster: { isManaged } }, className, ...menuProps } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MenuActions
|
|
||||||
{...menuProps}
|
|
||||||
className={cssNames("WorkspaceClusterMenu", className)}
|
|
||||||
removeAction={isManaged ? null : this.remove}
|
|
||||||
removeConfirmationMessage={this.renderRemoveMessage}
|
|
||||||
>
|
|
||||||
{this.renderContent()}
|
|
||||||
</MenuActions>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,72 +0,0 @@
|
|||||||
import { WorkspaceId } from "../../../common/workspace-store";
|
|
||||||
import { Cluster } from "../../../main/cluster";
|
|
||||||
import { clusterStore } from "../../../common/cluster-store";
|
|
||||||
import { ItemObject, ItemStore } from "../../item.store";
|
|
||||||
import { autobind } from "../../utils";
|
|
||||||
|
|
||||||
export class ClusterItem implements ItemObject {
|
|
||||||
constructor(public cluster: Cluster) {}
|
|
||||||
|
|
||||||
get name() {
|
|
||||||
return this.cluster.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
get distribution() {
|
|
||||||
return this.cluster.metadata?.distribution?.toString() ?? "unknown";
|
|
||||||
}
|
|
||||||
|
|
||||||
get version() {
|
|
||||||
return this.cluster.version;
|
|
||||||
}
|
|
||||||
|
|
||||||
get connectionStatus() {
|
|
||||||
return this.cluster.online ? "connected" : "disconnected";
|
|
||||||
}
|
|
||||||
|
|
||||||
getName() {
|
|
||||||
return this.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
get id() {
|
|
||||||
return this.cluster.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
get clusterId() {
|
|
||||||
return this.cluster.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
getId() {
|
|
||||||
return this.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** an ItemStore of the clusters belonging to a given workspace */
|
|
||||||
@autobind()
|
|
||||||
export class WorkspaceClusterStore extends ItemStore<ClusterItem> {
|
|
||||||
|
|
||||||
workspaceId: WorkspaceId;
|
|
||||||
|
|
||||||
constructor(workspaceId: WorkspaceId) {
|
|
||||||
super();
|
|
||||||
this.workspaceId = workspaceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
loadAll() {
|
|
||||||
return this.loadItems(
|
|
||||||
() => (
|
|
||||||
clusterStore
|
|
||||||
.getByWorkspaceId(this.workspaceId)
|
|
||||||
.filter(cluster => cluster.enabled)
|
|
||||||
.map(cluster => new ClusterItem(cluster))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async remove(clusterItem: ClusterItem) {
|
|
||||||
const { cluster: { isManaged, id: clusterId }} = clusterItem;
|
|
||||||
|
|
||||||
if (!isManaged) {
|
|
||||||
return super.removeItem(clusterItem, () => clusterStore.removeById(clusterId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
.WorkspaceOverview {
|
|
||||||
max-height: 50%;
|
|
||||||
.Table {
|
|
||||||
padding-bottom: 60px;
|
|
||||||
}
|
|
||||||
.TableCell {
|
|
||||||
display: flex;
|
|
||||||
align-items: left;
|
|
||||||
|
|
||||||
&.cluster-icon {
|
|
||||||
align-items: center;
|
|
||||||
flex-grow: 0.2;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.connected {
|
|
||||||
color: var(--colorSuccess);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.TableCell.status {
|
|
||||||
flex: 0.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.TableCell.distribution {
|
|
||||||
flex: 0.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.TableCell.version {
|
|
||||||
flex: 0.2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,85 +0,0 @@
|
|||||||
import "./workspace-overview.scss";
|
|
||||||
|
|
||||||
import React, { Component } from "react";
|
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
|
||||||
import { ItemListLayout } from "../item-object-list/item-list-layout";
|
|
||||||
import { ClusterItem, WorkspaceClusterStore } from "./workspace-cluster.store";
|
|
||||||
import { navigate } from "../../navigation";
|
|
||||||
import { clusterViewURL } from "../cluster-manager/cluster-view.route";
|
|
||||||
import { WorkspaceClusterMenu } from "./workspace-cluster-menu";
|
|
||||||
import { kebabCase } from "lodash";
|
|
||||||
import { addClusterURL } from "../+add-cluster";
|
|
||||||
import { observable, reaction } from "mobx";
|
|
||||||
import { workspaceStore } from "../../../common/workspace-store";
|
|
||||||
|
|
||||||
enum sortBy {
|
|
||||||
name = "name",
|
|
||||||
distribution = "distribution",
|
|
||||||
version = "version",
|
|
||||||
online = "online"
|
|
||||||
}
|
|
||||||
|
|
||||||
@observer
|
|
||||||
export class WorkspaceOverview extends Component {
|
|
||||||
@observable private workspaceClusterStore?: WorkspaceClusterStore;
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
disposeOnUnmount(this, [
|
|
||||||
reaction(() => workspaceStore.currentWorkspaceId, workspaceId => {
|
|
||||||
this.workspaceClusterStore = new WorkspaceClusterStore(workspaceId);
|
|
||||||
this.workspaceClusterStore.loadAll().catch(error => console.log("workspaceClusterStore.loadAll", error));
|
|
||||||
}, {
|
|
||||||
fireImmediately: true,
|
|
||||||
})
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
showCluster = ({ clusterId }: ClusterItem) => {
|
|
||||||
navigate(clusterViewURL({ params: { clusterId } }));
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { workspaceClusterStore } = this;
|
|
||||||
|
|
||||||
if (!workspaceClusterStore) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ItemListLayout
|
|
||||||
renderHeaderTitle="Clusters"
|
|
||||||
isClusterScoped
|
|
||||||
isSearchable={false}
|
|
||||||
isSelectable={false}
|
|
||||||
className="WorkspaceOverview"
|
|
||||||
store={workspaceClusterStore}
|
|
||||||
sortingCallbacks={{
|
|
||||||
[sortBy.name]: (item: ClusterItem) => item.name,
|
|
||||||
[sortBy.distribution]: (item: ClusterItem) => item.distribution,
|
|
||||||
[sortBy.version]: (item: ClusterItem) => item.version,
|
|
||||||
[sortBy.online]: (item: ClusterItem) => item.connectionStatus,
|
|
||||||
}}
|
|
||||||
renderTableHeader={[
|
|
||||||
{ title: "Name", className: "name", sortBy: sortBy.name },
|
|
||||||
{ title: "Distribution", className: "distribution", sortBy: sortBy.distribution },
|
|
||||||
{ title: "Version", className: "version", sortBy: sortBy.version },
|
|
||||||
{ title: "Status", className: "status", sortBy: sortBy.online },
|
|
||||||
]}
|
|
||||||
renderTableContents={(item: ClusterItem) => [
|
|
||||||
item.name,
|
|
||||||
item.distribution,
|
|
||||||
item.version,
|
|
||||||
{ title: item.connectionStatus, className: kebabCase(item.connectionStatus) }
|
|
||||||
]}
|
|
||||||
onDetails={this.showCluster}
|
|
||||||
addRemoveButtons={{
|
|
||||||
addTooltip: "Add Cluster",
|
|
||||||
onAdd: () => navigate(addClusterURL()),
|
|
||||||
}}
|
|
||||||
renderItemMenu={(clusterItem: ClusterItem) => (
|
|
||||||
<WorkspaceClusterMenu clusterItem={clusterItem} workspace={workspaceStore.currentWorkspace} workspaceClusterStore={workspaceClusterStore}/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,64 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { observer } from "mobx-react";
|
|
||||||
import { Workspace, workspaceStore } from "../../../common/workspace-store";
|
|
||||||
import { v4 as uuid } from "uuid";
|
|
||||||
import { commandRegistry } from "../../../extensions/registries/command-registry";
|
|
||||||
import { Input, InputValidator } from "../input";
|
|
||||||
import { navigate } from "../../navigation";
|
|
||||||
import { CommandOverlay } from "../command-palette/command-container";
|
|
||||||
import { landingURL } from "../+landing-page";
|
|
||||||
import { clusterStore } from "../../../common/cluster-store";
|
|
||||||
|
|
||||||
const uniqueWorkspaceName: InputValidator = {
|
|
||||||
condition: ({ required }) => required,
|
|
||||||
message: () => `Workspace with this name already exists`,
|
|
||||||
validate: value => !workspaceStore.getByName(value),
|
|
||||||
};
|
|
||||||
|
|
||||||
@observer
|
|
||||||
export class AddWorkspace extends React.Component {
|
|
||||||
onSubmit(name: string) {
|
|
||||||
if (!name.trim()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const workspace = workspaceStore.addWorkspace(new Workspace({
|
|
||||||
id: uuid(),
|
|
||||||
name
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (!workspace) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
workspaceStore.setActive(workspace.id);
|
|
||||||
clusterStore.setActive(null);
|
|
||||||
navigate(landingURL());
|
|
||||||
CommandOverlay.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Input
|
|
||||||
placeholder="Workspace name"
|
|
||||||
autoFocus={true}
|
|
||||||
theme="round-black"
|
|
||||||
data-test-id="command-palette-workspace-add-name"
|
|
||||||
validators={[uniqueWorkspaceName]}
|
|
||||||
onSubmit={(v) => this.onSubmit(v)}
|
|
||||||
dirty={true}
|
|
||||||
showValidationLine={true} />
|
|
||||||
<small className="hint">
|
|
||||||
Please provide a new workspace name (Press "Enter" to confirm or "Escape" to cancel)
|
|
||||||
</small>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
commandRegistry.add({
|
|
||||||
id: "workspace.addWorkspace",
|
|
||||||
title: "Workspace: Add workspace ...",
|
|
||||||
scope: "global",
|
|
||||||
action: () => CommandOverlay.open(<AddWorkspace />)
|
|
||||||
});
|
|
||||||
@ -1,82 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { observer } from "mobx-react";
|
|
||||||
import { WorkspaceStore, workspaceStore } from "../../../common/workspace-store";
|
|
||||||
import { commandRegistry } from "../../../extensions/registries/command-registry";
|
|
||||||
import { Input, InputValidator } from "../input";
|
|
||||||
import { CommandOverlay } from "../command-palette/command-container";
|
|
||||||
|
|
||||||
const validateWorkspaceName: InputValidator = {
|
|
||||||
condition: ({ required }) => required,
|
|
||||||
message: () => `Workspace with this name already exists`,
|
|
||||||
validate: (value) => {
|
|
||||||
const current = workspaceStore.currentWorkspace;
|
|
||||||
|
|
||||||
if (current.name === value.trim()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return !workspaceStore.enabledWorkspacesList.find((workspace) => workspace.name === value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
interface EditWorkspaceState {
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@observer
|
|
||||||
export class EditWorkspace extends React.Component<{}, EditWorkspaceState> {
|
|
||||||
|
|
||||||
state: EditWorkspaceState = {
|
|
||||||
name: ""
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.setState({name: workspaceStore.currentWorkspace.name});
|
|
||||||
}
|
|
||||||
|
|
||||||
onSubmit(name: string) {
|
|
||||||
if (name.trim() === "") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
workspaceStore.currentWorkspace.name = name;
|
|
||||||
CommandOverlay.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange(name: string) {
|
|
||||||
this.setState({name});
|
|
||||||
}
|
|
||||||
|
|
||||||
get name() {
|
|
||||||
return this.state.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Input
|
|
||||||
placeholder="Workspace name"
|
|
||||||
autoFocus={true}
|
|
||||||
theme="round-black"
|
|
||||||
data-test-id="command-palette-workspace-add-name"
|
|
||||||
validators={[validateWorkspaceName]}
|
|
||||||
onChange={(v) => this.onChange(v)}
|
|
||||||
onSubmit={(v) => this.onSubmit(v)}
|
|
||||||
dirty={true}
|
|
||||||
value={this.name}
|
|
||||||
showValidationLine={true} />
|
|
||||||
<small className="hint">
|
|
||||||
Please provide a new workspace name (Press "Enter" to confirm or "Escape" to cancel)
|
|
||||||
</small>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
commandRegistry.add({
|
|
||||||
id: "workspace.editCurrentWorkspace",
|
|
||||||
title: "Workspace: Edit current workspace ...",
|
|
||||||
scope: "global",
|
|
||||||
action: () => CommandOverlay.open(<EditWorkspace />),
|
|
||||||
isActive: (context) => context.workspace?.id !== WorkspaceStore.defaultId
|
|
||||||
});
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./workspaces";
|
|
||||||
@ -1,68 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { observer } from "mobx-react";
|
|
||||||
import { computed} from "mobx";
|
|
||||||
import { WorkspaceStore, workspaceStore } from "../../../common/workspace-store";
|
|
||||||
import { ConfirmDialog } from "../confirm-dialog";
|
|
||||||
import { commandRegistry } from "../../../extensions/registries/command-registry";
|
|
||||||
import { Select } from "../select";
|
|
||||||
import { CommandOverlay } from "../command-palette/command-container";
|
|
||||||
|
|
||||||
@observer
|
|
||||||
export class RemoveWorkspace extends React.Component {
|
|
||||||
@computed get options() {
|
|
||||||
return workspaceStore.enabledWorkspacesList.filter((workspace) => workspace.id !== WorkspaceStore.defaultId).map((workspace) => {
|
|
||||||
return { value: workspace.id, label: workspace.name };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange(id: string) {
|
|
||||||
const workspace = workspaceStore.enabledWorkspacesList.find((workspace) => workspace.id === id);
|
|
||||||
|
|
||||||
if (!workspace ) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
CommandOverlay.close();
|
|
||||||
ConfirmDialog.open({
|
|
||||||
okButtonProps: {
|
|
||||||
label: `Remove Workspace`,
|
|
||||||
primary: false,
|
|
||||||
accent: true,
|
|
||||||
},
|
|
||||||
ok: () => {
|
|
||||||
workspaceStore.removeWorkspace(workspace);
|
|
||||||
},
|
|
||||||
message: (
|
|
||||||
<div className="confirm flex column gaps">
|
|
||||||
<p>
|
|
||||||
Are you sure you want remove workspace <b>{workspace.name}</b>?
|
|
||||||
</p>
|
|
||||||
<p className="info">
|
|
||||||
All clusters within workspace will be cleared as well
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
onChange={(v) => this.onChange(v.value)}
|
|
||||||
components={{ DropdownIndicator: null, IndicatorSeparator: null }}
|
|
||||||
menuIsOpen={true}
|
|
||||||
options={this.options}
|
|
||||||
autoFocus={true}
|
|
||||||
escapeClearsValue={false}
|
|
||||||
data-test-id="command-palette-workspace-remove-select"
|
|
||||||
placeholder="Remove workspace" />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
commandRegistry.add({
|
|
||||||
id: "workspace.removeWorkspace",
|
|
||||||
title: "Workspace: Remove workspace ...",
|
|
||||||
scope: "global",
|
|
||||||
action: () => CommandOverlay.open(<RemoveWorkspace />)
|
|
||||||
});
|
|
||||||
@ -1,99 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { observer } from "mobx-react";
|
|
||||||
import { computed} from "mobx";
|
|
||||||
import { WorkspaceStore, workspaceStore } from "../../../common/workspace-store";
|
|
||||||
import { commandRegistry } from "../../../extensions/registries/command-registry";
|
|
||||||
import { Select } from "../select";
|
|
||||||
import { navigate } from "../../navigation";
|
|
||||||
import { CommandOverlay } from "../command-palette/command-container";
|
|
||||||
import { AddWorkspace } from "./add-workspace";
|
|
||||||
import { RemoveWorkspace } from "./remove-workspace";
|
|
||||||
import { EditWorkspace } from "./edit-workspace";
|
|
||||||
import { landingURL } from "../+landing-page";
|
|
||||||
import { clusterViewURL } from "../cluster-manager/cluster-view.route";
|
|
||||||
|
|
||||||
@observer
|
|
||||||
export class ChooseWorkspace extends React.Component {
|
|
||||||
private static overviewActionId = "__overview__";
|
|
||||||
private static addActionId = "__add__";
|
|
||||||
private static removeActionId = "__remove__";
|
|
||||||
private static editActionId = "__edit__";
|
|
||||||
|
|
||||||
@computed get options() {
|
|
||||||
const options = workspaceStore.enabledWorkspacesList.map((workspace) => {
|
|
||||||
return { value: workspace.id, label: workspace.name };
|
|
||||||
});
|
|
||||||
|
|
||||||
options.push({ value: ChooseWorkspace.overviewActionId, label: "Show current workspace overview ..." });
|
|
||||||
|
|
||||||
options.push({ value: ChooseWorkspace.addActionId, label: "Add workspace ..." });
|
|
||||||
|
|
||||||
if (options.length > 1) {
|
|
||||||
options.push({ value: ChooseWorkspace.removeActionId, label: "Remove workspace ..." });
|
|
||||||
|
|
||||||
if (workspaceStore.currentWorkspace.id !== WorkspaceStore.defaultId) {
|
|
||||||
options.push({ value: ChooseWorkspace.editActionId, label: "Edit current workspace ..." });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange(id: string) {
|
|
||||||
if (id === ChooseWorkspace.overviewActionId) {
|
|
||||||
navigate(landingURL()); // overview of active workspace. TODO: change name from landing
|
|
||||||
CommandOverlay.close();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (id === ChooseWorkspace.addActionId) {
|
|
||||||
CommandOverlay.open(<AddWorkspace />);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (id === ChooseWorkspace.removeActionId) {
|
|
||||||
CommandOverlay.open(<RemoveWorkspace />);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (id === ChooseWorkspace.editActionId) {
|
|
||||||
CommandOverlay.open(<EditWorkspace />);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
workspaceStore.setActive(id);
|
|
||||||
const clusterId = workspaceStore.getById(id).lastActiveClusterId;
|
|
||||||
|
|
||||||
if (clusterId) {
|
|
||||||
navigate(clusterViewURL({ params: { clusterId } }));
|
|
||||||
} else {
|
|
||||||
navigate(landingURL());
|
|
||||||
}
|
|
||||||
|
|
||||||
CommandOverlay.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
onChange={(v) => this.onChange(v.value)}
|
|
||||||
components={{ DropdownIndicator: null, IndicatorSeparator: null }}
|
|
||||||
menuIsOpen={true}
|
|
||||||
options={this.options}
|
|
||||||
autoFocus={true}
|
|
||||||
escapeClearsValue={false}
|
|
||||||
placeholder="Switch to workspace" />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
commandRegistry.add({
|
|
||||||
id: "workspace.chooseWorkspace",
|
|
||||||
title: "Workspace: Switch to workspace ...",
|
|
||||||
scope: "global",
|
|
||||||
action: () => CommandOverlay.open(<ChooseWorkspace />)
|
|
||||||
});
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { computed, observable, reaction } from "mobx";
|
import { observable } from "mobx";
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import { Redirect, Route, Router, Switch } from "react-router";
|
import { Redirect, Route, Router, Switch } from "react-router";
|
||||||
import { history } from "../navigation";
|
import { history } from "../navigation";
|
||||||
@ -36,7 +36,7 @@ import { webFrame } from "electron";
|
|||||||
import { clusterPageRegistry, getExtensionPageUrl } from "../../extensions/registries/page-registry";
|
import { clusterPageRegistry, getExtensionPageUrl } from "../../extensions/registries/page-registry";
|
||||||
import { extensionLoader } from "../../extensions/extension-loader";
|
import { extensionLoader } from "../../extensions/extension-loader";
|
||||||
import { appEventBus } from "../../common/event-bus";
|
import { appEventBus } from "../../common/event-bus";
|
||||||
import { broadcastMessage, requestMain } from "../../common/ipc";
|
import { requestMain } from "../../common/ipc";
|
||||||
import whatInput from "what-input";
|
import whatInput from "what-input";
|
||||||
import { clusterSetFrameIdHandler } from "../../common/cluster-ipc";
|
import { clusterSetFrameIdHandler } from "../../common/cluster-ipc";
|
||||||
import { ClusterPageMenuRegistration, clusterPageMenuRegistry } from "../../extensions/registries";
|
import { ClusterPageMenuRegistration, clusterPageMenuRegistry } from "../../extensions/registries";
|
||||||
@ -86,20 +86,12 @@ export class App extends React.Component {
|
|||||||
disposeOnUnmount(this, [
|
disposeOnUnmount(this, [
|
||||||
kubeWatchApi.subscribeStores([podsStore, nodesStore, eventStore], {
|
kubeWatchApi.subscribeStores([podsStore, nodesStore, eventStore], {
|
||||||
preload: true,
|
preload: true,
|
||||||
}),
|
})
|
||||||
|
|
||||||
reaction(() => this.warningsTotal, (count: number) => {
|
|
||||||
broadcastMessage(`cluster-warning-event-count:${getHostedCluster().id}`, count);
|
|
||||||
}),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@observable startUrl = isAllowedResource(["events", "nodes", "pods"]) ? clusterURL() : workloadsURL();
|
@observable startUrl = isAllowedResource(["events", "nodes", "pods"]) ? clusterURL() : workloadsURL();
|
||||||
|
|
||||||
@computed get warningsTotal(): number {
|
|
||||||
return nodesStore.getWarningsCount() + eventStore.getWarningsCount();
|
|
||||||
}
|
|
||||||
|
|
||||||
getTabLayoutRoutes(menuItem: ClusterPageMenuRegistration) {
|
getTabLayoutRoutes(menuItem: ClusterPageMenuRegistration) {
|
||||||
const routes: TabLayoutRoute[] = [];
|
const routes: TabLayoutRoute[] = [];
|
||||||
|
|
||||||
|
|||||||
@ -1,38 +0,0 @@
|
|||||||
.ClusterIcon {
|
|
||||||
--size: 37px;
|
|
||||||
|
|
||||||
position: relative;
|
|
||||||
border-radius: $radius;
|
|
||||||
padding: $radius;
|
|
||||||
user-select: none;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&.interactive {
|
|
||||||
img {
|
|
||||||
opacity: .55;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active, &.interactive:hover {
|
|
||||||
background-color: #fff;
|
|
||||||
|
|
||||||
img {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: var(--size);
|
|
||||||
height: var(--size);
|
|
||||||
}
|
|
||||||
|
|
||||||
.Badge {
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
margin: -$padding;
|
|
||||||
font-size: $font-size-small;
|
|
||||||
background: $colorError;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,81 +0,0 @@
|
|||||||
import "./cluster-icon.scss";
|
|
||||||
|
|
||||||
import React, { DOMAttributes } from "react";
|
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
|
||||||
import { Params as HashiconParams } from "@emeraldpay/hashicon";
|
|
||||||
import { Hashicon } from "@emeraldpay/hashicon-react";
|
|
||||||
import { Cluster } from "../../../main/cluster";
|
|
||||||
import { cssNames, IClassName } from "../../utils";
|
|
||||||
import { Badge } from "../badge";
|
|
||||||
import { Tooltip } from "../tooltip";
|
|
||||||
import { subscribeToBroadcast } from "../../../common/ipc";
|
|
||||||
import { observable } from "mobx";
|
|
||||||
|
|
||||||
interface Props extends DOMAttributes<HTMLElement> {
|
|
||||||
cluster: Cluster;
|
|
||||||
className?: IClassName;
|
|
||||||
errorClass?: IClassName;
|
|
||||||
showErrors?: boolean;
|
|
||||||
showTooltip?: boolean;
|
|
||||||
interactive?: boolean;
|
|
||||||
isActive?: boolean;
|
|
||||||
options?: HashiconParams;
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultProps: Partial<Props> = {
|
|
||||||
showErrors: true,
|
|
||||||
showTooltip: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
@observer
|
|
||||||
export class ClusterIcon extends React.Component<Props> {
|
|
||||||
static defaultProps = defaultProps as object;
|
|
||||||
|
|
||||||
@observable eventCount = 0;
|
|
||||||
|
|
||||||
get eventCountBroadcast() {
|
|
||||||
return `cluster-warning-event-count:${this.props.cluster.id}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const subscriber = subscribeToBroadcast(this.eventCountBroadcast, (ev, eventCount) => {
|
|
||||||
this.eventCount = eventCount;
|
|
||||||
});
|
|
||||||
|
|
||||||
disposeOnUnmount(this, [
|
|
||||||
subscriber
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
cluster, showErrors, showTooltip, errorClass, options, interactive, isActive,
|
|
||||||
children, ...elemProps
|
|
||||||
} = this.props;
|
|
||||||
const { name, preferences, id: clusterId, online } = cluster;
|
|
||||||
const eventCount = this.eventCount;
|
|
||||||
const { icon } = preferences;
|
|
||||||
const clusterIconId = `cluster-icon-${clusterId}`;
|
|
||||||
const className = cssNames("ClusterIcon flex inline", this.props.className, {
|
|
||||||
interactive: interactive !== undefined ? interactive : !!this.props.onClick,
|
|
||||||
active: isActive,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div {...elemProps} className={className} id={showTooltip ? clusterIconId : null}>
|
|
||||||
{showTooltip && (
|
|
||||||
<Tooltip targetId={clusterIconId}>{name}</Tooltip>
|
|
||||||
)}
|
|
||||||
{icon && <img src={icon} alt={name}/>}
|
|
||||||
{!icon && <Hashicon value={clusterId} options={options}/>}
|
|
||||||
{showErrors && eventCount > 0 && !isActive && online && (
|
|
||||||
<Badge
|
|
||||||
className={cssNames("events-count", errorClass)}
|
|
||||||
label={eventCount >= 1000 ? `${Math.ceil(eventCount / 1000)}k+` : eventCount}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./cluster-icon";
|
|
||||||
@ -2,11 +2,7 @@ import "./bottom-bar.scss";
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Icon } from "../icon";
|
|
||||||
import { workspaceStore } from "../../../common/workspace-store";
|
|
||||||
import { StatusBarRegistration, statusBarRegistry } from "../../../extensions/registries";
|
import { StatusBarRegistration, statusBarRegistry } from "../../../extensions/registries";
|
||||||
import { CommandOverlay } from "../command-palette/command-container";
|
|
||||||
import { ChooseWorkspace } from "../+workspaces";
|
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class BottomBar extends React.Component {
|
export class BottomBar extends React.Component {
|
||||||
@ -45,14 +41,8 @@ export class BottomBar extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { currentWorkspace } = workspaceStore;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="BottomBar flex gaps">
|
<div className="BottomBar flex gaps">
|
||||||
<div id="current-workspace" data-test-id="current-workspace" className="flex gaps align-center" onClick={() => CommandOverlay.open(<ChooseWorkspace />)}>
|
|
||||||
<Icon smallest material="layers"/>
|
|
||||||
<span className="workspace-name" data-test-id="current-workspace-name">{currentWorkspace.name}</span>
|
|
||||||
</div>
|
|
||||||
{this.renderRegisteredItems()}
|
{this.renderRegisteredItems()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -4,19 +4,19 @@ import React from "react";
|
|||||||
import { Redirect, Route, Switch } from "react-router";
|
import { Redirect, Route, Switch } from "react-router";
|
||||||
import { comparer, reaction } from "mobx";
|
import { comparer, reaction } from "mobx";
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import { ClustersMenu } from "./clusters-menu";
|
|
||||||
import { BottomBar } from "./bottom-bar";
|
import { BottomBar } from "./bottom-bar";
|
||||||
import { LandingPage, landingRoute, landingURL } from "../+landing-page";
|
import { LandingPage, landingRoute, landingURL } from "../+landing-page";
|
||||||
import { Preferences, preferencesRoute } from "../+preferences";
|
import { Preferences, preferencesRoute } from "../+preferences";
|
||||||
import { AddCluster, addClusterRoute } from "../+add-cluster";
|
import { AddCluster, addClusterRoute } from "../+add-cluster";
|
||||||
import { ClusterView } from "./cluster-view";
|
import { ClusterView } from "./cluster-view";
|
||||||
import { ClusterSettings, clusterSettingsRoute } from "../+cluster-settings";
|
import { ClusterSettings, clusterSettingsRoute } from "../+cluster-settings";
|
||||||
import { clusterViewRoute, clusterViewURL } from "./cluster-view.route";
|
import { clusterViewRoute } from "./cluster-view.route";
|
||||||
import { clusterStore } from "../../../common/cluster-store";
|
import { clusterStore } from "../../../common/cluster-store";
|
||||||
import { hasLoadedView, initView, lensViews, refreshViews } from "./lens-views";
|
import { hasLoadedView, initView, lensViews, refreshViews } from "./lens-views";
|
||||||
import { globalPageRegistry } from "../../../extensions/registries/page-registry";
|
import { globalPageRegistry } from "../../../extensions/registries/page-registry";
|
||||||
import { Extensions, extensionsRoute } from "../+extensions";
|
import { Extensions, extensionsRoute } from "../+extensions";
|
||||||
import { getMatchedClusterId } from "../../navigation";
|
import { getMatchedClusterId } from "../../navigation";
|
||||||
|
import { HotbarMenu } from "../hotbar/hotbar-menu";
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class ClusterManager extends React.Component {
|
export class ClusterManager extends React.Component {
|
||||||
@ -44,16 +44,6 @@ export class ClusterManager extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get startUrl() {
|
get startUrl() {
|
||||||
const { activeClusterId } = clusterStore;
|
|
||||||
|
|
||||||
if (activeClusterId) {
|
|
||||||
return clusterViewURL({
|
|
||||||
params: {
|
|
||||||
clusterId: activeClusterId
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return landingURL();
|
return landingURL();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,7 +65,7 @@ export class ClusterManager extends React.Component {
|
|||||||
<Redirect exact to={this.startUrl}/>
|
<Redirect exact to={this.startUrl}/>
|
||||||
</Switch>
|
</Switch>
|
||||||
</main>
|
</main>
|
||||||
<ClustersMenu/>
|
<HotbarMenu/>
|
||||||
<BottomBar/>
|
<BottomBar/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,67 +0,0 @@
|
|||||||
.ClustersMenu {
|
|
||||||
$spacing: $padding * 2;
|
|
||||||
|
|
||||||
position: relative;
|
|
||||||
text-align: center;
|
|
||||||
background: $clusterMenuBackground;
|
|
||||||
border-right: 1px solid $clusterMenuBorderColor;
|
|
||||||
padding: $spacing 0;
|
|
||||||
min-width: 75px;
|
|
||||||
|
|
||||||
.is-mac &:before {
|
|
||||||
content: "";
|
|
||||||
height: 20px; // extra spacing for mac-os "traffic-light" buttons
|
|
||||||
}
|
|
||||||
|
|
||||||
.clusters {
|
|
||||||
@include hidden-scrollbar;
|
|
||||||
padding: 0 $spacing; // extra spacing for cluster-icon's badge
|
|
||||||
margin-bottom: $margin;
|
|
||||||
|
|
||||||
.ClusterIcon {
|
|
||||||
margin-bottom: $margin * 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:empty {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .WorkspaceMenu {
|
|
||||||
position: relative;
|
|
||||||
margin-bottom: $margin;
|
|
||||||
|
|
||||||
.Icon {
|
|
||||||
margin-bottom: $margin * 1.5;
|
|
||||||
border-radius: $radius;
|
|
||||||
padding: $padding / 3;
|
|
||||||
color: var(--textColorPrimary);
|
|
||||||
background: unset;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
box-shadow: none;
|
|
||||||
color: var(--textColorAccent);
|
|
||||||
background-color: unset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .extensions {
|
|
||||||
&:not(:empty) {
|
|
||||||
padding-top: $spacing;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Icon {
|
|
||||||
--size: 40px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.Menu.WorkspaceMenu {
|
|
||||||
z-index: 2; // Place behind Preferences, Extension pages etc...
|
|
||||||
}
|
|
||||||
@ -1,198 +0,0 @@
|
|||||||
import "./clusters-menu.scss";
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { remote } from "electron";
|
|
||||||
import type { Cluster } from "../../../main/cluster";
|
|
||||||
import { DragDropContext, Draggable, DraggableProvided, Droppable, DroppableProvided, DropResult } from "react-beautiful-dnd";
|
|
||||||
import { observer } from "mobx-react";
|
|
||||||
import { ClusterId, clusterStore } from "../../../common/cluster-store";
|
|
||||||
import { workspaceStore } from "../../../common/workspace-store";
|
|
||||||
import { ClusterIcon } from "../cluster-icon";
|
|
||||||
import { Icon } from "../icon";
|
|
||||||
import { autobind, cssNames, IClassName } from "../../utils";
|
|
||||||
import { isActiveRoute, navigate } from "../../navigation";
|
|
||||||
import { addClusterURL } from "../+add-cluster";
|
|
||||||
import { landingURL } from "../+landing-page";
|
|
||||||
import { clusterViewURL } from "./cluster-view.route";
|
|
||||||
import { ClusterActions } from "./cluster-actions";
|
|
||||||
import { getExtensionPageUrl, globalPageMenuRegistry, globalPageRegistry } from "../../../extensions/registries";
|
|
||||||
import { commandRegistry } from "../../../extensions/registries/command-registry";
|
|
||||||
import { CommandOverlay } from "../command-palette/command-container";
|
|
||||||
import { computed, observable } from "mobx";
|
|
||||||
import { Select } from "../select";
|
|
||||||
import { Menu, MenuItem } from "../menu";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
className?: IClassName;
|
|
||||||
}
|
|
||||||
|
|
||||||
@observer
|
|
||||||
export class ClustersMenu extends React.Component<Props> {
|
|
||||||
@observable workspaceMenuVisible = false;
|
|
||||||
|
|
||||||
showCluster = (clusterId: ClusterId) => {
|
|
||||||
navigate(clusterViewURL({ params: { clusterId } }));
|
|
||||||
};
|
|
||||||
|
|
||||||
showContextMenu = (cluster: Cluster) => {
|
|
||||||
const { Menu, MenuItem } = remote;
|
|
||||||
const menu = new Menu();
|
|
||||||
const actions = ClusterActions(cluster);
|
|
||||||
|
|
||||||
menu.append(new MenuItem({
|
|
||||||
label: `Settings`,
|
|
||||||
click: actions.showSettings
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (cluster.online) {
|
|
||||||
menu.append(new MenuItem({
|
|
||||||
label: `Disconnect`,
|
|
||||||
click: actions.disconnect
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!cluster.isManaged) {
|
|
||||||
menu.append(new MenuItem({
|
|
||||||
label: `Remove`,
|
|
||||||
click: actions.remove
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
menu.popup({
|
|
||||||
window: remote.getCurrentWindow()
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
@autobind()
|
|
||||||
swapClusterIconOrder(result: DropResult) {
|
|
||||||
if (result.reason === "DROP") {
|
|
||||||
const { currentWorkspaceId } = workspaceStore;
|
|
||||||
const {
|
|
||||||
source: { index: from },
|
|
||||||
destination: { index: to },
|
|
||||||
} = result;
|
|
||||||
|
|
||||||
clusterStore.swapIconOrders(currentWorkspaceId, from, to);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { className } = this.props;
|
|
||||||
const workspace = workspaceStore.getById(workspaceStore.currentWorkspaceId);
|
|
||||||
const clusters = clusterStore.getByWorkspaceId(workspace.id).filter(cluster => cluster.enabled);
|
|
||||||
const activeClusterId = clusterStore.activeCluster;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cssNames("ClustersMenu flex column", className)}>
|
|
||||||
<div className="clusters flex column gaps">
|
|
||||||
<DragDropContext onDragEnd={this.swapClusterIconOrder}>
|
|
||||||
<Droppable droppableId="cluster-menu" type="CLUSTER">
|
|
||||||
{({ innerRef, droppableProps, placeholder }: DroppableProvided) => (
|
|
||||||
<div ref={innerRef} {...droppableProps}>
|
|
||||||
{clusters.map((cluster, index) => {
|
|
||||||
const isActive = cluster.id === activeClusterId;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Draggable draggableId={cluster.id} index={index} key={cluster.id}>
|
|
||||||
{({ draggableProps, dragHandleProps, innerRef }: DraggableProvided) => (
|
|
||||||
<div ref={innerRef} {...draggableProps} {...dragHandleProps}>
|
|
||||||
<ClusterIcon
|
|
||||||
key={cluster.id}
|
|
||||||
showErrors={true}
|
|
||||||
cluster={cluster}
|
|
||||||
isActive={isActive}
|
|
||||||
onClick={() => this.showCluster(cluster.id)}
|
|
||||||
onContextMenu={() => this.showContextMenu(cluster)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Draggable>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{placeholder}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Droppable>
|
|
||||||
</DragDropContext>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="WorkspaceMenu">
|
|
||||||
<Icon big material="menu" id="workspace-menu-icon" data-test-id="workspace-menu" />
|
|
||||||
<Menu
|
|
||||||
usePortal
|
|
||||||
htmlFor="workspace-menu-icon"
|
|
||||||
className="WorkspaceMenu"
|
|
||||||
isOpen={this.workspaceMenuVisible}
|
|
||||||
open={() => this.workspaceMenuVisible = true}
|
|
||||||
close={() => this.workspaceMenuVisible = false}
|
|
||||||
toggleEvent="click"
|
|
||||||
>
|
|
||||||
<MenuItem onClick={() => navigate(addClusterURL())} data-test-id="add-cluster-menu-item">
|
|
||||||
<Icon small material="add" /> Add Cluster
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem onClick={() => navigate(landingURL())} data-test-id="workspace-overview-menu-item">
|
|
||||||
<Icon small material="dashboard" /> Workspace Overview
|
|
||||||
</MenuItem>
|
|
||||||
</Menu>
|
|
||||||
</div>
|
|
||||||
<div className="extensions">
|
|
||||||
{globalPageMenuRegistry.getItems().map(({ title, target, components: { Icon } }) => {
|
|
||||||
const registeredPage = globalPageRegistry.getByPageTarget(target);
|
|
||||||
|
|
||||||
if (!registeredPage){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const pageUrl = getExtensionPageUrl(target);
|
|
||||||
const isActive = isActiveRoute(registeredPage.url);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Icon
|
|
||||||
key={pageUrl}
|
|
||||||
tooltip={title}
|
|
||||||
active={isActive}
|
|
||||||
onClick={() => navigate(pageUrl)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@observer
|
|
||||||
export class ChooseCluster extends React.Component {
|
|
||||||
@computed get options() {
|
|
||||||
const clusters = clusterStore.getByWorkspaceId(workspaceStore.currentWorkspaceId).filter(cluster => cluster.enabled);
|
|
||||||
const options = clusters.map((cluster) => {
|
|
||||||
return { value: cluster.id, label: cluster.name };
|
|
||||||
});
|
|
||||||
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange(clusterId: string) {
|
|
||||||
navigate(clusterViewURL({ params: { clusterId } }));
|
|
||||||
CommandOverlay.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
onChange={(v) => this.onChange(v.value)}
|
|
||||||
components={{ DropdownIndicator: null, IndicatorSeparator: null }}
|
|
||||||
menuIsOpen={true}
|
|
||||||
options={this.options}
|
|
||||||
autoFocus={true}
|
|
||||||
escapeClearsValue={false}
|
|
||||||
placeholder="Switch to cluster" />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
commandRegistry.add({
|
|
||||||
id: "workspace.chooseCluster",
|
|
||||||
title: "Workspace: Switch to cluster ...",
|
|
||||||
scope: "global",
|
|
||||||
action: () => CommandOverlay.open(<ChooseCluster />)
|
|
||||||
});
|
|
||||||
@ -9,7 +9,6 @@ import { subscribeToBroadcast } from "../../../common/ipc";
|
|||||||
import { CommandDialog } from "./command-dialog";
|
import { CommandDialog } from "./command-dialog";
|
||||||
import { CommandRegistration, commandRegistry } from "../../../extensions/registries/command-registry";
|
import { CommandRegistration, commandRegistry } from "../../../extensions/registries/command-registry";
|
||||||
import { clusterStore } from "../../../common/cluster-store";
|
import { clusterStore } from "../../../common/cluster-store";
|
||||||
import { workspaceStore } from "../../../common/workspace-store";
|
|
||||||
|
|
||||||
export type CommandDialogEvent = {
|
export type CommandDialogEvent = {
|
||||||
component: React.ReactElement
|
component: React.ReactElement
|
||||||
@ -49,8 +48,7 @@ export class CommandContainer extends React.Component<{ clusterId?: string }> {
|
|||||||
|
|
||||||
private runCommand(command: CommandRegistration) {
|
private runCommand(command: CommandRegistration) {
|
||||||
command.action({
|
command.action({
|
||||||
cluster: clusterStore.active,
|
cluster: clusterStore.active
|
||||||
workspace: workspaceStore.currentWorkspace
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import { observer } from "mobx-react";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { commandRegistry } from "../../../extensions/registries/command-registry";
|
import { commandRegistry } from "../../../extensions/registries/command-registry";
|
||||||
import { clusterStore } from "../../../common/cluster-store";
|
import { clusterStore } from "../../../common/cluster-store";
|
||||||
import { workspaceStore } from "../../../common/workspace-store";
|
|
||||||
import { CommandOverlay } from "./command-container";
|
import { CommandOverlay } from "./command-container";
|
||||||
import { broadcastMessage } from "../../../common/ipc";
|
import { broadcastMessage } from "../../../common/ipc";
|
||||||
import { navigate } from "../../navigation";
|
import { navigate } from "../../navigation";
|
||||||
@ -17,8 +16,7 @@ export class CommandDialog extends React.Component {
|
|||||||
|
|
||||||
@computed get options() {
|
@computed get options() {
|
||||||
const context = {
|
const context = {
|
||||||
cluster: clusterStore.active,
|
cluster: clusterStore.active
|
||||||
workspace: workspaceStore.currentWorkspace
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return commandRegistry.getItems().filter((command) => {
|
return commandRegistry.getItems().filter((command) => {
|
||||||
@ -56,8 +54,7 @@ export class CommandDialog extends React.Component {
|
|||||||
|
|
||||||
if (command.scope === "global") {
|
if (command.scope === "global") {
|
||||||
action({
|
action({
|
||||||
cluster: clusterStore.active,
|
cluster: clusterStore.active
|
||||||
workspace: workspaceStore.currentWorkspace
|
|
||||||
});
|
});
|
||||||
} else if(clusterStore.active) {
|
} else if(clusterStore.active) {
|
||||||
navigate(clusterViewURL({
|
navigate(clusterViewURL({
|
||||||
|
|||||||
44
src/renderer/components/hotbar/hotbar-icon.scss
Normal file
44
src/renderer/components/hotbar/hotbar-icon.scss
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
.HotbarIcon {
|
||||||
|
--size: 37px;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 2px;
|
||||||
|
user-select: none;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
div.MuiAvatar-colorDefault {
|
||||||
|
font-weight:500;
|
||||||
|
text-transform: uppercase;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.active {
|
||||||
|
background-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.default {
|
||||||
|
background-color: var(--halfGray);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
margin-left: -3px;
|
||||||
|
border: 3px solid #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active, &.interactive:hover {
|
||||||
|
|
||||||
|
div {
|
||||||
|
background-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: var(--size);
|
||||||
|
height: var(--size);
|
||||||
|
}
|
||||||
|
}
|
||||||
58
src/renderer/components/hotbar/hotbar-icon.tsx
Normal file
58
src/renderer/components/hotbar/hotbar-icon.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import "./hotbar-icon.scss";
|
||||||
|
|
||||||
|
import React, { DOMAttributes } from "react";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { cssNames, IClassName } from "../../utils";
|
||||||
|
import { Tooltip } from "../tooltip";
|
||||||
|
import { Avatar } from "@material-ui/core";
|
||||||
|
import { CatalogEntity } from "../../../common/catalog-entity";
|
||||||
|
|
||||||
|
interface Props extends DOMAttributes<HTMLElement> {
|
||||||
|
entity: CatalogEntity;
|
||||||
|
className?: IClassName;
|
||||||
|
errorClass?: IClassName;
|
||||||
|
isActive?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class HotbarIcon extends React.Component<Props> {
|
||||||
|
get iconString() {
|
||||||
|
let splittedName = this.props.entity.metadata.name.split(" ");
|
||||||
|
|
||||||
|
if (splittedName.length === 1) {
|
||||||
|
splittedName = splittedName[0].split("-");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (splittedName.length === 1) {
|
||||||
|
splittedName = splittedName[0].split("@");
|
||||||
|
}
|
||||||
|
|
||||||
|
splittedName = splittedName.map((part) => part.replace(/\W/g, ""));
|
||||||
|
|
||||||
|
if (splittedName.length === 1) {
|
||||||
|
return splittedName[0].substring(0, 2);
|
||||||
|
} else {
|
||||||
|
return splittedName[0].substring(0, 1) + splittedName[1].substring(0, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
entity, errorClass, isActive,
|
||||||
|
children, ...elemProps
|
||||||
|
} = this.props;
|
||||||
|
const entityIconId = `hotbar-icon-${entity.metadata.uid}`;
|
||||||
|
const className = cssNames("HotbarIcon flex inline", this.props.className, {
|
||||||
|
interactive: true,
|
||||||
|
active: isActive,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div {...elemProps} className={className} id={entityIconId}>
|
||||||
|
<Tooltip targetId={entityIconId}>{entity.metadata.name}</Tooltip>
|
||||||
|
<Avatar variant="square" className={isActive ? "active" : "default"}>{this.iconString}</Avatar>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/renderer/components/hotbar/hotbar-menu.scss
Normal file
29
src/renderer/components/hotbar/hotbar-menu.scss
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
.HotbarMenu {
|
||||||
|
$spacing: $padding * 2;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
text-align: center;
|
||||||
|
background: $clusterMenuBackground;
|
||||||
|
border-right: 1px solid $clusterMenuBorderColor;
|
||||||
|
padding: $spacing 0;
|
||||||
|
min-width: 75px;
|
||||||
|
|
||||||
|
.is-mac &:before {
|
||||||
|
content: "";
|
||||||
|
height: 20px; // extra spacing for mac-os "traffic-light" buttons
|
||||||
|
}
|
||||||
|
|
||||||
|
.clusters {
|
||||||
|
@include hidden-scrollbar;
|
||||||
|
padding: 0 $spacing; // extra spacing for cluster-icon's badge
|
||||||
|
margin-bottom: $margin;
|
||||||
|
|
||||||
|
.ClusterIcon {
|
||||||
|
margin-bottom: $margin * 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/renderer/components/hotbar/hotbar-menu.tsx
Normal file
43
src/renderer/components/hotbar/hotbar-menu.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import "./hotbar-menu.scss";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { HotbarIcon } from "./hotbar-icon";
|
||||||
|
import { cssNames, IClassName } from "../../utils";
|
||||||
|
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||||
|
import { KubernetesCluster } from "../../../common/catalog-entities/kubernetes-cluster";
|
||||||
|
import { navigate } from "../../navigation";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
className?: IClassName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class HotbarMenu extends React.Component<Props> {
|
||||||
|
render() {
|
||||||
|
const { className } = this.props;
|
||||||
|
const items = catalogEntityRegistry.getItemsForApiKind<KubernetesCluster>("entity.k8slens.dev/v1alpha1", "KubernetesCluster");
|
||||||
|
const runContext = {
|
||||||
|
navigate: (url: string) => navigate(url)
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cssNames("HotbarMenu flex column", className)}>
|
||||||
|
<div className="clusters flex column gaps">
|
||||||
|
{items.map((entity) => {
|
||||||
|
return (
|
||||||
|
<HotbarIcon
|
||||||
|
key={entity.metadata.uid}
|
||||||
|
entity={entity}
|
||||||
|
isActive={entity.status.active}
|
||||||
|
onClick={() => entity.onRun(runContext)}
|
||||||
|
onContextMenu={() => entity.onContextMenuOpen()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -6,7 +6,6 @@ import "@testing-library/jest-dom/extend-expect";
|
|||||||
|
|
||||||
import { MainLayoutHeader } from "../main-layout-header";
|
import { MainLayoutHeader } from "../main-layout-header";
|
||||||
import { Cluster } from "../../../../main/cluster";
|
import { Cluster } from "../../../../main/cluster";
|
||||||
import { workspaceStore } from "../../../../common/workspace-store";
|
|
||||||
import { broadcastMessage, requestMain } from "../../../../common/ipc";
|
import { broadcastMessage, requestMain } from "../../../../common/ipc";
|
||||||
import { clusterDisconnectHandler } from "../../../../common/cluster-ipc";
|
import { clusterDisconnectHandler } from "../../../../common/cluster-ipc";
|
||||||
import { ConfirmDialog } from "../../confirm-dialog";
|
import { ConfirmDialog } from "../../confirm-dialog";
|
||||||
@ -19,7 +18,6 @@ const cluster: Cluster = new Cluster({
|
|||||||
id: "foo",
|
id: "foo",
|
||||||
contextName: "minikube",
|
contextName: "minikube",
|
||||||
kubeConfigPath: "minikube-config.yml",
|
kubeConfigPath: "minikube-config.yml",
|
||||||
workspace: workspaceStore.currentWorkspaceId,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("<MainLayoutHeader />", () => {
|
describe("<MainLayoutHeader />", () => {
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { NavigationTree, RecursiveTreeView } from "../tree-view";
|
|||||||
|
|
||||||
export interface PageLayoutProps extends React.DOMAttributes<any> {
|
export interface PageLayoutProps extends React.DOMAttributes<any> {
|
||||||
className?: IClassName;
|
className?: IClassName;
|
||||||
header: React.ReactNode;
|
header?: React.ReactNode;
|
||||||
headerClass?: IClassName;
|
headerClass?: IClassName;
|
||||||
contentClass?: IClassName;
|
contentClass?: IClassName;
|
||||||
provideBackButtonNavigation?: boolean;
|
provideBackButtonNavigation?: boolean;
|
||||||
@ -65,7 +65,7 @@ export class PageLayout extends React.Component<PageLayoutProps> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div {...elemProps} className={className}>
|
<div {...elemProps} className={className}>
|
||||||
<div className={cssNames("header flex gaps align-center", headerClass)}>
|
{ header && (<div className={cssNames("header flex gaps align-center", headerClass)}>
|
||||||
{header}
|
{header}
|
||||||
{provideBackButtonNavigation && (
|
{provideBackButtonNavigation && (
|
||||||
<Icon
|
<Icon
|
||||||
@ -75,6 +75,10 @@ export class PageLayout extends React.Component<PageLayoutProps> {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
{ !header && (
|
||||||
|
<div className="flex gaps"></div>
|
||||||
|
)}
|
||||||
{ navigation && (
|
{ navigation && (
|
||||||
<nav className="content-navigation">
|
<nav className="content-navigation">
|
||||||
<RecursiveTreeView data={navigation}/>
|
<RecursiveTreeView data={navigation}/>
|
||||||
|
|||||||
@ -16,10 +16,12 @@ import { LensProtocolRouterRenderer, bindProtocolAddRouteHandlers } from "./prot
|
|||||||
import { registerIpcHandlers } from "./ipc";
|
import { registerIpcHandlers } from "./ipc";
|
||||||
import { ipcRenderer } from "electron";
|
import { ipcRenderer } from "electron";
|
||||||
import { IpcRendererNavigationEvents } from "./navigation/events";
|
import { IpcRendererNavigationEvents } from "./navigation/events";
|
||||||
|
import { catalogEntityRegistry } from "./api/catalog-entity-registry";
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class LensApp extends React.Component {
|
export class LensApp extends React.Component {
|
||||||
static async init() {
|
static async init() {
|
||||||
|
catalogEntityRegistry.init();
|
||||||
extensionLoader.loadOnClusterManagerRenderer();
|
extensionLoader.loadOnClusterManagerRenderer();
|
||||||
LensProtocolRouterRenderer.getInstance<LensProtocolRouterRenderer>().init();
|
LensProtocolRouterRenderer.getInstance<LensProtocolRouterRenderer>().init();
|
||||||
bindProtocolAddRouteHandlers();
|
bindProtocolAddRouteHandlers();
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import { clusterViewURL } from "../components/cluster-manager/cluster-view.route
|
|||||||
import { LensProtocolRouterRenderer } from "./router";
|
import { LensProtocolRouterRenderer } from "./router";
|
||||||
import { navigate } from "../navigation/helpers";
|
import { navigate } from "../navigation/helpers";
|
||||||
import { clusterStore } from "../../common/cluster-store";
|
import { clusterStore } from "../../common/cluster-store";
|
||||||
import { workspaceStore } from "../../common/workspace-store";
|
|
||||||
|
|
||||||
export function bindProtocolAddRouteHandlers() {
|
export function bindProtocolAddRouteHandlers() {
|
||||||
LensProtocolRouterRenderer
|
LensProtocolRouterRenderer
|
||||||
@ -21,14 +20,6 @@ export function bindProtocolAddRouteHandlers() {
|
|||||||
.addInternalHandler("/landing", () => {
|
.addInternalHandler("/landing", () => {
|
||||||
navigate(landingURL());
|
navigate(landingURL());
|
||||||
})
|
})
|
||||||
.addInternalHandler("/landing/:workspaceId", ({ pathname: { workspaceId } }) => {
|
|
||||||
if (workspaceStore.getById(workspaceId)) {
|
|
||||||
workspaceStore.setActive(workspaceId);
|
|
||||||
navigate(landingURL());
|
|
||||||
} else {
|
|
||||||
console.log("[APP-HANDLER]: workspace with given ID does not exist", { workspaceId });
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.addInternalHandler("/cluster", () => {
|
.addInternalHandler("/cluster", () => {
|
||||||
navigate(addClusterURL());
|
navigate(addClusterURL());
|
||||||
})
|
})
|
||||||
@ -36,7 +27,6 @@ export function bindProtocolAddRouteHandlers() {
|
|||||||
const cluster = clusterStore.getById(clusterId);
|
const cluster = clusterStore.getById(clusterId);
|
||||||
|
|
||||||
if (cluster) {
|
if (cluster) {
|
||||||
workspaceStore.setActive(cluster.workspace);
|
|
||||||
navigate(clusterViewURL({ params: { clusterId } }));
|
navigate(clusterViewURL({ params: { clusterId } }));
|
||||||
} else {
|
} else {
|
||||||
console.log("[APP-HANDLER]: cluster with given ID does not exist", { clusterId });
|
console.log("[APP-HANDLER]: cluster with given ID does not exist", { clusterId });
|
||||||
@ -46,7 +36,6 @@ export function bindProtocolAddRouteHandlers() {
|
|||||||
const cluster = clusterStore.getById(clusterId);
|
const cluster = clusterStore.getById(clusterId);
|
||||||
|
|
||||||
if (cluster) {
|
if (cluster) {
|
||||||
workspaceStore.setActive(cluster.workspace);
|
|
||||||
navigate(clusterSettingsURL({ params: { clusterId } }));
|
navigate(clusterSettingsURL({ params: { clusterId } }));
|
||||||
} else {
|
} else {
|
||||||
console.log("[APP-HANDLER]: cluster with given ID does not exist", { clusterId });
|
console.log("[APP-HANDLER]: cluster with given ID does not exist", { clusterId });
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user