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:main": "yarn run compile:main --watch",
|
||||
"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:main": "yarn run webpack --config webpack.main.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 { Cluster } from "../../main/cluster";
|
||||
import { ClusterStore, getClusterIdFromHost } from "../cluster-store";
|
||||
import { workspaceStore } from "../workspace-store";
|
||||
|
||||
const testDataIcon = fs.readFileSync("test-data/cluster-store-migration-icon.png");
|
||||
const kubeconfig = `
|
||||
@ -76,8 +75,7 @@ describe("empty config", () => {
|
||||
icon: "data:image/jpeg;base64, iVBORw0KGgoAAAANSUhEUgAAA1wAAAKoCAYAAABjkf5",
|
||||
clusterName: "minikube"
|
||||
},
|
||||
kubeConfigPath: ClusterStore.embedCustomKubeConfig("foo", kubeconfig),
|
||||
workspace: workspaceStore.currentWorkspaceId
|
||||
kubeConfigPath: ClusterStore.embedCustomKubeConfig("foo", kubeconfig)
|
||||
})
|
||||
);
|
||||
});
|
||||
@ -91,12 +89,6 @@ describe("empty config", () => {
|
||||
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 () => {
|
||||
await clusterStore.removeById("foo");
|
||||
expect(clusterStore.getById("foo")).toBeUndefined();
|
||||
@ -105,7 +97,6 @@ describe("empty config", () => {
|
||||
it("sets active cluster", () => {
|
||||
clusterStore.setActive("foo");
|
||||
expect(clusterStore.active.id).toBe("foo");
|
||||
expect(workspaceStore.currentWorkspace.lastActiveClusterId).toBe("foo");
|
||||
});
|
||||
});
|
||||
|
||||
@ -118,8 +109,7 @@ describe("empty config", () => {
|
||||
preferences: {
|
||||
clusterName: "prod"
|
||||
},
|
||||
kubeConfigPath: ClusterStore.embedCustomKubeConfig("prod", kubeconfig),
|
||||
workspace: "workstation"
|
||||
kubeConfigPath: ClusterStore.embedCustomKubeConfig("prod", kubeconfig)
|
||||
}),
|
||||
new Cluster({
|
||||
id: "dev",
|
||||
@ -127,8 +117,7 @@ describe("empty config", () => {
|
||||
preferences: {
|
||||
clusterName: "dev"
|
||||
},
|
||||
kubeConfigPath: ClusterStore.embedCustomKubeConfig("dev", kubeconfig),
|
||||
workspace: "workstation"
|
||||
kubeConfigPath: ClusterStore.embedCustomKubeConfig("dev", kubeconfig)
|
||||
})
|
||||
);
|
||||
});
|
||||
@ -138,51 +127,11 @@ describe("empty config", () => {
|
||||
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", () => {
|
||||
const file = ClusterStore.embedCustomKubeConfig("boo", "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(() => {
|
||||
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", () => {
|
||||
|
||||
@ -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 { ipcMain, IpcMainInvokeEvent } from "electron";
|
||||
import { clusterFrameMap } from "./cluster-frames";
|
||||
import { catalogEntityRegistry } from "./catalog-entity-registry";
|
||||
|
||||
export const clusterActivateHandler = "cluster:activate";
|
||||
export const clusterSetFrameIdHandler = "cluster:set-frame-id";
|
||||
@ -17,6 +18,10 @@ if (ipcMain) {
|
||||
const cluster = clusterStore.getById(clusterId);
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
@ -43,6 +48,9 @@ if (ipcMain) {
|
||||
|
||||
if (cluster) {
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { workspaceStore } from "./workspace-store";
|
||||
import path from "path";
|
||||
import { app, ipcRenderer, remote, webFrame } from "electron";
|
||||
import { unlink } from "fs-extra";
|
||||
@ -12,9 +11,7 @@ import { dumpConfigYaml } from "./kube-helpers";
|
||||
import { saveToAppFiles } from "./utils/saveToAppFiles";
|
||||
import { KubeConfig } from "@kubernetes/client-node";
|
||||
import { handleRequest, requestMain, subscribeToBroadcast, unsubscribeAllFromBroadcast } from "./ipc";
|
||||
import _ from "lodash";
|
||||
import move from "array-move";
|
||||
import type { WorkspaceId } from "./workspace-store";
|
||||
import { ResourceType } from "../renderer/components/+cluster-settings/components/cluster-metrics-setting";
|
||||
|
||||
export interface ClusterIconUpload {
|
||||
@ -47,9 +44,6 @@ export interface ClusterModel {
|
||||
/** Path to cluster kubeconfig */
|
||||
kubeConfigPath: string;
|
||||
|
||||
/** Workspace id */
|
||||
workspace?: WorkspaceId;
|
||||
|
||||
/** User context in kubeconfig */
|
||||
contextName?: string;
|
||||
|
||||
@ -110,6 +104,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
@observable removedClusters = observable.map<ClusterId, Cluster>();
|
||||
@observable clusters = observable.map<ClusterId, Cluster>();
|
||||
|
||||
|
||||
private static stateRequestChannel = "cluster:states";
|
||||
|
||||
private constructor() {
|
||||
@ -222,7 +217,6 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
const clusterId = this.clusters.has(id) ? id : null;
|
||||
|
||||
this.activeCluster = clusterId;
|
||||
workspaceStore.setLastActiveClusterId(clusterId);
|
||||
}
|
||||
|
||||
deactivate(id: ClusterId) {
|
||||
@ -232,8 +226,8 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
}
|
||||
|
||||
@action
|
||||
swapIconOrders(workspace: WorkspaceId, from: number, to: number) {
|
||||
const clusters = this.getByWorkspaceId(workspace);
|
||||
swapIconOrders(from: number, to: number) {
|
||||
const clusters = this.enabledClustersList;
|
||||
|
||||
if (from < 0 || to < 0 || from >= clusters.length || to >= clusters.length || isNaN(from) || isNaN(to)) {
|
||||
throw new Error(`invalid from<->to arguments`);
|
||||
@ -255,13 +249,6 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
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
|
||||
addClusters(...models: ClusterModel[]): 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
|
||||
protected fromStore({ activeCluster, clusters = [] }: ClusterStoreModel = {}) {
|
||||
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 { 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 { WindowManager } from "../main/window-manager";
|
||||
import { getExtensionPageUrl } from "./registries/page-registry";
|
||||
import { catalogEntityRegistry } from "../common/catalog-entity-registry";
|
||||
import { CatalogEntity } from "../common/catalog-entity";
|
||||
|
||||
export class LensMainExtension extends LensExtension {
|
||||
appMenus: MenuRegistration[] = [];
|
||||
@ -16,4 +18,12 @@ export class LensMainExtension extends LensExtension {
|
||||
|
||||
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
|
||||
|
||||
import type { Cluster } from "../../main/cluster";
|
||||
import type { Workspace } from "../../common/workspace-store";
|
||||
import { BaseRegistry } from "./base-registry";
|
||||
import { action } from "mobx";
|
||||
import { LensExtension } from "../lens-extension";
|
||||
|
||||
export type CommandContext = {
|
||||
cluster?: Cluster;
|
||||
workspace?: Workspace;
|
||||
};
|
||||
|
||||
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 mockFs from "mock-fs";
|
||||
import { workspaceStore } from "../../common/workspace-store";
|
||||
import { Cluster } from "../cluster";
|
||||
import { ContextHandler } from "../context-handler";
|
||||
import { getFreePort } from "../port";
|
||||
@ -81,8 +80,7 @@ describe("create clusters", () => {
|
||||
c = new Cluster({
|
||||
id: "foo",
|
||||
contextName: "minikube",
|
||||
kubeConfigPath: "minikube-config.yml",
|
||||
workspace: workspaceStore.currentWorkspaceId
|
||||
kubeConfigPath: "minikube-config.yml"
|
||||
});
|
||||
});
|
||||
|
||||
@ -162,8 +160,7 @@ describe("create clusters", () => {
|
||||
}({
|
||||
id: "foo",
|
||||
contextName: "minikube",
|
||||
kubeConfigPath: "minikube-config.yml",
|
||||
workspace: workspaceStore.currentWorkspaceId
|
||||
kubeConfigPath: "minikube-config.yml"
|
||||
});
|
||||
|
||||
await c.init(port);
|
||||
|
||||
@ -26,7 +26,6 @@ jest.mock("winston", () => ({
|
||||
import { KubeconfigManager } from "../kubeconfig-manager";
|
||||
import mockFs from "mock-fs";
|
||||
import { Cluster } from "../cluster";
|
||||
import { workspaceStore } from "../../common/workspace-store";
|
||||
import { ContextHandler } from "../context-handler";
|
||||
import { getFreePort } from "../port";
|
||||
import fse from "fs-extra";
|
||||
@ -77,8 +76,7 @@ describe("kubeconfig manager tests", () => {
|
||||
const cluster = new Cluster({
|
||||
id: "foo",
|
||||
contextName: "minikube",
|
||||
kubeConfigPath: "minikube-config.yml",
|
||||
workspace: workspaceStore.currentWorkspaceId
|
||||
kubeConfigPath: "minikube-config.yml"
|
||||
});
|
||||
const contextHandler = new ContextHandler(cluster);
|
||||
const port = await getFreePort();
|
||||
@ -98,8 +96,7 @@ describe("kubeconfig manager tests", () => {
|
||||
const cluster = new Cluster({
|
||||
id: "foo",
|
||||
contextName: "minikube",
|
||||
kubeConfigPath: "minikube-config.yml",
|
||||
workspace: workspaceStore.currentWorkspaceId
|
||||
kubeConfigPath: "minikube-config.yml"
|
||||
});
|
||||
const contextHandler = new ContextHandler(cluster);
|
||||
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 type http from "http";
|
||||
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 { Cluster } from "./cluster";
|
||||
import logger from "./logger";
|
||||
import { apiKubePrefix } from "../common/vars";
|
||||
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 {
|
||||
@observable.deep catalogSource: CatalogEntity[] = [];
|
||||
|
||||
constructor(public readonly port: number) {
|
||||
super();
|
||||
|
||||
catalogEntityRegistry.addSource("lens:kubernetes-clusters", this.catalogSource);
|
||||
// auto-init clusters
|
||||
reaction(() => clusterStore.enabledClustersList, (clusters) => {
|
||||
clusters.forEach((cluster) => {
|
||||
@ -19,8 +26,14 @@ export class ClusterManager extends Singleton {
|
||||
cluster.init(port);
|
||||
}
|
||||
});
|
||||
|
||||
}, { fireImmediately: true });
|
||||
|
||||
reaction(() => toJS(clusterStore.enabledClustersList, { recurseEverything: true }), () => {
|
||||
this.updateCatalogSource(clusterStore.enabledClustersList);
|
||||
}, { fireImmediately: true });
|
||||
|
||||
|
||||
// auto-stop removed clusters
|
||||
autorun(() => {
|
||||
const removedClusters = Array.from(clusterStore.removedClusters.values());
|
||||
@ -40,6 +53,55 @@ export class ClusterManager extends Singleton {
|
||||
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() {
|
||||
logger.info("[CLUSTER-MANAGER]: network is offline");
|
||||
clusterStore.enabledClustersList.forEach((cluster) => {
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { ipcMain } from "electron";
|
||||
import type { ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences } from "../common/cluster-store";
|
||||
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 { apiKubePrefix } from "../common/vars";
|
||||
import { broadcastMessage, InvalidKubeconfigChannel, ClusterListNamespaceForbiddenChannel } from "../common/ipc";
|
||||
@ -104,12 +103,6 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
* @observable
|
||||
*/
|
||||
@observable contextName: string;
|
||||
/**
|
||||
* Workspace id
|
||||
*
|
||||
* @observable
|
||||
*/
|
||||
@observable workspace: WorkspaceId;
|
||||
/**
|
||||
* Path to kubeconfig
|
||||
*
|
||||
@ -611,7 +604,6 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
id: this.id,
|
||||
contextName: this.contextName,
|
||||
kubeConfigPath: this.kubeConfigPath,
|
||||
workspace: this.workspace,
|
||||
preferences: this.preferences,
|
||||
metadata: this.metadata,
|
||||
ownerRef: this.ownerRef,
|
||||
|
||||
@ -17,7 +17,6 @@ import { registerFileProtocol } from "../common/register-protocol";
|
||||
import logger from "./logger";
|
||||
import { clusterStore } from "../common/cluster-store";
|
||||
import { userStore } from "../common/user-store";
|
||||
import { workspaceStore } from "../common/workspace-store";
|
||||
import { appEventBus } from "../common/event-bus";
|
||||
import { extensionLoader } from "../extensions/extension-loader";
|
||||
import { extensionsStore } from "../extensions/extensions-store";
|
||||
@ -30,6 +29,8 @@ import { getAppVersion, getAppVersionFromProxyServer } from "../common/utils";
|
||||
import { bindBroadcastHandlers } from "../common/ipc";
|
||||
import { startUpdateChecking } from "./app-updater";
|
||||
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);
|
||||
let proxyPort: number;
|
||||
@ -107,7 +108,6 @@ app.on("ready", async () => {
|
||||
await Promise.all([
|
||||
userStore.load(),
|
||||
clusterStore.load(),
|
||||
workspaceStore.load(),
|
||||
extensionsStore.load(),
|
||||
filesystemProvisionerStore.load(),
|
||||
]);
|
||||
@ -164,6 +164,7 @@ app.on("ready", async () => {
|
||||
}
|
||||
|
||||
ipcMain.on(IpcRendererNavigationEvents.LOADED, () => {
|
||||
CatalogPusher.init(catalogEntityRegistry);
|
||||
startUpdateChecking();
|
||||
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 { clusterSettingsURL } from "../renderer/components/+cluster-settings/cluster-settings.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 logger from "./logger";
|
||||
import { exitApp } from "./exit-app";
|
||||
@ -175,6 +176,13 @@ export function buildMenu(windowManager: WindowManager) {
|
||||
const viewMenu: MenuItemConstructorOptions = {
|
||||
label: "View",
|
||||
submenu: [
|
||||
{
|
||||
label: "Catalog",
|
||||
accelerator: "Shift+CmdOrCtrl+C",
|
||||
click() {
|
||||
navigate(landingURL());
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Command Palette...",
|
||||
accelerator: "Shift+CmdOrCtrl+P",
|
||||
|
||||
@ -5,10 +5,7 @@ import { autorun } from "mobx";
|
||||
import { showAbout } from "./menu";
|
||||
import { checkForUpdates } from "./app-updater";
|
||||
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 { clusterViewURL } from "../renderer/components/cluster-manager/cluster-view.route";
|
||||
import logger from "./logger";
|
||||
import { isDevelopment, isWindows } from "../common/vars";
|
||||
import { exitApp } from "./exit-app";
|
||||
@ -74,31 +71,6 @@ function createTrayMenu(windowManager: WindowManager): Menu {
|
||||
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",
|
||||
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 { delay } from "../common/utils";
|
||||
import { isMac, isDevelopment } from "../common/vars";
|
||||
import { workspaceStore } from "../common/workspace-store";
|
||||
import * as LensExtensions from "../extensions/extension-api";
|
||||
import { extensionDiscovery } from "../extensions/extension-discovery";
|
||||
import { extensionLoader } from "../extensions/extension-loader";
|
||||
@ -56,7 +55,6 @@ export async function bootstrap(App: AppComponent) {
|
||||
// preload common stores
|
||||
await Promise.all([
|
||||
userStore.load(),
|
||||
workspaceStore.load(),
|
||||
clusterStore.load(),
|
||||
extensionsStore.load(),
|
||||
filesystemProvisionerStore.load(),
|
||||
@ -65,7 +63,6 @@ export async function bootstrap(App: AppComponent) {
|
||||
|
||||
// Register additional store listeners
|
||||
clusterStore.registerIpcListener();
|
||||
workspaceStore.registerIpcListener();
|
||||
|
||||
// init app's dependencies if any
|
||||
if (App.init) {
|
||||
@ -74,7 +71,6 @@ export async function bootstrap(App: AppComponent) {
|
||||
window.addEventListener("message", (ev: MessageEvent) => {
|
||||
if (ev.data === "teardown") {
|
||||
userStore.unregisterIpcListener();
|
||||
workspaceStore.unregisterIpcListener();
|
||||
clusterStore.unregisterIpcListener();
|
||||
unmountComponentAtNode(rootElem);
|
||||
window.location.href = "about:blank";
|
||||
|
||||
@ -12,7 +12,6 @@ import { Button } from "../button";
|
||||
import { Icon } from "../icon";
|
||||
import { kubeConfigDefaultPath, loadConfig, splitConfig, validateConfig, validateKubeConfig } from "../../../common/kube-helpers";
|
||||
import { ClusterModel, ClusterStore, clusterStore } from "../../../common/cluster-store";
|
||||
import { workspaceStore } from "../../../common/workspace-store";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import { navigate } from "../../navigation";
|
||||
import { userStore } from "../../../common/user-store";
|
||||
@ -171,7 +170,6 @@ export class AddCluster extends React.Component {
|
||||
return {
|
||||
id: clusterId,
|
||||
kubeConfigPath,
|
||||
workspace: workspaceStore.currentWorkspaceId,
|
||||
contextName: kubeConfig.currentContext,
|
||||
preferences: {
|
||||
clusterName: kubeConfig.currentContext,
|
||||
|
||||
@ -9,7 +9,6 @@ import { Removal } from "./removal";
|
||||
import { Status } from "./status";
|
||||
import { General } from "./general";
|
||||
import { Cluster } from "../../../main/cluster";
|
||||
import { ClusterIcon } from "../cluster-icon";
|
||||
import { IClusterSettingsRouteParams } from "./cluster-settings.route";
|
||||
import { clusterStore } from "../../../common/cluster-store";
|
||||
import { PageLayout } from "../layout/page-layout";
|
||||
@ -58,7 +57,6 @@ export class ClusterSettings extends React.Component<Props> {
|
||||
if (!cluster) return null;
|
||||
const header = (
|
||||
<>
|
||||
<ClusterIcon cluster={cluster} showErrors={false} showTooltip={false}/>
|
||||
<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 { Cluster } from "../../../main/cluster";
|
||||
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 { ClusterPrometheusSetting } from "./components/cluster-prometheus-setting";
|
||||
import { ClusterHomeDirSetting } from "./components/cluster-home-dir-setting";
|
||||
@ -19,8 +17,6 @@ export class General extends React.Component<Props> {
|
||||
return <div>
|
||||
<h2>General</h2>
|
||||
<ClusterNameSetting cluster={this.props.cluster} />
|
||||
<ClusterWorkspaceSetting cluster={this.props.cluster} />
|
||||
<ClusterIconSetting cluster={this.props.cluster} />
|
||||
<ClusterProxySetting cluster={this.props.cluster} />
|
||||
<ClusterPrometheusSetting 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%;
|
||||
--height: 100%;
|
||||
text-align: center;
|
||||
|
||||
|
||||
|
||||
.content-wrapper {
|
||||
|
||||
.content {
|
||||
margin: 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 React from "react";
|
||||
import { computed, observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { clusterStore } from "../../../common/cluster-store";
|
||||
import { workspaceStore } from "../../../common/workspace-store";
|
||||
import { WorkspaceOverview } from "./workspace-overview";
|
||||
import { ItemListLayout } from "../item-object-list";
|
||||
import { IReactionDisposer, observable } from "mobx";
|
||||
import { CatalogEntityItem, CatalogEntityStore } from "./catalog-entity.store";
|
||||
import { navigate } from "../../navigation";
|
||||
import { kebabCase } from "lodash";
|
||||
import { PageLayout } from "../layout/page-layout";
|
||||
import { Notifications } from "../notifications";
|
||||
import { Icon } from "../icon";
|
||||
|
||||
enum sortBy {
|
||||
name = "name",
|
||||
status = "status"
|
||||
}
|
||||
|
||||
@observer
|
||||
export class LandingPage extends React.Component {
|
||||
@observable showHint = true;
|
||||
|
||||
@computed
|
||||
get clusters() {
|
||||
return clusterStore.getByWorkspaceId(workspaceStore.currentWorkspaceId);
|
||||
}
|
||||
@observable private catalogEntityStore?: CatalogEntityStore;
|
||||
private disposers: IReactionDisposer[] = [];
|
||||
|
||||
componentDidMount() {
|
||||
const noClustersInScope = !this.clusters.length;
|
||||
const showStartupHint = this.showHint;
|
||||
this.catalogEntityStore = new CatalogEntityStore();
|
||||
this.disposers.push(this.catalogEntityStore.watch());
|
||||
}
|
||||
|
||||
if (showStartupHint && noClustersInScope) {
|
||||
Notifications.info(<><b>Welcome!</b><p>Get started by associating one or more clusters to Lens</p></>, {
|
||||
timeout: 30_000,
|
||||
id: "landing-welcome"
|
||||
});
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.disposers.forEach((d) => d());
|
||||
}
|
||||
|
||||
render() {
|
||||
const showBackButton = this.clusters.length > 0;
|
||||
const header = <><Icon svg="logo-lens" big /> <h2>{workspaceStore.currentWorkspace.name}</h2></>;
|
||||
if (!this.catalogEntityStore) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<PageLayout className="LandingOverview flex" header={header} provideBackButtonNavigation={showBackButton} showOnTop={true}>
|
||||
<WorkspaceOverview />
|
||||
<PageLayout className="LandingPage">
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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 { computed, observable, reaction } from "mobx";
|
||||
import { observable } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { Redirect, Route, Router, Switch } from "react-router";
|
||||
import { history } from "../navigation";
|
||||
@ -36,7 +36,7 @@ import { webFrame } from "electron";
|
||||
import { clusterPageRegistry, getExtensionPageUrl } from "../../extensions/registries/page-registry";
|
||||
import { extensionLoader } from "../../extensions/extension-loader";
|
||||
import { appEventBus } from "../../common/event-bus";
|
||||
import { broadcastMessage, requestMain } from "../../common/ipc";
|
||||
import { requestMain } from "../../common/ipc";
|
||||
import whatInput from "what-input";
|
||||
import { clusterSetFrameIdHandler } from "../../common/cluster-ipc";
|
||||
import { ClusterPageMenuRegistration, clusterPageMenuRegistry } from "../../extensions/registries";
|
||||
@ -86,20 +86,12 @@ export class App extends React.Component {
|
||||
disposeOnUnmount(this, [
|
||||
kubeWatchApi.subscribeStores([podsStore, nodesStore, eventStore], {
|
||||
preload: true,
|
||||
}),
|
||||
|
||||
reaction(() => this.warningsTotal, (count: number) => {
|
||||
broadcastMessage(`cluster-warning-event-count:${getHostedCluster().id}`, count);
|
||||
}),
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
||||
@observable startUrl = isAllowedResource(["events", "nodes", "pods"]) ? clusterURL() : workloadsURL();
|
||||
|
||||
@computed get warningsTotal(): number {
|
||||
return nodesStore.getWarningsCount() + eventStore.getWarningsCount();
|
||||
}
|
||||
|
||||
getTabLayoutRoutes(menuItem: ClusterPageMenuRegistration) {
|
||||
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 { observer } from "mobx-react";
|
||||
import { Icon } from "../icon";
|
||||
import { workspaceStore } from "../../../common/workspace-store";
|
||||
import { StatusBarRegistration, statusBarRegistry } from "../../../extensions/registries";
|
||||
import { CommandOverlay } from "../command-palette/command-container";
|
||||
import { ChooseWorkspace } from "../+workspaces";
|
||||
|
||||
@observer
|
||||
export class BottomBar extends React.Component {
|
||||
@ -45,14 +41,8 @@ export class BottomBar extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { currentWorkspace } = workspaceStore;
|
||||
|
||||
return (
|
||||
<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()}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -4,19 +4,19 @@ import React from "react";
|
||||
import { Redirect, Route, Switch } from "react-router";
|
||||
import { comparer, reaction } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { ClustersMenu } from "./clusters-menu";
|
||||
import { BottomBar } from "./bottom-bar";
|
||||
import { LandingPage, landingRoute, landingURL } from "../+landing-page";
|
||||
import { Preferences, preferencesRoute } from "../+preferences";
|
||||
import { AddCluster, addClusterRoute } from "../+add-cluster";
|
||||
import { ClusterView } from "./cluster-view";
|
||||
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 { hasLoadedView, initView, lensViews, refreshViews } from "./lens-views";
|
||||
import { globalPageRegistry } from "../../../extensions/registries/page-registry";
|
||||
import { Extensions, extensionsRoute } from "../+extensions";
|
||||
import { getMatchedClusterId } from "../../navigation";
|
||||
import { HotbarMenu } from "../hotbar/hotbar-menu";
|
||||
|
||||
@observer
|
||||
export class ClusterManager extends React.Component {
|
||||
@ -44,16 +44,6 @@ export class ClusterManager extends React.Component {
|
||||
}
|
||||
|
||||
get startUrl() {
|
||||
const { activeClusterId } = clusterStore;
|
||||
|
||||
if (activeClusterId) {
|
||||
return clusterViewURL({
|
||||
params: {
|
||||
clusterId: activeClusterId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return landingURL();
|
||||
}
|
||||
|
||||
@ -75,7 +65,7 @@ export class ClusterManager extends React.Component {
|
||||
<Redirect exact to={this.startUrl}/>
|
||||
</Switch>
|
||||
</main>
|
||||
<ClustersMenu/>
|
||||
<HotbarMenu/>
|
||||
<BottomBar/>
|
||||
</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 { CommandRegistration, commandRegistry } from "../../../extensions/registries/command-registry";
|
||||
import { clusterStore } from "../../../common/cluster-store";
|
||||
import { workspaceStore } from "../../../common/workspace-store";
|
||||
|
||||
export type CommandDialogEvent = {
|
||||
component: React.ReactElement
|
||||
@ -49,8 +48,7 @@ export class CommandContainer extends React.Component<{ clusterId?: string }> {
|
||||
|
||||
private runCommand(command: CommandRegistration) {
|
||||
command.action({
|
||||
cluster: clusterStore.active,
|
||||
workspace: workspaceStore.currentWorkspace
|
||||
cluster: clusterStore.active
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -5,7 +5,6 @@ import { observer } from "mobx-react";
|
||||
import React from "react";
|
||||
import { commandRegistry } from "../../../extensions/registries/command-registry";
|
||||
import { clusterStore } from "../../../common/cluster-store";
|
||||
import { workspaceStore } from "../../../common/workspace-store";
|
||||
import { CommandOverlay } from "./command-container";
|
||||
import { broadcastMessage } from "../../../common/ipc";
|
||||
import { navigate } from "../../navigation";
|
||||
@ -17,8 +16,7 @@ export class CommandDialog extends React.Component {
|
||||
|
||||
@computed get options() {
|
||||
const context = {
|
||||
cluster: clusterStore.active,
|
||||
workspace: workspaceStore.currentWorkspace
|
||||
cluster: clusterStore.active
|
||||
};
|
||||
|
||||
return commandRegistry.getItems().filter((command) => {
|
||||
@ -56,8 +54,7 @@ export class CommandDialog extends React.Component {
|
||||
|
||||
if (command.scope === "global") {
|
||||
action({
|
||||
cluster: clusterStore.active,
|
||||
workspace: workspaceStore.currentWorkspace
|
||||
cluster: clusterStore.active
|
||||
});
|
||||
} else if(clusterStore.active) {
|
||||
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 { Cluster } from "../../../../main/cluster";
|
||||
import { workspaceStore } from "../../../../common/workspace-store";
|
||||
import { broadcastMessage, requestMain } from "../../../../common/ipc";
|
||||
import { clusterDisconnectHandler } from "../../../../common/cluster-ipc";
|
||||
import { ConfirmDialog } from "../../confirm-dialog";
|
||||
@ -19,7 +18,6 @@ const cluster: Cluster = new Cluster({
|
||||
id: "foo",
|
||||
contextName: "minikube",
|
||||
kubeConfigPath: "minikube-config.yml",
|
||||
workspace: workspaceStore.currentWorkspaceId,
|
||||
});
|
||||
|
||||
describe("<MainLayoutHeader />", () => {
|
||||
|
||||
@ -9,7 +9,7 @@ import { NavigationTree, RecursiveTreeView } from "../tree-view";
|
||||
|
||||
export interface PageLayoutProps extends React.DOMAttributes<any> {
|
||||
className?: IClassName;
|
||||
header: React.ReactNode;
|
||||
header?: React.ReactNode;
|
||||
headerClass?: IClassName;
|
||||
contentClass?: IClassName;
|
||||
provideBackButtonNavigation?: boolean;
|
||||
@ -65,7 +65,7 @@ export class PageLayout extends React.Component<PageLayoutProps> {
|
||||
|
||||
return (
|
||||
<div {...elemProps} className={className}>
|
||||
<div className={cssNames("header flex gaps align-center", headerClass)}>
|
||||
{ header && (<div className={cssNames("header flex gaps align-center", headerClass)}>
|
||||
{header}
|
||||
{provideBackButtonNavigation && (
|
||||
<Icon
|
||||
@ -75,6 +75,10 @@ export class PageLayout extends React.Component<PageLayoutProps> {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{ !header && (
|
||||
<div className="flex gaps"></div>
|
||||
)}
|
||||
{ navigation && (
|
||||
<nav className="content-navigation">
|
||||
<RecursiveTreeView data={navigation}/>
|
||||
|
||||
@ -16,10 +16,12 @@ import { LensProtocolRouterRenderer, bindProtocolAddRouteHandlers } from "./prot
|
||||
import { registerIpcHandlers } from "./ipc";
|
||||
import { ipcRenderer } from "electron";
|
||||
import { IpcRendererNavigationEvents } from "./navigation/events";
|
||||
import { catalogEntityRegistry } from "./api/catalog-entity-registry";
|
||||
|
||||
@observer
|
||||
export class LensApp extends React.Component {
|
||||
static async init() {
|
||||
catalogEntityRegistry.init();
|
||||
extensionLoader.loadOnClusterManagerRenderer();
|
||||
LensProtocolRouterRenderer.getInstance<LensProtocolRouterRenderer>().init();
|
||||
bindProtocolAddRouteHandlers();
|
||||
|
||||
@ -7,7 +7,6 @@ import { clusterViewURL } from "../components/cluster-manager/cluster-view.route
|
||||
import { LensProtocolRouterRenderer } from "./router";
|
||||
import { navigate } from "../navigation/helpers";
|
||||
import { clusterStore } from "../../common/cluster-store";
|
||||
import { workspaceStore } from "../../common/workspace-store";
|
||||
|
||||
export function bindProtocolAddRouteHandlers() {
|
||||
LensProtocolRouterRenderer
|
||||
@ -21,14 +20,6 @@ export function bindProtocolAddRouteHandlers() {
|
||||
.addInternalHandler("/landing", () => {
|
||||
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", () => {
|
||||
navigate(addClusterURL());
|
||||
})
|
||||
@ -36,7 +27,6 @@ export function bindProtocolAddRouteHandlers() {
|
||||
const cluster = clusterStore.getById(clusterId);
|
||||
|
||||
if (cluster) {
|
||||
workspaceStore.setActive(cluster.workspace);
|
||||
navigate(clusterViewURL({ params: { clusterId } }));
|
||||
} else {
|
||||
console.log("[APP-HANDLER]: cluster with given ID does not exist", { clusterId });
|
||||
@ -46,7 +36,6 @@ export function bindProtocolAddRouteHandlers() {
|
||||
const cluster = clusterStore.getById(clusterId);
|
||||
|
||||
if (cluster) {
|
||||
workspaceStore.setActive(cluster.workspace);
|
||||
navigate(clusterSettingsURL({ params: { clusterId } }));
|
||||
} else {
|
||||
console.log("[APP-HANDLER]: cluster with given ID does not exist", { clusterId });
|
||||
|
||||
Loading…
Reference in New Issue
Block a user