diff --git a/src/common/__tests__/cluster-store.test.ts b/src/common/__tests__/cluster-store.test.ts index e9ee4ee486..6b634cff76 100644 --- a/src/common/__tests__/cluster-store.test.ts +++ b/src/common/__tests__/cluster-store.test.ts @@ -102,12 +102,6 @@ describe("empty config", () => { await clusterStore.removeById("foo"); expect(clusterStore.getById("foo")).toBeNull(); }); - - it("sets active cluster", () => { - clusterStore.setActive("foo"); - expect(clusterStore.active.id).toBe("foo"); - expect(workspaceStore.currentWorkspace.lastActiveClusterId).toBe("foo"); - }); }); describe("with prod and dev clusters added", () => { diff --git a/src/common/__tests__/workspace-store.test.ts b/src/common/__tests__/workspace-store.test.ts index ae9538ead3..89a5e9f49d 100644 --- a/src/common/__tests__/workspace-store.test.ts +++ b/src/common/__tests__/workspace-store.test.ts @@ -50,6 +50,12 @@ describe("workspace store tests", () => { expect(() => ws.removeWorkspaceById(WorkspaceStore.defaultId)).toThrowError("Cannot remove"); }); + it("has the default workspace as active", () => { + const ws = WorkspaceStore.getInstance(); + + expect(ws.isActive(WorkspaceStore.defaultId)).toBe(true); + }); + it("can update workspace description", () => { const ws = WorkspaceStore.getInstance(); const workspace = ws.addWorkspace(new Workspace({ diff --git a/src/common/__tests__/workspace.test.ts b/src/common/__tests__/workspace.test.ts new file mode 100644 index 0000000000..8b03bd466b --- /dev/null +++ b/src/common/__tests__/workspace.test.ts @@ -0,0 +1,182 @@ +import { Workspace } from "../workspace-store"; +import { clusterStore } from "../cluster-store"; +import { Cluster } from "../../main/cluster"; + +jest.mock("../cluster-store"); + +const mockedClusterStore = clusterStore as jest.Mocked; + +describe("Workspace tests", () => { + it("should be enabled if not managed", () => { + const w = new Workspace({ + id: "f", + name: "f" + }); + + expect(w.enabled).toBe(true); + expect(w.isManaged).toBe(false); + }); + + it("should not be enabled initially if managed", () => { + const w = new Workspace({ + id: "f", + name: "f", + ownerRef: "f" + }); + + expect(w.enabled).toBe(false); + expect(w.isManaged).toBe(true); + }); + + it("should be able to be enabled when managed", () => { + const w = new Workspace({ + id: "f", + name: "f", + ownerRef: "f" + }); + + expect(w.enabled).toBe(false); + expect(w.isManaged).toBe(true); + + w.enabled = true; + expect(w.enabled).toBe(true); + }); + + it("should allow valid clusterId to be set to activeClusterId", () => { + mockedClusterStore.getById.mockImplementationOnce(id => { + expect(id).toBe("foobar"); + + return { + workspace: "f", + id + } as Cluster; + }); + + const w = new Workspace({ + id: "f", + name: "f" + }); + + w.setActiveCluster("foobar"); + expect(w.activeClusterId).toBe("foobar"); + }); + + it("should clear activeClusterId", () => { + mockedClusterStore.getById.mockImplementationOnce(id => { + expect(id).toBe("foobar"); + + return { + workspace: "f", + id + } as Cluster; + }); + + const w = new Workspace({ + id: "f", + name: "f" + }); + + w.setActiveCluster("foobar"); + expect(w.activeClusterId).toBe("foobar"); + + w.clearActiveCluster(); + expect(w.activeClusterId).toBe(undefined); + }); + + it("should disallow valid clusterId to be set to activeClusterId", () => { + mockedClusterStore.getById.mockImplementationOnce(id => { + expect(id).toBe("foobar"); + + return undefined; + }); + + const w = new Workspace({ + id: "f", + name: "f" + }); + + w.setActiveCluster("foobar"); + expect(w.activeClusterId).toBe(undefined); + }); + + describe("Workspace.tryClearAsCurrentActiveCluster", () => { + it("should return false for non-matching ID", () => { + mockedClusterStore.getById.mockImplementationOnce(id => { + expect(id).toBe("foobar"); + + return { + workspace: "f", + id + } as Cluster; + }); + + const w = new Workspace({ + id: "f", + name: "f", + activeClusterId: "foobar" + }); + + expect(w.tryClearAsActiveCluster("fa")).toBe(false); + expect(w.activeClusterId).toBe("foobar"); + }); + it("should return false for non-matching cluster", () => { + mockedClusterStore.getById.mockImplementationOnce(id => { + expect(id).toBe("foobar"); + + return { + workspace: "f", + id + } as Cluster; + }); + + const w = new Workspace({ + id: "f", + name: "f", + activeClusterId: "foobar" + }); + + expect(w.tryClearAsActiveCluster({ id: "fa" } as Cluster)).toBe(false); + expect(w.activeClusterId).toBe("foobar"); + }); + + it("should return true for matching ID", () => { + mockedClusterStore.getById.mockImplementationOnce(id => { + expect(id).toBe("foobar"); + + return { + workspace: "f", + id + } as Cluster; + }); + + const w = new Workspace({ + id: "f", + name: "f", + activeClusterId: "foobar" + }); + + expect(w.tryClearAsActiveCluster("foobar")).toBe(true); + expect(w.activeClusterId).toBe(undefined); + }); + + it("should return true for matching cluster", () => { + mockedClusterStore.getById.mockImplementationOnce(id => { + expect(id).toBe("foobar"); + + return { + workspace: "f", + id + } as Cluster; + }); + + const w = new Workspace({ + id: "f", + name: "f", + activeClusterId: "foobar" + }); + + expect(w.tryClearAsActiveCluster({ id: "foobar"} as Cluster)).toBe(true); + expect(w.activeClusterId).toBe(undefined); + }); + }); +}); diff --git a/src/common/cluster-store.ts b/src/common/cluster-store.ts index 4ac0ad9bdf..ab7872f850 100644 --- a/src/common/cluster-store.ts +++ b/src/common/cluster-store.ts @@ -15,7 +15,6 @@ import { handleRequest, requestMain, subscribeToBroadcast, unsubscribeAllFromBro 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 { clusterId: string; @@ -34,7 +33,6 @@ export type ClusterPrometheusMetadata = { }; export interface ClusterStoreModel { - activeCluster?: ClusterId; // last opened cluster clusters?: ClusterModel[]; } @@ -106,7 +104,6 @@ export class ClusterStore extends BaseStore { return filePath; } - @observable activeCluster: ClusterId; @observable removedClusters = observable.map(); @observable clusters = observable.map(); @@ -189,10 +186,6 @@ export class ClusterStore extends BaseStore { }); } - get activeClusterId() { - return this.activeCluster; - } - @computed get clustersList(): Cluster[] { return Array.from(this.clusters.values()); } @@ -201,40 +194,10 @@ export class ClusterStore extends BaseStore { return this.clustersList.filter((c) => c.enabled); } - @computed get active(): Cluster | null { - return this.getById(this.activeCluster); - } - @computed get connectedClustersList(): Cluster[] { return this.clustersList.filter((c) => !c.disconnected); } - isActive(id: ClusterId) { - return this.activeCluster === id; - } - - isMetricHidden(resource: ResourceType) { - return Boolean(this.active?.preferences.hiddenMetrics?.includes(resource)); - } - - @action - setActive(clusterId: ClusterId) { - const cluster = this.clusters.get(clusterId); - - if (!cluster?.enabled) { - clusterId = null; - } - - this.activeCluster = clusterId; - workspaceStore.setLastActiveClusterId(clusterId); - } - - deactivate(id: ClusterId) { - if (this.isActive(id)) { - this.setActive(null); - } - } - @action swapIconOrders(workspace: WorkspaceId, from: number, to: number) { const clusters = this.getByWorkspaceId(workspace); @@ -268,28 +231,22 @@ export class ClusterStore extends BaseStore { @action addClusters(...models: ClusterModel[]): Cluster[] { - const clusters: Cluster[] = []; - - models.forEach(model => { - clusters.push(this.addCluster(model)); - }); - - return clusters; + return models.map(model => this.addCluster(model)); } @action - addCluster(model: ClusterModel | Cluster): Cluster { + addCluster(clusterOrModel: ClusterModel | Cluster): Cluster { appEventBus.emit({ name: "cluster", action: "add" }); - let cluster = model as Cluster; - if (!(model instanceof Cluster)) { - cluster = new Cluster(model); - } + const cluster = clusterOrModel instanceof Cluster + ? clusterOrModel + : new Cluster(clusterOrModel); if (!cluster.isManaged) { cluster.enabled = true; } - this.clusters.set(model.id, cluster); + + this.clusters.set(cluster.id, cluster); return cluster; } @@ -304,12 +261,9 @@ export class ClusterStore extends BaseStore { const cluster = this.getById(clusterId); if (cluster) { + workspaceStore.getById(cluster.workspace)?.tryClearAsActiveCluster(cluster); this.clusters.delete(clusterId); - if (this.activeCluster === clusterId) { - this.setActive(null); - } - // remove only custom kubeconfigs (pasted as text) if (cluster.kubeConfigPath == ClusterStore.getCustomKubeConfigPath(clusterId)) { unlink(cluster.kubeConfigPath).catch(() => null); @@ -325,7 +279,7 @@ export class ClusterStore extends BaseStore { } @action - protected fromStore({ activeCluster, clusters = [] }: ClusterStoreModel = {}) { + protected fromStore({ clusters = [] }: ClusterStoreModel = {}) { const currentClusters = this.clusters.toJS(); const newClusters = new Map(); const removedClusters = new Map(); @@ -353,14 +307,12 @@ export class ClusterStore extends BaseStore { } }); - this.activeCluster = newClusters.get(activeCluster)?.enabled ? activeCluster : null; this.clusters.replace(newClusters); this.removedClusters.replace(removedClusters); } toJSON(): ClusterStoreModel { return toJS({ - activeCluster: this.activeCluster, clusters: this.clustersList.map(cluster => cluster.toJSON()), }, { recurseEverything: true diff --git a/src/common/workspace-store.ts b/src/common/workspace-store.ts index b037124721..454163d22f 100644 --- a/src/common/workspace-store.ts +++ b/src/common/workspace-store.ts @@ -6,9 +6,14 @@ import { appEventBus } from "./event-bus"; import { broadcastMessage, handleRequest, requestMain } from "../common/ipc"; import logger from "../main/logger"; import type { ClusterId } from "./cluster-store"; +import { Cluster } from "../main/cluster"; +import migrations from "../migrations/workspace-store"; +import { clusterViewURL } from "../renderer/components/cluster-manager/cluster-view.route"; export type WorkspaceId = string; +export class InvariantError extends Error {} + export interface WorkspaceStoreModel { workspaces: WorkspaceModel[]; currentWorkspace?: WorkspaceId; @@ -19,7 +24,7 @@ export interface WorkspaceModel { name: string; description?: string; ownerRef?: string; - lastActiveClusterId?: ClusterId; + activeClusterId?: ClusterId; } export interface WorkspaceState { @@ -61,18 +66,19 @@ export class Workspace implements WorkspaceModel, WorkspaceState { */ @observable ownerRef?: string; + @observable private _enabled = false; + /** - * Last active cluster id - * - * @observable + * The active cluster within this workspace */ - @observable lastActiveClusterId?: ClusterId; + #activeClusterId = observable.box(); + get activeClusterId() { + return this.#activeClusterId.get(); + } - @observable private _enabled: boolean; - - constructor(data: WorkspaceModel) { - Object.assign(this, data); + constructor(model: WorkspaceModel) { + this[updateFromModel](model); if (!ipcRenderer) { reaction(() => this.getState(), () => { @@ -86,9 +92,9 @@ export class Workspace implements WorkspaceModel, WorkspaceState { * * Workspaces that don't have ownerRef will be enabled by default. Workspaces with ownerRef need to explicitly enable a workspace. * - * @observable + * @computed */ - get enabled(): boolean { + @computed get enabled(): boolean { return !this.isManaged || this._enabled; } @@ -98,9 +104,82 @@ export class Workspace implements WorkspaceModel, WorkspaceState { /** * Is workspace managed by an extension + * + * @computed */ - get isManaged(): boolean { - return !!this.ownerRef; + @computed get isManaged(): boolean { + return Boolean(this.ownerRef); + } + + @computed get activeCluster(): Cluster | undefined { + return clusterStore.getById(this.activeClusterId); + } + + /** + * Resolves the clusterId or cluster, checking some invariants + * @param clusterOrId The ID or cluster object to resolve + * @returns A Cluster instance of the specified cluster if it is in this workspace + * @throws if provided a falsey value or if it is an unknown ClusterId or if + * the cluster is not in this workspace. + */ + private resolveClusterOrId(clusterOrId: ClusterId | Cluster): Cluster { + if (!clusterOrId) { + throw new InvariantError("Must provide a Cluster or a ClusterId"); + } + + const cluster = typeof clusterOrId === "string" + ? clusterStore.getById(clusterOrId) + : clusterOrId; + + if (!cluster) { + throw new InvariantError(`ClusterId ${clusterOrId} is invalid`); + } + + if (cluster.workspace !== this.id) { + throw new InvariantError(`Cluster ${cluster.name} is not in Workspace ${this.name}`); + } + + return cluster; + } + + /** + * Sets workspace's active cluster to resolved `clusterOrId`. As long as it + * is valid + * @param clusterOrId the cluster instance or its ID + */ + @action setActiveCluster(clusterOrId?: ClusterId | Cluster) { + try { + if (clusterOrId === undefined) { + this.#activeClusterId.set(undefined); + } else { + this.#activeClusterId.set(this.resolveClusterOrId(clusterOrId).id); + } + } catch (error) { + logger.error("[WORKSPACE]: activeClusterId was attempted to be set to an invalid value", { error, workspaceName: this.name }); + } + } + + /** + * Tries to clear the cluster as this workspace's activeCluster. + * @param clusterOrId the cluster instance or its ID + * @returns true if it matches the `activeClusterId` (and is thus cleared) else false + */ + @action tryClearAsActiveCluster(clusterOrId: ClusterId | Cluster): boolean { + const clusterId = typeof clusterOrId === "string" + ? clusterOrId + : clusterOrId.id; + + const clearActive = this.activeClusterId === clusterId; + + if (clearActive) { + this.clearActiveCluster(); + } + + return clearActive; + } + + @action clearActiveCluster() { + this.#activeClusterId.set(undefined); } /** @@ -129,11 +208,15 @@ export class Workspace implements WorkspaceModel, WorkspaceState { * @param state workspace state */ @action setState(state: WorkspaceState) { - Object.assign(this, state); + this.enabled = state.enabled; } [updateFromModel] = action((model: WorkspaceModel) => { - Object.assign(this, model); + this.id = model.id; + this.name = model.name; + this.description = model.description; + this.ownerRef = model.ownerRef; + this.setActiveCluster(model.activeClusterId); }); toJSON(): WorkspaceModel { @@ -142,7 +225,7 @@ export class Workspace implements WorkspaceModel, WorkspaceState { name: this.name, description: this.description, ownerRef: this.ownerRef, - lastActiveClusterId: this.lastActiveClusterId + activeClusterId: this.activeClusterId, }); } } @@ -152,16 +235,18 @@ export class WorkspaceStore extends BaseStore { private static stateRequestChannel = "workspace:states"; @observable currentWorkspaceId = WorkspaceStore.defaultId; + @observable workspaces = observable.map(); private constructor() { super({ configName: "lens-workspace-store", + migrations }); this.workspaces.set(WorkspaceStore.defaultId, new Workspace({ id: WorkspaceStore.defaultId, - name: "default" + name: "default", })); } @@ -233,6 +318,19 @@ export class WorkspaceStore extends BaseStore { return id === WorkspaceStore.defaultId; } + /** + * Checks if `workspaceOrId` represents `WorkspaceStore.currentWorkspaceId` + * @param workspaceOrId The workspace or its ID + * @returns true if the given workspace is the currently active on + */ + isActive(workspaceOrId: Workspace | WorkspaceId): boolean { + const workspaceId = typeof workspaceOrId === "string" + ? workspaceOrId + : workspaceOrId.id; + + return this.currentWorkspaceId === workspaceId; + } + getById(id: WorkspaceId): Workspace { return this.workspaces.get(id); } @@ -248,9 +346,34 @@ export class WorkspaceStore extends BaseStore { if (!this.getById(id)) { throw new Error(`workspace ${id} doesn't exist`); } + this.currentWorkspaceId = id; } + @action + async setActiveCluster(clusterOrId: ClusterId | Cluster): Promise { + const cluster = typeof clusterOrId === "string" + ? clusterStore.getById(clusterOrId) + : clusterOrId; + + if (!cluster?.enabled) { + throw new Error(`cluster ${(clusterOrId as Cluster)?.id ?? clusterOrId} doesn't exist`); + } + + this.setActive(this.getById(cluster.workspace).id); + + if (ipcRenderer) { + const { navigate } = await import("../renderer/navigation"); + + navigate(clusterViewURL({ params: { clusterId: cluster.id } })); + } else { + const { WindowManager } = await import("../main/window-manager"); + const windowManager = WindowManager.getInstance() as any; + + await windowManager.navigate(clusterViewURL({ params: { clusterId: cluster.id } })); + } + } + @action addWorkspace(workspace: Workspace) { const { id, name } = workspace; @@ -293,14 +416,20 @@ export class WorkspaceStore extends BaseStore { 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; + /** + * Attempts to clear `cluster` as the `activeCluster` from its own workspace + * @returns true if the cluster was previously the active one for its workspace + */ + tryClearAsActiveCluster(cluster: Cluster): boolean { + return this.getById(cluster.workspace).tryClearAsActiveCluster(cluster); } @action diff --git a/src/extensions/stores/cluster-store.ts b/src/extensions/stores/cluster-store.ts index c1a18c453b..b1ff73adac 100644 --- a/src/extensions/stores/cluster-store.ts +++ b/src/extensions/stores/cluster-store.ts @@ -1,4 +1,5 @@ import { clusterStore as internalClusterStore, ClusterId } from "../../common/cluster-store"; +import { workspaceStore as internalWorkspaceStore } from "../../common/workspace-store"; import type { ClusterModel } from "../../common/cluster-store"; import { Cluster } from "../../main/cluster"; import { Singleton } from "../core-api/utils"; @@ -16,16 +17,22 @@ export class ClusterStore extends Singleton { /** * Active cluster id + * + * @deprecated use `workspaceStore.currentWorkspace.activeClusterId` */ get activeClusterId(): string { - return internalClusterStore.activeCluster; + console.warn("get Store.ClusterStore.activeClusterId is deprecated. Use workspace.currentWorkspace.activeClusterId"); + + return internalWorkspaceStore.currentWorkspace.activeClusterId; } /** * Set active cluster id + * @deprecated use `LensExtension.navigate()` */ set activeClusterId(id : ClusterId) { - internalClusterStore.setActive(id); + console.warn("Store.ClusterStore.activeClusterId is deprecated. Use LensExtension.navigate()"); + internalWorkspaceStore.currentWorkspace.setActiveCluster(id); } /** @@ -37,9 +44,11 @@ export class ClusterStore extends Singleton { /** * Get active cluster (a cluster which is currently visible) + * + * @deprecated use `clusterStore.getById(workspaceStore.currentWorkspace.activeClusterId)` */ - get activeCluster(): Cluster | null { - return internalClusterStore.active; + get activeCluster(): Cluster { + return clusterStore.getById(internalWorkspaceStore.currentWorkspace.activeClusterId); } /** diff --git a/src/extensions/stores/workspace-store.ts b/src/extensions/stores/workspace-store.ts index 2ff4a830fd..7e66f617e8 100644 --- a/src/extensions/stores/workspace-store.ts +++ b/src/extensions/stores/workspace-store.ts @@ -1,6 +1,7 @@ import { Singleton } from "../core-api/utils"; import { workspaceStore as internalWorkspaceStore, WorkspaceStore as InternalWorkspaceStore, Workspace, WorkspaceId } from "../../common/workspace-store"; import { ObservableMap } from "mobx"; +import { Cluster, ClusterId } from "../core-api/stores"; export { Workspace } from "../../common/workspace-store"; export type { WorkspaceId, WorkspaceModel } from "../../common/workspace-store"; @@ -113,6 +114,14 @@ export class WorkspaceStore extends Singleton { removeWorkspaceById(id: WorkspaceId) { return internalWorkspaceStore.removeWorkspaceById(id); } + + /** + * Sets the cluster and its workspace as active + * @param clusterOrId the cluster's ID or instance to set as the active cluster + */ + setActiveCluster(clusterOrId: ClusterId | Cluster) { + return internalWorkspaceStore.setActiveCluster(clusterOrId); + } } export const workspaceStore = WorkspaceStore.getInstance(); diff --git a/src/main/cluster.ts b/src/main/cluster.ts index a3d1ad1b5c..d7d69a8c69 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -16,6 +16,7 @@ import logger from "./logger"; import { VersionDetector } from "./cluster-detectors/version-detector"; import { detectorRegistry } from "./cluster-detectors/detector-registry"; import plimit from "p-limit"; +import { ResourceType } from "../renderer/components/+cluster-settings/components/cluster-metrics-setting"; export enum ClusterStatus { AccessGranted = 2, @@ -315,6 +316,10 @@ export class Cluster implements ClusterModel, ClusterState { } } + public isMetricHidden(resource: ResourceType) { + return Boolean(this.preferences.hiddenMetrics?.includes(resource)); + } + /** * @internal */ diff --git a/src/main/index.ts b/src/main/index.ts index 0a946b9c36..fb4bbd42cb 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -106,8 +106,7 @@ app.on("ready", async () => { // preload await Promise.all([ userStore.load(), - clusterStore.load(), - workspaceStore.load(), + clusterStore.load().then(() => workspaceStore.load()), extensionsStore.load(), filesystemProvisionerStore.load(), ]); diff --git a/src/migrations/workspace-store/4.2.0-beta.1.ts b/src/migrations/workspace-store/4.2.0-beta.1.ts new file mode 100644 index 0000000000..92244caf70 --- /dev/null +++ b/src/migrations/workspace-store/4.2.0-beta.1.ts @@ -0,0 +1,28 @@ +import { migration } from "../migration-wrapper"; + +interface Pre420Beta1WorkspaceModel { + id: string; + name: string; + description?: string; + ownerRef?: string; + lastActiveClusterId?: string; +} + +export default migration({ + version: "4.2.0-beta.1", + run(store) { + const oldWorkspaces: Pre420Beta1WorkspaceModel[] = store.get("workspaces") ?? []; + const workspaces = oldWorkspaces.map(({ lastActiveClusterId, ...rest }) => { + if (lastActiveClusterId) { + return { + activeClusterId: lastActiveClusterId, + ...rest, + }; + } + + return rest; + }); + + store.set("workspaces", workspaces); + } +}); diff --git a/src/migrations/workspace-store/index.ts b/src/migrations/workspace-store/index.ts new file mode 100644 index 0000000000..4d50843fe4 --- /dev/null +++ b/src/migrations/workspace-store/index.ts @@ -0,0 +1,5 @@ +import version420Beta1 from "./4.2.0-beta.1"; + +export default { + ...version420Beta1 +}; diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx index 0d412e257c..ef2fc87b44 100644 --- a/src/renderer/bootstrap.tsx +++ b/src/renderer/bootstrap.tsx @@ -56,8 +56,7 @@ export async function bootstrap(App: AppComponent) { // preload common stores await Promise.all([ userStore.load(), - workspaceStore.load(), - clusterStore.load(), + clusterStore.load().then(() => workspaceStore.load()), extensionsStore.load(), filesystemProvisionerStore.load(), themeStore.init(), diff --git a/src/renderer/components/+add-cluster/add-cluster.tsx b/src/renderer/components/+add-cluster/add-cluster.tsx index dc5cdf4d4c..723e129a40 100644 --- a/src/renderer/components/+add-cluster/add-cluster.tsx +++ b/src/renderer/components/+add-cluster/add-cluster.tsx @@ -45,7 +45,7 @@ export class AddCluster extends React.Component { @observable showSettings = false; componentDidMount() { - clusterStore.setActive(null); + workspaceStore.currentWorkspace.clearActiveCluster(); this.setKubeConfig(userStore.kubeConfigPath); appEventBus.emit({ name: "cluster-add", action: "start" }); } @@ -181,13 +181,11 @@ export class AddCluster extends React.Component { }); runInAction(() => { - clusterStore.addClusters(...newClusters); + const [cluster, ...rest] = clusterStore.addClusters(...newClusters); - if (newClusters.length === 1) { - const clusterId = newClusters[0].id; - - clusterStore.setActive(clusterId); - navigate(clusterViewURL({ params: { clusterId } })); + if (rest.length === 0) { + workspaceStore.getById(cluster.workspace).setActiveCluster(cluster); + navigate(clusterViewURL({ params: { clusterId: cluster.id } })); } else { if (newClusters.length > 1) { Notifications.ok( diff --git a/src/renderer/components/+cluster-settings/cluster-settings.command.ts b/src/renderer/components/+cluster-settings/cluster-settings.command.ts index a3b3c8792e..6e4743883b 100644 --- a/src/renderer/components/+cluster-settings/cluster-settings.command.ts +++ b/src/renderer/components/+cluster-settings/cluster-settings.command.ts @@ -1,7 +1,7 @@ import { navigate } from "../../navigation"; import { commandRegistry } from "../../../extensions/registries/command-registry"; import { clusterSettingsURL } from "./cluster-settings.route"; -import { clusterStore } from "../../../common/cluster-store"; +import { getHostedClusterId } from "../../../common/cluster-store"; commandRegistry.add({ id: "cluster.viewCurrentClusterSettings", @@ -9,7 +9,7 @@ commandRegistry.add({ scope: "global", action: () => navigate(clusterSettingsURL({ params: { - clusterId: clusterStore.active.id + clusterId: getHostedClusterId(), } })), isActive: (context) => !!context.cluster diff --git a/src/renderer/components/+cluster-settings/cluster-settings.tsx b/src/renderer/components/+cluster-settings/cluster-settings.tsx index 40472be1ec..eb1a3473f4 100644 --- a/src/renderer/components/+cluster-settings/cluster-settings.tsx +++ b/src/renderer/components/+cluster-settings/cluster-settings.tsx @@ -16,6 +16,7 @@ import { PageLayout } from "../layout/page-layout"; import { requestMain } from "../../../common/ipc"; import { clusterActivateHandler, clusterRefreshHandler } from "../../../common/cluster-ipc"; import { navigation } from "../../navigation"; +import { workspaceStore } from "../../../common/workspace-store"; interface Props extends RouteComponentProps { } @@ -39,7 +40,9 @@ export class ClusterSettings extends React.Component { reaction(() => this.cluster, this.refreshCluster, { fireImmediately: true, }), - reaction(() => this.clusterId, clusterId => clusterStore.setActive(clusterId), { + reaction(() => this.cluster, cluster => { + workspaceStore.getById(cluster.workspace).setActiveCluster(cluster); + }, { fireImmediately: true, }) ]); diff --git a/src/renderer/components/+cluster/cluster-overview.tsx b/src/renderer/components/+cluster/cluster-overview.tsx index eda8ac3090..776ec1dc93 100644 --- a/src/renderer/components/+cluster/cluster-overview.tsx +++ b/src/renderer/components/+cluster/cluster-overview.tsx @@ -5,7 +5,7 @@ import { reaction } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; import { nodesStore } from "../+nodes/nodes.store"; import { podsStore } from "../+workloads-pods/pods.store"; -import { clusterStore, getHostedCluster } from "../../../common/cluster-store"; +import { getHostedCluster } from "../../../common/cluster-store"; import { interval } from "../../utils"; import { TabLayout } from "../layout/tab-layout"; import { Spinner } from "../spinner"; @@ -66,7 +66,7 @@ export class ClusterOverview extends React.Component { render() { const isLoaded = nodesStore.isLoaded && podsStore.isLoaded; - const isMetricsHidden = clusterStore.isMetricHidden(ResourceType.Cluster); + const isMetricsHidden = getHostedCluster().isMetricHidden(ResourceType.Cluster); return ( diff --git a/src/renderer/components/+landing-page/landing-page.tsx b/src/renderer/components/+landing-page/landing-page.tsx index 830c0eb714..ee29f85df8 100644 --- a/src/renderer/components/+landing-page/landing-page.tsx +++ b/src/renderer/components/+landing-page/landing-page.tsx @@ -1,38 +1,58 @@ import "./landing-page.scss"; import React from "react"; -import { computed, observable } from "mobx"; -import { observer } from "mobx-react"; +import { computed, reaction } from "mobx"; +import { disposeOnUnmount, observer } from "mobx-react"; import { clusterStore } from "../../../common/cluster-store"; -import { workspaceStore } from "../../../common/workspace-store"; +import { WorkspaceId, workspaceStore } from "../../../common/workspace-store"; import { WorkspaceOverview } from "./workspace-overview"; import { PageLayout } from "../layout/page-layout"; import { Notifications } from "../notifications"; import { Icon } from "../icon"; +import { createStorage } from "../../utils"; @observer export class LandingPage extends React.Component { - @observable showHint = true; + private static storage = createStorage("seen_workspaces", []); - @computed - get clusters() { - return clusterStore.getByWorkspaceId(workspaceStore.currentWorkspaceId); + @computed get workspace() { + return workspaceStore.currentWorkspace; } componentDidMount() { - const noClustersInScope = !this.clusters.length; - const showStartupHint = this.showHint; + // ignore workspaces that don't exist + const seenWorkspaces = new Set( + LandingPage + .storage + .get() + .filter(id => workspaceStore.getById(id)) + ); - if (showStartupHint && noClustersInScope) { - Notifications.info(<>Welcome!

Get started by associating one or more clusters to Lens

, { - timeout: 30_000, - id: "landing-welcome" - }); - } + disposeOnUnmount(this, [ + reaction(() => this.workspace, workspace => { + const showWelcomeNotification = !( + seenWorkspaces.has(workspace.id) + || workspace.isManaged + || clusterStore.getByWorkspaceId(workspace.id).length + ); + + if (showWelcomeNotification) { + Notifications.info(<>Welcome!

Get started by associating one or more clusters to Lens

, { + timeout: 30_000, + id: "landing-welcome" + }); + } + + seenWorkspaces.add(workspace.id); + LandingPage.storage.set(Array.from(seenWorkspaces)); + }, { + fireImmediately: true, + }), + ]); } render() { - const showBackButton = this.clusters.length > 0; - const header = <>

{workspaceStore.currentWorkspace.name}

; + const showBackButton = Boolean(this.workspace.activeClusterId); + const header = <>

{this.workspace.name}

; return ( diff --git a/src/renderer/components/+network-ingresses/ingress-details.tsx b/src/renderer/components/+network-ingresses/ingress-details.tsx index bfbbfe6f5a..89e7857c05 100644 --- a/src/renderer/components/+network-ingresses/ingress-details.tsx +++ b/src/renderer/components/+network-ingresses/ingress-details.tsx @@ -15,7 +15,7 @@ import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; import { getBackendServiceNamePort } from "../../api/endpoints/ingress.api"; import { ResourceType } from "../+cluster-settings/components/cluster-metrics-setting"; -import { clusterStore } from "../../../common/cluster-store"; +import { getHostedCluster } from "../../../common/cluster-store"; interface Props extends KubeObjectDetailsProps { } @@ -101,6 +101,7 @@ export class IngressDetails extends React.Component { if (!ingress) { return null; } + const { spec, status } = ingress; const ingressPoints = status?.loadBalancer?.ingress; const { metrics } = ingressStore; @@ -108,8 +109,7 @@ export class IngressDetails extends React.Component { "Network", "Duration", ]; - const isMetricHidden = clusterStore.isMetricHidden(ResourceType.Ingress); - + const isMetricHidden = getHostedCluster().isMetricHidden(ResourceType.Ingress); const { serviceName, servicePort } = ingress.getServiceNamePort(); return ( diff --git a/src/renderer/components/+nodes/node-details.tsx b/src/renderer/components/+nodes/node-details.tsx index a565411e7e..4fbd14bbdf 100644 --- a/src/renderer/components/+nodes/node-details.tsx +++ b/src/renderer/components/+nodes/node-details.tsx @@ -18,7 +18,7 @@ import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { KubeEventDetails } from "../+events/kube-event-details"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; import { ResourceType } from "../+cluster-settings/components/cluster-metrics-setting"; -import { clusterStore } from "../../../common/cluster-store"; +import { getHostedCluster } from "../../../common/cluster-store"; interface Props extends KubeObjectDetailsProps { } @@ -54,7 +54,7 @@ export class NodeDetails extends React.Component { "Disk", "Pods", ]; - const isMetricHidden = clusterStore.isMetricHidden(ResourceType.Node); + const isMetricHidden = getHostedCluster().isMetricHidden(ResourceType.Node); return (
diff --git a/src/renderer/components/+storage-volume-claims/volume-claim-details.tsx b/src/renderer/components/+storage-volume-claims/volume-claim-details.tsx index 2aebba52b3..cde660be8f 100644 --- a/src/renderer/components/+storage-volume-claims/volume-claim-details.tsx +++ b/src/renderer/components/+storage-volume-claims/volume-claim-details.tsx @@ -15,7 +15,7 @@ import { getDetailsUrl, KubeObjectDetailsProps, KubeObjectMeta } from "../kube-o import { PersistentVolumeClaim } from "../../api/endpoints"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; import { ResourceType } from "../+cluster-settings/components/cluster-metrics-setting"; -import { clusterStore } from "../../../common/cluster-store"; +import { getHostedCluster } from "../../../common/cluster-store"; interface Props extends KubeObjectDetailsProps { } @@ -43,7 +43,7 @@ export class PersistentVolumeClaimDetails extends React.Component { const metricTabs = [ "Disk" ]; - const isMetricHidden = clusterStore.isMetricHidden(ResourceType.VolumeClaim); + const isMetricHidden = getHostedCluster().isMetricHidden(ResourceType.VolumeClaim); return (
diff --git a/src/renderer/components/+workloads-daemonsets/daemonset-details.tsx b/src/renderer/components/+workloads-daemonsets/daemonset-details.tsx index 9e613c3eff..6711857a8d 100644 --- a/src/renderer/components/+workloads-daemonsets/daemonset-details.tsx +++ b/src/renderer/components/+workloads-daemonsets/daemonset-details.tsx @@ -19,7 +19,7 @@ import { PodDetailsList } from "../+workloads-pods/pod-details-list"; import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; import { ResourceType } from "../+cluster-settings/components/cluster-metrics-setting"; -import { clusterStore } from "../../../common/cluster-store"; +import { getHostedCluster } from "../../../common/cluster-store"; interface Props extends KubeObjectDetailsProps { } @@ -49,7 +49,7 @@ export class DaemonSetDetails extends React.Component { const nodeSelector = daemonSet.getNodeSelectors(); const childPods = daemonSetStore.getChildPods(daemonSet); const metrics = daemonSetStore.metrics; - const isMetricHidden = clusterStore.isMetricHidden(ResourceType.DaemonSet); + const isMetricHidden = getHostedCluster().isMetricHidden(ResourceType.DaemonSet); return (
diff --git a/src/renderer/components/+workloads-deployments/deployment-details.tsx b/src/renderer/components/+workloads-deployments/deployment-details.tsx index 7cd2350a17..68547d95cf 100644 --- a/src/renderer/components/+workloads-deployments/deployment-details.tsx +++ b/src/renderer/components/+workloads-deployments/deployment-details.tsx @@ -20,7 +20,7 @@ import { PodDetailsList } from "../+workloads-pods/pod-details-list"; import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; import { ResourceType } from "../+cluster-settings/components/cluster-metrics-setting"; -import { clusterStore } from "../../../common/cluster-store"; +import { getHostedCluster } from "../../../common/cluster-store"; interface Props extends KubeObjectDetailsProps { } @@ -49,7 +49,7 @@ export class DeploymentDetails extends React.Component { const selectors = deployment.getSelectors(); const childPods = deploymentStore.getChildPods(deployment); const metrics = deploymentStore.metrics; - const isMetricHidden = clusterStore.isMetricHidden(ResourceType.Deployment); + const isMetricHidden = getHostedCluster().isMetricHidden(ResourceType.Deployment); return (
diff --git a/src/renderer/components/+workloads-pods/pod-details-container.tsx b/src/renderer/components/+workloads-pods/pod-details-container.tsx index 99238aa5ad..7de9379f5c 100644 --- a/src/renderer/components/+workloads-pods/pod-details-container.tsx +++ b/src/renderer/components/+workloads-pods/pod-details-container.tsx @@ -12,7 +12,7 @@ import { ResourceMetrics } from "../resource-metrics"; import { IMetrics } from "../../api/endpoints/metrics.api"; import { ContainerCharts } from "./container-charts"; import { ResourceType } from "../+cluster-settings/components/cluster-metrics-setting"; -import { clusterStore } from "../../../common/cluster-store"; +import { getHostedCluster } from "../../../common/cluster-store"; interface Props { pod: Pod; @@ -65,7 +65,7 @@ export class PodDetailsContainer extends React.Component { "Memory", "Filesystem", ]; - const isMetricHidden = clusterStore.isMetricHidden(ResourceType.Container); + const isMetricHidden = getHostedCluster().isMetricHidden(ResourceType.Container); return (
diff --git a/src/renderer/components/+workloads-pods/pod-details.tsx b/src/renderer/components/+workloads-pods/pod-details.tsx index 6736e1e326..2d348a7665 100644 --- a/src/renderer/components/+workloads-pods/pod-details.tsx +++ b/src/renderer/components/+workloads-pods/pod-details.tsx @@ -23,7 +23,7 @@ import { PodCharts, podMetricTabs } from "./pod-charts"; import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; import { ResourceType } from "../+cluster-settings/components/cluster-metrics-setting"; -import { clusterStore } from "../../../common/cluster-store"; +import { getHostedCluster } from "../../../common/cluster-store"; interface Props extends KubeObjectDetailsProps { } @@ -68,7 +68,7 @@ export class PodDetails extends React.Component { const nodeSelector = pod.getNodeSelectors(); const volumes = pod.getVolumes(); const metrics = podsStore.metrics; - const isMetricHidden = clusterStore.isMetricHidden(ResourceType.Pod); + const isMetricHidden = getHostedCluster().isMetricHidden(ResourceType.Pod); return (
diff --git a/src/renderer/components/+workloads-replicasets/replicaset-details.tsx b/src/renderer/components/+workloads-replicasets/replicaset-details.tsx index 4510d0add7..a09599da2d 100644 --- a/src/renderer/components/+workloads-replicasets/replicaset-details.tsx +++ b/src/renderer/components/+workloads-replicasets/replicaset-details.tsx @@ -18,7 +18,7 @@ import { PodDetailsList } from "../+workloads-pods/pod-details-list"; import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; import { ResourceType } from "../+cluster-settings/components/cluster-metrics-setting"; -import { clusterStore } from "../../../common/cluster-store"; +import { getHostedCluster } from "../../../common/cluster-store"; interface Props extends KubeObjectDetailsProps { } @@ -49,7 +49,7 @@ export class ReplicaSetDetails extends React.Component { const nodeSelector = replicaSet.getNodeSelectors(); const images = replicaSet.getImages(); const childPods = replicaSetStore.getChildPods(replicaSet); - const isMetricHidden = clusterStore.isMetricHidden(ResourceType.ReplicaSet); + const isMetricHidden = getHostedCluster().isMetricHidden(ResourceType.ReplicaSet); return (
diff --git a/src/renderer/components/+workloads-statefulsets/statefulset-details.tsx b/src/renderer/components/+workloads-statefulsets/statefulset-details.tsx index 97e83807f9..b312a33f11 100644 --- a/src/renderer/components/+workloads-statefulsets/statefulset-details.tsx +++ b/src/renderer/components/+workloads-statefulsets/statefulset-details.tsx @@ -19,7 +19,7 @@ import { PodDetailsList } from "../+workloads-pods/pod-details-list"; import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; import { ResourceType } from "../+cluster-settings/components/cluster-metrics-setting"; -import { clusterStore } from "../../../common/cluster-store"; +import { getHostedCluster } from "../../../common/cluster-store"; interface Props extends KubeObjectDetailsProps { } @@ -48,7 +48,7 @@ export class StatefulSetDetails extends React.Component { const nodeSelector = statefulSet.getNodeSelectors(); const childPods = statefulSetStore.getChildPods(statefulSet); const metrics = statefulSetStore.metrics; - const isMetricHidden = clusterStore.isMetricHidden(ResourceType.StatefulSet); + const isMetricHidden = getHostedCluster().isMetricHidden(ResourceType.StatefulSet); return (
diff --git a/src/renderer/components/+workspaces/add-workspace.tsx b/src/renderer/components/+workspaces/add-workspace.tsx index 868acc0e98..3c08bfff47 100644 --- a/src/renderer/components/+workspaces/add-workspace.tsx +++ b/src/renderer/components/+workspaces/add-workspace.tsx @@ -7,7 +7,6 @@ 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, @@ -31,7 +30,6 @@ export class AddWorkspace extends React.Component { } workspaceStore.setActive(workspace.id); - clusterStore.setActive(null); navigate(landingURL()); CommandOverlay.close(); } diff --git a/src/renderer/components/+workspaces/workspaces.tsx b/src/renderer/components/+workspaces/workspaces.tsx index 3bea020ef3..74f9558cd8 100644 --- a/src/renderer/components/+workspaces/workspaces.tsx +++ b/src/renderer/components/+workspaces/workspaces.tsx @@ -3,7 +3,7 @@ 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 { Select, SelectOption } from "../select"; import { navigate } from "../../navigation"; import { CommandOverlay } from "../command-palette/command-container"; import { AddWorkspace } from "./add-workspace"; @@ -20,8 +20,8 @@ export class ChooseWorkspace extends React.Component { private static editActionId = "__edit__"; @computed get options() { - const options = workspaceStore.enabledWorkspacesList.map((workspace) => { - return { value: workspace.id, label: workspace.name }; + const options: SelectOption[] = workspaceStore.enabledWorkspacesList.map((workspace) => { + return { value: workspace.id, label: workspace.name, isDisabled: workspaceStore.isActive(workspace) }; }); options.push({ value: ChooseWorkspace.overviewActionId, label: "Show current workspace overview ..." }); @@ -39,42 +39,30 @@ export class ChooseWorkspace extends React.Component { return options; } - onChange(id: string) { - if (id === ChooseWorkspace.overviewActionId) { - navigate(landingURL()); // overview of active workspace. TODO: change name from landing - CommandOverlay.close(); + onChange(idOrAction: string): void { + switch (idOrAction) { + case ChooseWorkspace.overviewActionId: + navigate(landingURL()); // overview of active workspace. TODO: change name from landing - return; + return CommandOverlay.close(); + case ChooseWorkspace.addActionId: + return CommandOverlay.open(); + case ChooseWorkspace.removeActionId: + return CommandOverlay.open(); + case ChooseWorkspace.editActionId: + return CommandOverlay.open(); + default: // assume id + workspaceStore.setActive(idOrAction); + const clusterId = workspaceStore.getById(idOrAction).activeClusterId; + + if (clusterId) { + navigate(clusterViewURL({ params: { clusterId } })); + } else { + navigate(landingURL()); + } + + CommandOverlay.close(); } - - if (id === ChooseWorkspace.addActionId) { - CommandOverlay.open(); - - return; - } - - if (id === ChooseWorkspace.removeActionId) { - CommandOverlay.open(); - - return; - } - - if (id === ChooseWorkspace.editActionId) { - CommandOverlay.open(); - - return; - } - - workspaceStore.setActive(id); - const clusterId = workspaceStore.getById(id).lastActiveClusterId; - - if (clusterId) { - navigate(clusterViewURL({ params: { clusterId } })); - } else { - navigate(landingURL()); - } - - CommandOverlay.close(); } render() { diff --git a/src/renderer/components/cluster-icon/cluster-icon.tsx b/src/renderer/components/cluster-icon/cluster-icon.tsx index 48d23eb91f..0025e137b6 100644 --- a/src/renderer/components/cluster-icon/cluster-icon.tsx +++ b/src/renderer/components/cluster-icon/cluster-icon.tsx @@ -9,7 +9,8 @@ import { cssNames, IClassName } from "../../utils"; import { Badge } from "../badge"; import { Tooltip } from "../tooltip"; import { subscribeToBroadcast } from "../../../common/ipc"; -import { observable } from "mobx"; +import { computed, observable } from "mobx"; +import { workspaceStore } from "../../../common/workspace-store"; interface Props extends DOMAttributes { cluster: Cluster; @@ -18,7 +19,6 @@ interface Props extends DOMAttributes { showErrors?: boolean; showTooltip?: boolean; interactive?: boolean; - isActive?: boolean; options?: HashiconParams; } @@ -33,8 +33,16 @@ export class ClusterIcon extends React.Component { @observable eventCount = 0; - get eventCountBroadcast() { - return `cluster-warning-event-count:${this.props.cluster.id}`; + @computed get eventCountBroadcast() { + const { cluster } = this.props; + + return `cluster-warning-event-count:${cluster.id}`; + } + + @computed get isActive() { + const { cluster } = this.props; + + return workspaceStore.getById(cluster.workspace).activeClusterId === cluster.id; } componentDidMount() { @@ -48,8 +56,9 @@ export class ClusterIcon extends React.Component { } render() { + const { isActive } = this; const { - cluster, showErrors, showTooltip, errorClass, options, interactive, isActive, + cluster, showErrors, showTooltip, errorClass, options, interactive, children, ...elemProps } = this.props; const { name, preferences, id: clusterId, online } = cluster; diff --git a/src/renderer/components/cluster-manager/cluster-actions.tsx b/src/renderer/components/cluster-manager/cluster-actions.tsx index 93bde6d80d..41bef7e385 100644 --- a/src/renderer/components/cluster-manager/cluster-actions.tsx +++ b/src/renderer/components/cluster-manager/cluster-actions.tsx @@ -10,6 +10,7 @@ import { ConfirmDialog } from "../confirm-dialog"; import { Cluster } from "../../../main/cluster"; import { Tooltip } from "../../components//tooltip"; import { IpcRendererNavigationEvents } from "../../navigation/events"; +import { workspaceStore } from "../../../common/workspace-store"; const navigate = (route: string) => broadcastMessage(IpcRendererNavigationEvents.NAVIGATE_IN_APP, route); @@ -24,8 +25,10 @@ export const ClusterActions = (cluster: Cluster) => ({ params: { clusterId: cluster.id } })), disconnect: async () => { - clusterStore.deactivate(cluster.id); - navigate(landingURL()); + if (workspaceStore.tryClearAsActiveCluster(cluster)) { + navigate(landingURL()); + } + await requestMain(clusterDisconnectHandler, cluster.id); }, remove: () => { @@ -38,7 +41,6 @@ export const ClusterActions = (cluster: Cluster) => ({ label: "Remove" }, ok: () => { - clusterStore.deactivate(cluster.id); clusterStore.removeById(cluster.id); navigate(landingURL()); }, diff --git a/src/renderer/components/cluster-manager/cluster-manager.tsx b/src/renderer/components/cluster-manager/cluster-manager.tsx index 1c3306b65f..bedaa74024 100644 --- a/src/renderer/components/cluster-manager/cluster-manager.tsx +++ b/src/renderer/components/cluster-manager/cluster-manager.tsx @@ -17,6 +17,7 @@ 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 { workspaceStore } from "../../../common/workspace-store"; @observer export class ClusterManager extends React.Component { @@ -44,12 +45,12 @@ export class ClusterManager extends React.Component { } get startUrl() { - const { activeClusterId } = clusterStore; + const { currentWorkspace } = workspaceStore; - if (activeClusterId) { + if (currentWorkspace.activeClusterId) { return clusterViewURL({ params: { - clusterId: activeClusterId + clusterId: currentWorkspace.activeClusterId } }); } diff --git a/src/renderer/components/cluster-manager/cluster-view.tsx b/src/renderer/components/cluster-manager/cluster-view.tsx index ddedf145e7..e1b8bb9b5a 100644 --- a/src/renderer/components/cluster-manager/cluster-view.tsx +++ b/src/renderer/components/cluster-manager/cluster-view.tsx @@ -8,6 +8,7 @@ import { ClusterStatus } from "./cluster-status"; import { hasLoadedView } from "./lens-views"; import { Cluster } from "../../../main/cluster"; import { clusterStore } from "../../../common/cluster-store"; +import { workspaceStore } from "../../../common/workspace-store"; interface Props extends RouteComponentProps { } @@ -24,7 +25,9 @@ export class ClusterView extends React.Component { async componentDidMount() { disposeOnUnmount(this, [ - reaction(() => this.clusterId, clusterId => clusterStore.setActive(clusterId), { + reaction(() => this.cluster, cluster => { + workspaceStore.getById(cluster.workspace).setActiveCluster(cluster); + }, { fireImmediately: true, }) ]); diff --git a/src/renderer/components/cluster-manager/clusters-menu.tsx b/src/renderer/components/cluster-manager/clusters-menu.tsx index 03711e41fc..4ec3594019 100644 --- a/src/renderer/components/cluster-manager/clusters-menu.tsx +++ b/src/renderer/components/cluster-manager/clusters-menu.tsx @@ -30,6 +30,10 @@ interface Props { export class ClustersMenu extends React.Component { @observable workspaceMenuVisible = false; + get workspace() { + return workspaceStore.currentWorkspace; + } + showCluster = (clusterId: ClusterId) => { navigate(clusterViewURL({ params: { clusterId } })); }; @@ -77,9 +81,7 @@ export class ClustersMenu extends React.Component { 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; + const clusters = clusterStore.getByWorkspaceId(this.workspace.id).filter(cluster => cluster.enabled); return (
@@ -88,26 +90,21 @@ export class ClustersMenu extends React.Component { {({ innerRef, droppableProps, placeholder }: DroppableProvided) => (
- {clusters.map((cluster, index) => { - const isActive = cluster.id === activeClusterId; - - return ( - - {({ draggableProps, dragHandleProps, innerRef }: DraggableProvided) => ( -
- this.showCluster(cluster.id)} - onContextMenu={() => this.showContextMenu(cluster)} - /> -
- )} -
- ); - })} + {clusters.map((cluster, index) => ( + + {({ draggableProps, dragHandleProps, innerRef }: DraggableProvided) => ( +
+ this.showCluster(cluster.id)} + onContextMenu={() => this.showContextMenu(cluster)} + /> +
+ )} +
+ ))} {placeholder}
)} diff --git a/src/renderer/components/command-palette/command-container.tsx b/src/renderer/components/command-palette/command-container.tsx index c6950bc5b9..301b867ba7 100644 --- a/src/renderer/components/command-palette/command-container.tsx +++ b/src/renderer/components/command-palette/command-container.tsx @@ -8,7 +8,6 @@ import { EventEmitter } from "../../../common/event-emitter"; 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 = { @@ -49,7 +48,7 @@ export class CommandContainer extends React.Component<{ clusterId?: string }> { private runCommand(command: CommandRegistration) { command.action({ - cluster: clusterStore.active, + cluster: workspaceStore.currentWorkspace.activeCluster, workspace: workspaceStore.currentWorkspace }); } diff --git a/src/renderer/components/command-palette/command-dialog.tsx b/src/renderer/components/command-palette/command-dialog.tsx index a8b0965488..c1557300db 100644 --- a/src/renderer/components/command-palette/command-dialog.tsx +++ b/src/renderer/components/command-palette/command-dialog.tsx @@ -4,7 +4,6 @@ import { computed, observable, toJS } from "mobx"; import { observer } from "mobx-react"; import React from "react"; import { commandRegistry } from "../../../extensions/registries/command-registry"; -import { clusterStore } from "../../../common/cluster-store"; import { workspaceStore } from "../../../common/workspace-store"; import { CommandOverlay } from "./command-container"; import { broadcastMessage } from "../../../common/ipc"; @@ -16,30 +15,31 @@ export class CommandDialog extends React.Component { @observable menuIsOpen = true; @computed get options() { - const context = { - cluster: clusterStore.active, - workspace: workspaceStore.currentWorkspace - }; + const activeCluster = workspaceStore.currentWorkspace.activeCluster; - return commandRegistry.getItems().filter((command) => { - if (command.scope === "cluster" && !clusterStore.active) { - return false; - } + return commandRegistry.getItems() + .filter(command => { + if (command.scope === "cluster" && !activeCluster) { + return false; + } - if (!command.isActive) { - return true; - } + if (!command.isActive) { + return true; + } - try { - return command.isActive(context); - } catch(e) { - console.error(e); + try { + return command.isActive({ + cluster: activeCluster, + workspace: workspaceStore.currentWorkspace + }); + } catch(e) { + console.error(e); - return false; - } - }).map((command) => { - return { value: command.id, label: command.title }; - }).sort((a, b) => a.label > b.label ? 1 : -1); + return false; + } + }) + .map(({ id, title }) => ({ value: id, label: title })) + .sort((a, b) => a.label > b.label ? 1 : -1); } private onChange(value: string) { @@ -49,6 +49,7 @@ export class CommandDialog extends React.Component { return; } + const activeCluster = workspaceStore.currentWorkspace.activeCluster; const action = toJS(command.action); try { @@ -56,16 +57,16 @@ export class CommandDialog extends React.Component { if (command.scope === "global") { action({ - cluster: clusterStore.active, + cluster: activeCluster, workspace: workspaceStore.currentWorkspace }); - } else if(clusterStore.active) { + } else if(activeCluster) { navigate(clusterViewURL({ params: { - clusterId: clusterStore.active.id + clusterId: activeCluster.id } })); - broadcastMessage(`command-palette:run-action:${clusterStore.active.id}`, command.id); + broadcastMessage(`command-palette:run-action:${activeCluster.id}`, command.id); } } catch(error) { console.error("[COMMAND-DIALOG] failed to execute command", command.id, error); diff --git a/src/renderer/ipc/index.tsx b/src/renderer/ipc/index.tsx index ccbeef4797..b7d1b9d58d 100644 --- a/src/renderer/ipc/index.tsx +++ b/src/renderer/ipc/index.tsx @@ -8,6 +8,7 @@ import { invalidKubeconfigHandler } from "./invalid-kubeconfig-handler"; import { clusterStore } from "../../common/cluster-store"; import { navigate } from "../navigation"; import { clusterSettingsURL } from "../components/+cluster-settings"; +import logger from "../../main/logger"; function sendToBackchannel(backchannel: string, notificationId: string, data: BackchannelArg): void { notificationsStore.remove(notificationId); @@ -59,6 +60,12 @@ const listNamespacesForbiddenHandlerDisplayedAt = new Map(); const intervalBetweenNotifications = 1000 * 60; // 60s function ListNamespacesForbiddenHandler(event: IpcRendererEvent, ...[clusterId]: ListNamespaceForbiddenArgs): void { + const cluster = clusterStore.getById(clusterId); + + if (!cluster) { + return void logger.warn("[ListNamespacesForbiddenHandler]: received event but was given an unknown cluster ID", { clusterId }); + } + const lastDisplayedAt = listNamespacesForbiddenHandlerDisplayedAt.get(clusterId); const wasDisplayed = Boolean(lastDisplayedAt); const now = Date.now(); @@ -76,7 +83,7 @@ function ListNamespacesForbiddenHandler(event: IpcRendererEvent, ...[clusterId]: (
Add Accessible Namespaces -

Cluster {clusterStore.active.name} does not have permissions to list namespaces. Please add the namespaces you have access to.

+

Cluster {cluster.name} does not have permissions to list namespaces. Please add the namespaces you have access to.