Catalog & Hotbar - initial groundwork (#2418)

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
Jari Kolehmainen authored and GitHub committed 2021-04-09 09:11:58 +03:00
1 parent e18a041b13
commit 99a464c61d
109 files changed
+1604 -2612

No files matched your search

+3 -60
View File
@@ -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");
});
});
});
+56
View File
@@ -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();
+2
View File
@@ -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());
+65
View File
@@ -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());
+32
View File
@@ -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();
+75
View File
@@ -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>;
}
+6 -37
View File
@@ -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();
+67
View File
@@ -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>();
-345
View File
@@ -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>();