mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Catalog & Hotbar - initial groundwork (#2418)
Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
1 parent
e18a041b13
commit
99a464c61d
109 files changed
+1604
-2612
No files matched your search
@@ -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 = `
|
||||
@@ -77,8 +76,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)
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -92,12 +90,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")).toBeNull();
|
||||
@@ -106,7 +98,6 @@ describe("empty config", () => {
|
||||
it("sets active cluster", () => {
|
||||
clusterStore.setActive("foo");
|
||||
expect(clusterStore.active.id).toBe("foo");
|
||||
expect(workspaceStore.currentWorkspace.lastActiveClusterId).toBe("foo");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -119,8 +110,7 @@ describe("empty config", () => {
|
||||
preferences: {
|
||||
clusterName: "prod"
|
||||
},
|
||||
kubeConfigPath: ClusterStore.embedCustomKubeConfig("prod", kubeconfig),
|
||||
workspace: "workstation"
|
||||
kubeConfigPath: ClusterStore.embedCustomKubeConfig("prod", kubeconfig)
|
||||
}),
|
||||
new Cluster({
|
||||
id: "dev",
|
||||
@@ -128,8 +118,7 @@ describe("empty config", () => {
|
||||
preferences: {
|
||||
clusterName: "dev"
|
||||
},
|
||||
kubeConfigPath: ClusterStore.embedCustomKubeConfig("dev", kubeconfig),
|
||||
workspace: "workstation"
|
||||
kubeConfigPath: ClusterStore.embedCustomKubeConfig("dev", kubeconfig)
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -139,51 +128,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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -486,12 +435,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,189 +0,0 @@
|
||||
import mockFs from "mock-fs";
|
||||
|
||||
jest.mock("electron", () => {
|
||||
return {
|
||||
app: {
|
||||
getVersion: () => "99.99.99",
|
||||
getPath: () => "tmp",
|
||||
getLocale: () => "en",
|
||||
setLoginItemSettings: jest.fn(),
|
||||
},
|
||||
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");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
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);
|
||||
}
|
||||
|
||||
getForGroupKind<T extends CatalogCategory>(group: string, kind: string) {
|
||||
return this.categories.find((c) => c.spec.group === group && c.spec.names.kind === kind) as T;
|
||||
}
|
||||
|
||||
getEntityForData(data: CatalogEntityData) {
|
||||
const category = this.getCategoryForEntity(data);
|
||||
|
||||
if (!category) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const splitApiVersion = data.apiVersion.split("/");
|
||||
const version = splitApiVersion[1];
|
||||
|
||||
const specVersion = category.spec.versions.find((v) => v.name === version);
|
||||
|
||||
if (!specVersion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new specVersion.entityClass(data);
|
||||
}
|
||||
|
||||
getCategoryForEntity<T extends CatalogCategory>(data: CatalogEntityData) {
|
||||
const splitApiVersion = data.apiVersion.split("/");
|
||||
const group = splitApiVersion[0];
|
||||
|
||||
const category = this.categories.find((category) => {
|
||||
return category.spec.group === group && category.spec.names.kind === data.kind;
|
||||
});
|
||||
|
||||
if (!category) return null;
|
||||
|
||||
return category as T;
|
||||
}
|
||||
}
|
||||
|
||||
export const catalogCategoryRegistry = new CatalogCategoryRegistry();
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./kubernetes-cluster";
|
||||
export * from "./web-link";
|
||||
@@ -0,0 +1,105 @@
|
||||
import { EventEmitter } from "events";
|
||||
import { observable } from "mobx";
|
||||
import { catalogCategoryRegistry } from "../catalog-category-registry";
|
||||
import { CatalogCategory, CatalogEntity, CatalogEntityActionContext, CatalogEntityContextMenuContext, CatalogEntityData, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog-entity";
|
||||
import { clusterDisconnectHandler } from "../cluster-ipc";
|
||||
import { clusterStore } from "../cluster-store";
|
||||
import { requestMain } from "../ipc";
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
async onRun(context: CatalogEntityActionContext) {
|
||||
context.navigate(`/cluster/${this.metadata.uid}`);
|
||||
}
|
||||
|
||||
async onDetailsOpen() {
|
||||
//
|
||||
}
|
||||
|
||||
async onContextMenuOpen(context: CatalogEntityContextMenuContext) {
|
||||
context.menuItems = [
|
||||
{
|
||||
icon: "settings",
|
||||
title: "Settings",
|
||||
onClick: async () => context.navigate(`/cluster/${this.metadata.uid}/settings`)
|
||||
},
|
||||
{
|
||||
icon: "delete",
|
||||
title: "Delete",
|
||||
onClick: async () => clusterStore.removeById(this.metadata.uid),
|
||||
confirm: {
|
||||
message: `Remove Kubernetes Cluster "${this.metadata.name} from Lens?`
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
if (this.status.active) {
|
||||
context.menuItems.unshift({
|
||||
icon: "link_off",
|
||||
title: "Disconnect",
|
||||
onClick: async () => {
|
||||
clusterStore.deactivate(this.metadata.uid);
|
||||
requestMain(clusterDisconnectHandler, this.metadata.uid);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const category = catalogCategoryRegistry.getCategoryForEntity<KubernetesClusterCategory>(this);
|
||||
|
||||
if (category) category.emit("contextMenuOpen", this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class KubernetesClusterCategory extends EventEmitter 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"
|
||||
}
|
||||
};
|
||||
|
||||
getId() {
|
||||
return `${this.spec.group}/${this.spec.names.kind}`;
|
||||
}
|
||||
}
|
||||
|
||||
catalogCategoryRegistry.add(new KubernetesClusterCategory());
|
||||
@@ -0,0 +1,65 @@
|
||||
import { observable } from "mobx";
|
||||
import { CatalogCategory, CatalogEntity, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog-entity";
|
||||
import { catalogCategoryRegistry } from "../catalog-category-registry";
|
||||
|
||||
export interface WebLinkStatus extends CatalogEntityStatus {
|
||||
phase: "valid" | "invalid";
|
||||
}
|
||||
|
||||
export type WebLinkSpec = {
|
||||
url: string;
|
||||
};
|
||||
|
||||
export class WebLink implements CatalogEntity {
|
||||
public readonly apiVersion = "entity.k8slens.dev/v1alpha1";
|
||||
public readonly kind = "KubernetesCluster";
|
||||
@observable public metadata: CatalogEntityMetadata;
|
||||
@observable public status: WebLinkStatus;
|
||||
@observable public spec: WebLinkSpec;
|
||||
|
||||
getId() {
|
||||
return this.metadata.uid;
|
||||
}
|
||||
|
||||
getName() {
|
||||
return this.metadata.name;
|
||||
}
|
||||
|
||||
async onRun() {
|
||||
window.open(this.spec.url, "_blank");
|
||||
}
|
||||
|
||||
async onDetailsOpen() {
|
||||
//
|
||||
}
|
||||
|
||||
async onContextMenuOpen() {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
export class WebLinkCategory implements CatalogCategory {
|
||||
public readonly apiVersion = "catalog.k8slens.dev/v1alpha1";
|
||||
public readonly kind = "CatalogCategory";
|
||||
public metadata = {
|
||||
name: "Web Links"
|
||||
};
|
||||
public spec = {
|
||||
group: "entity.k8slens.dev",
|
||||
versions: [
|
||||
{
|
||||
name: "v1alpha1",
|
||||
entityClass: WebLink
|
||||
}
|
||||
],
|
||||
names: {
|
||||
kind: "WebLink"
|
||||
}
|
||||
};
|
||||
|
||||
getId() {
|
||||
return `${this.spec.group}/${this.spec.names.kind}`;
|
||||
}
|
||||
}
|
||||
|
||||
catalogCategoryRegistry.add(new WebLinkCategory());
|
||||
@@ -0,0 +1,32 @@
|
||||
import { action, computed, observable } from "mobx";
|
||||
import { CatalogEntity } from "./catalog-entity";
|
||||
|
||||
export class CatalogEntityRegistry {
|
||||
protected sources = observable.map<string, CatalogEntity[]>([], { 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 items of this.sources.values()) {
|
||||
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();
|
||||
@@ -0,0 +1,75 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
getId: () => 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;
|
||||
setCommandPaletteContext: (context?: CatalogEntity) => void;
|
||||
}
|
||||
|
||||
export type CatalogEntityContextMenu = {
|
||||
icon: string;
|
||||
title: string;
|
||||
onClick: () => Promise<void>;
|
||||
confirm?: {
|
||||
message: string;
|
||||
}
|
||||
};
|
||||
|
||||
export interface CatalogEntityContextMenuContext {
|
||||
navigate: (url: string) => void;
|
||||
menuItems: CatalogEntityContextMenu[];
|
||||
}
|
||||
|
||||
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>;
|
||||
}
|
||||
@@ -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,6 @@ 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,8 +43,12 @@ export interface ClusterModel {
|
||||
/** Path to cluster kubeconfig */
|
||||
kubeConfigPath: string;
|
||||
|
||||
/** Workspace id */
|
||||
workspace?: WorkspaceId;
|
||||
/**
|
||||
* Workspace id
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
workspace?: string;
|
||||
|
||||
/** User context in kubeconfig */
|
||||
contextName?: string;
|
||||
@@ -226,7 +226,6 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
}
|
||||
|
||||
this.activeCluster = clusterId;
|
||||
workspaceStore.setLastActiveClusterId(clusterId);
|
||||
}
|
||||
|
||||
deactivate(id: ClusterId) {
|
||||
@@ -235,22 +234,6 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
swapIconOrders(workspace: WorkspaceId, from: number, to: number) {
|
||||
const clusters = this.getByWorkspaceId(workspace);
|
||||
|
||||
if (from < 0 || to < 0 || from >= clusters.length || to >= clusters.length || isNaN(from) || isNaN(to)) {
|
||||
throw new Error(`invalid from<->to arguments`);
|
||||
}
|
||||
|
||||
move.mutate(clusters, from, to);
|
||||
|
||||
for (const i in clusters) {
|
||||
// This resets the iconOrder to the current display order
|
||||
clusters[i].preferences.iconOrder = +i;
|
||||
}
|
||||
}
|
||||
|
||||
hasClusters() {
|
||||
return this.clusters.size > 0;
|
||||
}
|
||||
@@ -259,13 +242,6 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
return this.clusters.get(id) ?? null;
|
||||
}
|
||||
|
||||
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[] = [];
|
||||
@@ -317,13 +293,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();
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { action, comparer, observable, toJS } from "mobx";
|
||||
import { BaseStore } from "./base-store";
|
||||
import migrations from "../migrations/hotbar-store";
|
||||
|
||||
export interface HotbarItem {
|
||||
entity: {
|
||||
uid: string;
|
||||
};
|
||||
params?: {
|
||||
[key: string]: string;
|
||||
}
|
||||
}
|
||||
|
||||
export interface Hotbar {
|
||||
name: string;
|
||||
items: HotbarItem[];
|
||||
}
|
||||
|
||||
export interface HotbarStoreModel {
|
||||
hotbars: Hotbar[];
|
||||
}
|
||||
|
||||
export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
||||
@observable hotbars: Hotbar[] = [];
|
||||
|
||||
private constructor() {
|
||||
super({
|
||||
configName: "lens-hotbar-store",
|
||||
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
|
||||
syncOptions: {
|
||||
equals: comparer.structural,
|
||||
},
|
||||
migrations,
|
||||
});
|
||||
}
|
||||
|
||||
@action protected async fromStore(data: Partial<HotbarStoreModel> = {}) {
|
||||
this.hotbars = data.hotbars || [{
|
||||
name: "default",
|
||||
items: []
|
||||
}];
|
||||
}
|
||||
|
||||
getByName(name: string) {
|
||||
return this.hotbars.find((hotbar) => hotbar.name === name);
|
||||
}
|
||||
|
||||
add(hotbar: Hotbar) {
|
||||
this.hotbars.push(hotbar);
|
||||
}
|
||||
|
||||
remove(hotbar: Hotbar) {
|
||||
this.hotbars = this.hotbars.filter((h) => h !== hotbar);
|
||||
}
|
||||
|
||||
toJSON(): HotbarStoreModel {
|
||||
const model: HotbarStoreModel = {
|
||||
hotbars: this.hotbars
|
||||
};
|
||||
|
||||
return toJS(model, {
|
||||
recurseEverything: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const hotbarStore = HotbarStore.getInstance<HotbarStore>();
|
||||
@@ -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>();
|
||||
Reference in new issue
Block a user