1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Fix being able to view clusters outside the current workspace

- Completely removes ClusterStore.activeCluster

- Every workspace now tracks it current activeCluster

- If an active cluster is removed then the workspace's activeClusterId
  is set to undefined

- Only show welcome notification on the first time a non-managed
  workspace is viewed in the workspace overview

- Add unit tests for the WorkspaceStore

- Add validation that only valid clusters can be set to the
  activeClusterId field

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2021-03-17 15:42:57 -04:00
parent ca39379b3a
commit 046d60ca71
34 changed files with 569 additions and 231 deletions

View File

@ -101,12 +101,6 @@ describe("empty config", () => {
await clusterStore.removeById("foo"); await clusterStore.removeById("foo");
expect(clusterStore.getById("foo")).toBeUndefined(); expect(clusterStore.getById("foo")).toBeUndefined();
}); });
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", () => { describe("with prod and dev clusters added", () => {

View File

@ -49,6 +49,26 @@ describe("workspace store tests", () => {
expect(() => ws.removeWorkspaceById(WorkspaceStore.defaultId)).toThrowError("Cannot remove"); expect(() => ws.removeWorkspaceById(WorkspaceStore.defaultId)).toThrowError("Cannot remove");
}); });
it("should not have the default workspace seen", () => {
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
expect(ws.hasBeenSeen(WorkspaceStore.defaultId)).toBe(false);
});
it("can mark only default workspace seen", () => {
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
ws.markSeen(WorkspaceStore.defaultId);
expect(ws.hasBeenSeen(WorkspaceStore.defaultId)).toBe(true);
expect(ws.hasBeenSeen("foobar")).toBe(false);
});
it("has the default workspace as active", () => {
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
expect(ws.isActive(WorkspaceStore.defaultId)).toBe(true);
});
it("can update workspace description", () => { it("can update workspace description", () => {
const ws = WorkspaceStore.getInstance<WorkspaceStore>(); const ws = WorkspaceStore.getInstance<WorkspaceStore>();
const workspace = ws.addWorkspace(new Workspace({ const workspace = ws.addWorkspace(new Workspace({

View File

@ -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<typeof clusterStore>;
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.tryClearAsCurrentActiveCluster("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.tryClearAsCurrentActiveCluster({ 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.tryClearAsCurrentActiveCluster("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.tryClearAsCurrentActiveCluster({ id: "foobar"} as Cluster)).toBe(true);
expect(w.activeClusterId).toBe(undefined);
});
});
});

View File

@ -15,7 +15,6 @@ import { handleRequest, requestMain, subscribeToBroadcast, unsubscribeAllFromBro
import _ from "lodash"; import _ from "lodash";
import move from "array-move"; import move from "array-move";
import type { WorkspaceId } from "./workspace-store"; import type { WorkspaceId } from "./workspace-store";
import { ResourceType } from "../renderer/components/+cluster-settings/components/cluster-metrics-setting";
export interface ClusterIconUpload { export interface ClusterIconUpload {
clusterId: string; clusterId: string;
@ -34,7 +33,6 @@ export type ClusterPrometheusMetadata = {
}; };
export interface ClusterStoreModel { export interface ClusterStoreModel {
activeCluster?: ClusterId; // last opened cluster
clusters?: ClusterModel[]; clusters?: ClusterModel[];
} }
@ -106,7 +104,6 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
return filePath; return filePath;
} }
@observable activeCluster: ClusterId;
@observable removedClusters = observable.map<ClusterId, Cluster>(); @observable removedClusters = observable.map<ClusterId, Cluster>();
@observable clusters = observable.map<ClusterId, Cluster>(); @observable clusters = observable.map<ClusterId, Cluster>();
@ -189,10 +186,6 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
}); });
} }
get activeClusterId() {
return this.activeCluster;
}
@computed get clustersList(): Cluster[] { @computed get clustersList(): Cluster[] {
return Array.from(this.clusters.values()); return Array.from(this.clusters.values());
} }
@ -201,30 +194,10 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
return this.clustersList.filter((c) => c.enabled); return this.clustersList.filter((c) => c.enabled);
} }
@computed get active(): Cluster | null {
return this.getById(this.activeCluster);
}
@computed get connectedClustersList(): Cluster[] { @computed get connectedClustersList(): Cluster[] {
return this.clustersList.filter((c) => !c.disconnected); 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(id: ClusterId) {
const clusterId = this.clusters.has(id) ? id : null;
this.activeCluster = clusterId;
workspaceStore.setLastActiveClusterId(clusterId);
}
@action @action
swapIconOrders(workspace: WorkspaceId, from: number, to: number) { swapIconOrders(workspace: WorkspaceId, from: number, to: number) {
const clusters = this.getByWorkspaceId(workspace); const clusters = this.getByWorkspaceId(workspace);
@ -258,28 +231,22 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
@action @action
addClusters(...models: ClusterModel[]): Cluster[] { addClusters(...models: ClusterModel[]): Cluster[] {
const clusters: Cluster[] = []; return models.map(model => this.addCluster(model));
models.forEach(model => {
clusters.push(this.addCluster(model));
});
return clusters;
} }
@action @action
addCluster(model: ClusterModel | Cluster): Cluster { addCluster(clusterOrModel: ClusterModel | Cluster): Cluster {
appEventBus.emit({ name: "cluster", action: "add" }); appEventBus.emit({ name: "cluster", action: "add" });
let cluster = model as Cluster;
if (!(model instanceof Cluster)) { const cluster = clusterOrModel instanceof Cluster
cluster = new Cluster(model); ? clusterOrModel
} : new Cluster(clusterOrModel);
if (!cluster.isManaged) { if (!cluster.isManaged) {
cluster.enabled = true; cluster.enabled = true;
} }
this.clusters.set(model.id, cluster);
this.clusters.set(cluster.id, cluster);
return cluster; return cluster;
} }
@ -294,12 +261,9 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
const cluster = this.getById(clusterId); const cluster = this.getById(clusterId);
if (cluster) { if (cluster) {
workspaceStore.getById(cluster.workspace)?.tryClearAsCurrentActiveCluster(cluster);
this.clusters.delete(clusterId); this.clusters.delete(clusterId);
if (this.activeCluster === clusterId) {
this.setActive(null);
}
// remove only custom kubeconfigs (pasted as text) // remove only custom kubeconfigs (pasted as text)
if (cluster.kubeConfigPath == ClusterStore.getCustomKubeConfigPath(clusterId)) { if (cluster.kubeConfigPath == ClusterStore.getCustomKubeConfigPath(clusterId)) {
unlink(cluster.kubeConfigPath).catch(() => null); unlink(cluster.kubeConfigPath).catch(() => null);
@ -315,7 +279,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
} }
@action @action
protected fromStore({ activeCluster, clusters = [] }: ClusterStoreModel = {}) { protected fromStore({ clusters = [] }: ClusterStoreModel = {}) {
const currentClusters = this.clusters.toJS(); const currentClusters = this.clusters.toJS();
const newClusters = new Map<ClusterId, Cluster>(); const newClusters = new Map<ClusterId, Cluster>();
const removedClusters = new Map<ClusterId, Cluster>(); const removedClusters = new Map<ClusterId, Cluster>();
@ -343,14 +307,12 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
} }
}); });
this.activeCluster = newClusters.get(activeCluster)?.enabled ? activeCluster : null;
this.clusters.replace(newClusters); this.clusters.replace(newClusters);
this.removedClusters.replace(removedClusters); this.removedClusters.replace(removedClusters);
} }
toJSON(): ClusterStoreModel { toJSON(): ClusterStoreModel {
return toJS({ return toJS({
activeCluster: this.activeCluster,
clusters: this.clustersList.map(cluster => cluster.toJSON()), clusters: this.clustersList.map(cluster => cluster.toJSON()),
}, { }, {
recurseEverything: true recurseEverything: true

View File

@ -6,12 +6,17 @@ import { appEventBus } from "./event-bus";
import { broadcastMessage, handleRequest, requestMain } from "../common/ipc"; import { broadcastMessage, handleRequest, requestMain } from "../common/ipc";
import logger from "../main/logger"; import logger from "../main/logger";
import type { ClusterId } from "./cluster-store"; import type { ClusterId } from "./cluster-store";
import { Cluster } from "../main/cluster";
import migrations from "../migrations/workspace-store";
export type WorkspaceId = string; export type WorkspaceId = string;
export class InvarientError extends Error {}
export interface WorkspaceStoreModel { export interface WorkspaceStoreModel {
workspaces: WorkspaceModel[]; workspaces: WorkspaceModel[];
currentWorkspace?: WorkspaceId; currentWorkspace?: WorkspaceId;
seenWorkspaces?: WorkspaceId[];
} }
export interface WorkspaceModel { export interface WorkspaceModel {
@ -19,7 +24,7 @@ export interface WorkspaceModel {
name: string; name: string;
description?: string; description?: string;
ownerRef?: string; ownerRef?: string;
lastActiveClusterId?: ClusterId; activeClusterId?: ClusterId;
} }
export interface WorkspaceState { export interface WorkspaceState {
@ -61,18 +66,19 @@ export class Workspace implements WorkspaceModel, WorkspaceState {
*/ */
@observable ownerRef?: string; @observable ownerRef?: string;
@observable private _enabled = false;
/** /**
* Last active cluster id * The active cluster within this workspace
*
* @observable
*/ */
@observable lastActiveClusterId?: ClusterId; #activeClusterId = observable.box<ClusterId | undefined>();
get activeClusterId() {
return this.#activeClusterId.get();
}
@observable private _enabled: boolean; constructor(model: WorkspaceModel) {
this[updateFromModel](model);
constructor(data: WorkspaceModel) {
Object.assign(this, data);
if (!ipcRenderer) { if (!ipcRenderer) {
reaction(() => this.getState(), () => { 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. * 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; return !this.isManaged || this._enabled;
} }
@ -98,9 +104,88 @@ export class Workspace implements WorkspaceModel, WorkspaceState {
/** /**
* Is workspace managed by an extension * Is workspace managed by an extension
*
* @computed
*/ */
get isManaged(): boolean { @computed get isManaged(): boolean {
return !!this.ownerRef; 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 InvarientError("Must provide a Cluster or a ClusterId");
}
const cluster = typeof clusterOrId === "string"
? clusterStore.getById(clusterOrId)
: clusterOrId;
if (!cluster) {
throw new InvarientError(`ClusterId ${clusterOrId} is invalid`);
}
if (cluster.workspace !== this.id) {
throw new InvarientError(`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", { clusterOrId, 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 tryClearAsCurrentActiveCluster(clusterOrId: ClusterId | Cluster): boolean {
if (typeof clusterOrId === "string") {
if (this.activeClusterId === clusterOrId) {
this.clearActiveCluster();
return true;
}
return false;
}
if (this.activeClusterId === clusterOrId.id) {
this.clearActiveCluster();
return true;
}
return false;
}
@action clearActiveCluster() {
this.#activeClusterId.set(undefined);
} }
/** /**
@ -129,11 +214,15 @@ export class Workspace implements WorkspaceModel, WorkspaceState {
* @param state workspace state * @param state workspace state
*/ */
@action setState(state: WorkspaceState) { @action setState(state: WorkspaceState) {
Object.assign(this, state); this.enabled = state.enabled;
} }
[updateFromModel] = action((model: WorkspaceModel) => { [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 { toJSON(): WorkspaceModel {
@ -142,7 +231,7 @@ export class Workspace implements WorkspaceModel, WorkspaceState {
name: this.name, name: this.name,
description: this.description, description: this.description,
ownerRef: this.ownerRef, ownerRef: this.ownerRef,
lastActiveClusterId: this.lastActiveClusterId activeClusterId: this.activeClusterId,
}); });
} }
} }
@ -152,16 +241,24 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
private static stateRequestChannel = "workspace:states"; private static stateRequestChannel = "workspace:states";
@observable currentWorkspaceId = WorkspaceStore.defaultId; @observable currentWorkspaceId = WorkspaceStore.defaultId;
#seenWorkspaces = observable.set<WorkspaceId>();
get seenWorkspaces(): WorkspaceId[] {
return Array.from(this.#seenWorkspaces.values());
}
@observable workspaces = observable.map<WorkspaceId, Workspace>(); @observable workspaces = observable.map<WorkspaceId, Workspace>();
private constructor() { private constructor() {
super({ super({
configName: "lens-workspace-store", configName: "lens-workspace-store",
migrations
}); });
this.workspaces.set(WorkspaceStore.defaultId, new Workspace({ this.workspaces.set(WorkspaceStore.defaultId, new Workspace({
id: WorkspaceStore.defaultId, id: WorkspaceStore.defaultId,
name: "default" name: "default",
})); }));
} }
@ -233,6 +330,44 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
return id === WorkspaceStore.defaultId; 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;
}
/**
* Checks to see if the workspace has been to the overview page before
* @param workspaceOrId The workspace or its ID
* @returns true if the given workspace has been to the overview page before
*/
hasBeenSeen(workspaceOrId: Workspace | WorkspaceId): boolean {
const workspaceId = typeof workspaceOrId === "string"
? workspaceOrId
: workspaceOrId.id;
return this.#seenWorkspaces.has(workspaceId);
}
/**
* Marks the given workspace as having visited the overview page
* @param workspaceOrId The workspace or its ID
*/
markSeen(workspaceOrId: Workspace | WorkspaceId): void {
const workspaceId = typeof workspaceOrId === "string"
? workspaceOrId
: workspaceOrId.id;
this.#seenWorkspaces.add(workspaceId);
}
getById(id: WorkspaceId): Workspace { getById(id: WorkspaceId): Workspace {
return this.workspaces.get(id); return this.workspaces.get(id);
} }
@ -293,18 +428,23 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
if (this.currentWorkspaceId === id) { if (this.currentWorkspaceId === id) {
this.currentWorkspaceId = WorkspaceStore.defaultId; // reset to default this.currentWorkspaceId = WorkspaceStore.defaultId; // reset to default
} }
this.workspaces.delete(id); this.workspaces.delete(id);
appEventBus.emit({name: "workspace", action: "remove"}); appEventBus.emit({name: "workspace", action: "remove"});
clusterStore.removeByWorkspaceId(id); clusterStore.removeByWorkspaceId(id);
} }
@action @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
*/
tryClearAsWorkspaceActiveCluster(cluster: Cluster): boolean {
return this.getById(cluster.workspace).tryClearAsCurrentActiveCluster(cluster);
} }
@action @action
protected fromStore({ currentWorkspace, workspaces = [] }: WorkspaceStoreModel) { protected fromStore({ currentWorkspace, workspaces = [], seenWorkspaces = [] }: WorkspaceStoreModel) {
if (currentWorkspace) { if (currentWorkspace) {
this.currentWorkspaceId = currentWorkspace; this.currentWorkspaceId = currentWorkspace;
} }
@ -330,12 +470,15 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
this.workspaces.delete(workspaceId); this.workspaces.delete(workspaceId);
} }
} }
this.#seenWorkspaces.replace(seenWorkspaces);
} }
toJSON(): WorkspaceStoreModel { toJSON(): WorkspaceStoreModel {
return toJS({ return toJS({
currentWorkspace: this.currentWorkspaceId, currentWorkspace: this.currentWorkspaceId,
workspaces: this.workspacesList.map((w) => w.toJSON()), workspaces: this.workspacesList.map((w) => w.toJSON()),
seenWorkspaces: this.seenWorkspaces,
}, { }, {
recurseEverything: true recurseEverything: true
}); });

View File

@ -1,4 +1,5 @@
import { clusterStore as internalClusterStore, ClusterId } from "../../common/cluster-store"; 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 type { ClusterModel } from "../../common/cluster-store";
import { Cluster } from "../../main/cluster"; import { Cluster } from "../../main/cluster";
import { Singleton } from "../core-api/utils"; import { Singleton } from "../core-api/utils";
@ -16,16 +17,21 @@ export class ClusterStore extends Singleton {
/** /**
* Active cluster id * Active cluster id
*
* @deprecated use `workspaceStore.activeCluster`
*/ */
get activeClusterId(): string { get activeClusterId(): string {
return internalClusterStore.activeCluster; console.warn("get Store.ClusterStore.activeClusterId is deprecated. Use workspace.activeCluster");
return internalWorkspaceStore.currentWorkspace.activeClusterId;
} }
/** /**
* Set active cluster id * Set active cluster id
* @deprecated use navigate
*/ */
set activeClusterId(id : ClusterId) { set activeClusterId(id : ClusterId) {
internalClusterStore.activeCluster = id; console.warn("Store.ClusterStore.activeClusterId is deprecated. Use LensExtension.navigate()");
} }
/** /**
@ -37,13 +43,11 @@ export class ClusterStore extends Singleton {
/** /**
* Get active cluster (a cluster which is currently visible) * Get active cluster (a cluster which is currently visible)
*
* @deprecated use `clusterStore.getById(workspaceStore.currentWorkspace.activeClusterId)`
*/ */
get activeCluster(): Cluster { get activeCluster(): Cluster {
if (!this.activeClusterId) { return clusterStore.getById(internalWorkspaceStore.currentWorkspace.activeClusterId);
return null;
}
return this.getById(this.activeClusterId);
} }
/** /**

View File

@ -16,6 +16,7 @@ import logger from "./logger";
import { VersionDetector } from "./cluster-detectors/version-detector"; import { VersionDetector } from "./cluster-detectors/version-detector";
import { detectorRegistry } from "./cluster-detectors/detector-registry"; import { detectorRegistry } from "./cluster-detectors/detector-registry";
import plimit from "p-limit"; import plimit from "p-limit";
import { ResourceType } from "../renderer/components/+cluster-settings/components/cluster-metrics-setting";
export enum ClusterStatus { export enum ClusterStatus {
AccessGranted = 2, AccessGranted = 2,
@ -315,6 +316,10 @@ export class Cluster implements ClusterModel, ClusterState {
} }
} }
public isMetricHidden(resource: ResourceType) {
return Boolean(this.preferences.hiddenMetrics?.includes(resource));
}
/** /**
* @internal * @internal
*/ */

View File

@ -106,11 +106,13 @@ app.on("ready", async () => {
await Promise.all([ await Promise.all([
userStore.load(), userStore.load(),
clusterStore.load(), clusterStore.load(),
workspaceStore.load(),
extensionsStore.load(), extensionsStore.load(),
filesystemProvisionerStore.load(), filesystemProvisionerStore.load(),
]); ]);
// load this after clusterStore, because it does validation on its entries
await workspaceStore.load();
// find free port // find free port
try { try {
logger.info("🔑 Getting free port for LensProxy server"); logger.info("🔑 Getting free port for LensProxy server");

View File

@ -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);
}
});

View File

@ -0,0 +1,5 @@
import version420Beta1 from "./4.2.0-beta.1";
export default {
...version420Beta1
};

View File

@ -56,13 +56,15 @@ export async function bootstrap(App: AppComponent) {
// preload common stores // preload common stores
await Promise.all([ await Promise.all([
userStore.load(), userStore.load(),
workspaceStore.load(),
clusterStore.load(), clusterStore.load(),
extensionsStore.load(), extensionsStore.load(),
filesystemProvisionerStore.load(), filesystemProvisionerStore.load(),
themeStore.init(), themeStore.init(),
]); ]);
// load this after clusterStore, because it does validation on its entries
await workspaceStore.load();
// Register additional store listeners // Register additional store listeners
clusterStore.registerIpcListener(); clusterStore.registerIpcListener();
workspaceStore.registerIpcListener(); workspaceStore.registerIpcListener();

View File

@ -45,7 +45,7 @@ export class AddCluster extends React.Component {
@observable showSettings = false; @observable showSettings = false;
componentDidMount() { componentDidMount() {
clusterStore.setActive(null); workspaceStore.currentWorkspace.clearActiveCluster();
this.setKubeConfig(userStore.kubeConfigPath); this.setKubeConfig(userStore.kubeConfigPath);
appEventBus.emit({ name: "cluster-add", action: "start" }); appEventBus.emit({ name: "cluster-add", action: "start" });
} }
@ -181,13 +181,11 @@ export class AddCluster extends React.Component {
}); });
runInAction(() => { runInAction(() => {
clusterStore.addClusters(...newClusters); const [cluster, ...rest] = clusterStore.addClusters(...newClusters);
if (newClusters.length === 1) { if (rest.length === 0) {
const clusterId = newClusters[0].id; workspaceStore.getById(cluster.workspace).setActiveCluster(cluster);
navigate(clusterViewURL({ params: { clusterId: cluster.id } }));
clusterStore.setActive(clusterId);
navigate(clusterViewURL({ params: { clusterId } }));
} else { } else {
if (newClusters.length > 1) { if (newClusters.length > 1) {
Notifications.ok( Notifications.ok(

View File

@ -1,7 +1,7 @@
import { navigate } from "../../navigation"; import { navigate } from "../../navigation";
import { commandRegistry } from "../../../extensions/registries/command-registry"; import { commandRegistry } from "../../../extensions/registries/command-registry";
import { clusterSettingsURL } from "./cluster-settings.route"; import { clusterSettingsURL } from "./cluster-settings.route";
import { clusterStore } from "../../../common/cluster-store"; import { getHostedClusterId } from "../../../common/cluster-store";
commandRegistry.add({ commandRegistry.add({
id: "cluster.viewCurrentClusterSettings", id: "cluster.viewCurrentClusterSettings",
@ -9,7 +9,7 @@ commandRegistry.add({
scope: "global", scope: "global",
action: () => navigate(clusterSettingsURL({ action: () => navigate(clusterSettingsURL({
params: { params: {
clusterId: clusterStore.active.id clusterId: getHostedClusterId(),
} }
})), })),
isActive: (context) => !!context.cluster isActive: (context) => !!context.cluster

View File

@ -15,6 +15,7 @@ import { clusterStore } from "../../../common/cluster-store";
import { PageLayout } from "../layout/page-layout"; import { PageLayout } from "../layout/page-layout";
import { requestMain } from "../../../common/ipc"; import { requestMain } from "../../../common/ipc";
import { clusterActivateHandler, clusterRefreshHandler } from "../../../common/cluster-ipc"; import { clusterActivateHandler, clusterRefreshHandler } from "../../../common/cluster-ipc";
import { workspaceStore } from "../../../common/workspace-store";
interface Props extends RouteComponentProps<IClusterSettingsRouteParams> { interface Props extends RouteComponentProps<IClusterSettingsRouteParams> {
} }
@ -34,7 +35,9 @@ export class ClusterSettings extends React.Component<Props> {
reaction(() => this.cluster, this.refreshCluster, { reaction(() => this.cluster, this.refreshCluster, {
fireImmediately: true, fireImmediately: true,
}), }),
reaction(() => this.clusterId, clusterId => clusterStore.setActive(clusterId), { reaction(() => this.cluster, cluster => {
workspaceStore.getById(cluster.workspace).setActiveCluster(cluster);
}, {
fireImmediately: true, fireImmediately: true,
}) })
]); ]);

View File

@ -5,7 +5,7 @@ import { reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react"; import { disposeOnUnmount, observer } from "mobx-react";
import { nodesStore } from "../+nodes/nodes.store"; import { nodesStore } from "../+nodes/nodes.store";
import { podsStore } from "../+workloads-pods/pods.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 { interval } from "../../utils";
import { TabLayout } from "../layout/tab-layout"; import { TabLayout } from "../layout/tab-layout";
import { Spinner } from "../spinner"; import { Spinner } from "../spinner";
@ -66,7 +66,7 @@ export class ClusterOverview extends React.Component {
render() { render() {
const isLoaded = nodesStore.isLoaded && podsStore.isLoaded; const isLoaded = nodesStore.isLoaded && podsStore.isLoaded;
const isMetricsHidden = clusterStore.isMetricHidden(ResourceType.Cluster); const isMetricsHidden = getHostedCluster().isMetricHidden(ResourceType.Cluster);
return ( return (
<TabLayout> <TabLayout>

View File

@ -1,6 +1,6 @@
import "./landing-page.scss"; import "./landing-page.scss";
import React from "react"; import React from "react";
import { computed, observable } from "mobx"; import { computed } from "mobx";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { clusterStore } from "../../../common/cluster-store"; import { clusterStore } from "../../../common/cluster-store";
import { workspaceStore } from "../../../common/workspace-store"; import { workspaceStore } from "../../../common/workspace-store";
@ -11,28 +11,28 @@ import { Icon } from "../icon";
@observer @observer
export class LandingPage extends React.Component { export class LandingPage extends React.Component {
@observable showHint = true; @computed get workspace() {
return workspaceStore.currentWorkspace;
}
@computed @computed get workspaceClusters() {
get clusters() { return clusterStore.getByWorkspaceId(this.workspace.id);
return clusterStore.getByWorkspaceId(workspaceStore.currentWorkspaceId);
} }
componentDidMount() { componentDidMount() {
const noClustersInScope = !this.clusters.length; if (!workspaceStore.hasBeenSeen(this.workspace) && this.workspaceClusters.length === 0) {
const showStartupHint = this.showHint;
if (showStartupHint && noClustersInScope) {
Notifications.info(<><b>Welcome!</b><p>Get started by associating one or more clusters to Lens</p></>, { Notifications.info(<><b>Welcome!</b><p>Get started by associating one or more clusters to Lens</p></>, {
timeout: 30_000, timeout: 30_000,
id: "landing-welcome" id: "landing-welcome"
}); });
} }
workspaceStore.markSeen(this.workspace);
} }
render() { render() {
const showBackButton = this.clusters.length > 0; const showBackButton = this.workspaceClusters.length > 0;
const header = <><Icon svg="logo-lens" big /> <h2>{workspaceStore.currentWorkspace.name}</h2></>; const header = <><Icon svg="logo-lens" big /> <h2>{this.workspace.name}</h2></>;
return ( return (
<PageLayout className="LandingOverview flex" header={header} provideBackButtonNavigation={showBackButton} showOnTop={true}> <PageLayout className="LandingOverview flex" header={header} provideBackButtonNavigation={showBackButton} showOnTop={true}>

View File

@ -15,7 +15,7 @@ import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
import { getBackendServiceNamePort } from "../../api/endpoints/ingress.api"; import { getBackendServiceNamePort } from "../../api/endpoints/ingress.api";
import { ResourceType } from "../+cluster-settings/components/cluster-metrics-setting"; 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<Ingress> { interface Props extends KubeObjectDetailsProps<Ingress> {
} }
@ -101,6 +101,7 @@ export class IngressDetails extends React.Component<Props> {
if (!ingress) { if (!ingress) {
return null; return null;
} }
const { spec, status } = ingress; const { spec, status } = ingress;
const ingressPoints = status?.loadBalancer?.ingress; const ingressPoints = status?.loadBalancer?.ingress;
const { metrics } = ingressStore; const { metrics } = ingressStore;
@ -108,8 +109,7 @@ export class IngressDetails extends React.Component<Props> {
"Network", "Network",
"Duration", "Duration",
]; ];
const isMetricHidden = clusterStore.isMetricHidden(ResourceType.Ingress); const isMetricHidden = getHostedCluster().isMetricHidden(ResourceType.Ingress);
const { serviceName, servicePort } = ingress.getServiceNamePort(); const { serviceName, servicePort } = ingress.getServiceNamePort();
return ( return (

View File

@ -18,7 +18,7 @@ import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { KubeEventDetails } from "../+events/kube-event-details"; import { KubeEventDetails } from "../+events/kube-event-details";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
import { ResourceType } from "../+cluster-settings/components/cluster-metrics-setting"; 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<Node> { interface Props extends KubeObjectDetailsProps<Node> {
} }
@ -54,7 +54,7 @@ export class NodeDetails extends React.Component<Props> {
"Disk", "Disk",
"Pods", "Pods",
]; ];
const isMetricHidden = clusterStore.isMetricHidden(ResourceType.Node); const isMetricHidden = getHostedCluster().isMetricHidden(ResourceType.Node);
return ( return (
<div className="NodeDetails"> <div className="NodeDetails">

View File

@ -15,7 +15,7 @@ import { getDetailsUrl, KubeObjectDetailsProps, KubeObjectMeta } from "../kube-o
import { PersistentVolumeClaim } from "../../api/endpoints"; import { PersistentVolumeClaim } from "../../api/endpoints";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
import { ResourceType } from "../+cluster-settings/components/cluster-metrics-setting"; 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<PersistentVolumeClaim> { interface Props extends KubeObjectDetailsProps<PersistentVolumeClaim> {
} }
@ -43,7 +43,7 @@ export class PersistentVolumeClaimDetails extends React.Component<Props> {
const metricTabs = [ const metricTabs = [
"Disk" "Disk"
]; ];
const isMetricHidden = clusterStore.isMetricHidden(ResourceType.VolumeClaim); const isMetricHidden = getHostedCluster().isMetricHidden(ResourceType.VolumeClaim);
return ( return (
<div className="PersistentVolumeClaimDetails"> <div className="PersistentVolumeClaimDetails">

View File

@ -19,7 +19,7 @@ import { PodDetailsList } from "../+workloads-pods/pod-details-list";
import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
import { ResourceType } from "../+cluster-settings/components/cluster-metrics-setting"; 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<DaemonSet> { interface Props extends KubeObjectDetailsProps<DaemonSet> {
} }
@ -49,7 +49,7 @@ export class DaemonSetDetails extends React.Component<Props> {
const nodeSelector = daemonSet.getNodeSelectors(); const nodeSelector = daemonSet.getNodeSelectors();
const childPods = daemonSetStore.getChildPods(daemonSet); const childPods = daemonSetStore.getChildPods(daemonSet);
const metrics = daemonSetStore.metrics; const metrics = daemonSetStore.metrics;
const isMetricHidden = clusterStore.isMetricHidden(ResourceType.DaemonSet); const isMetricHidden = getHostedCluster().isMetricHidden(ResourceType.DaemonSet);
return ( return (
<div className="DaemonSetDetails"> <div className="DaemonSetDetails">

View File

@ -20,7 +20,7 @@ import { PodDetailsList } from "../+workloads-pods/pod-details-list";
import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
import { ResourceType } from "../+cluster-settings/components/cluster-metrics-setting"; 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<Deployment> { interface Props extends KubeObjectDetailsProps<Deployment> {
} }
@ -49,7 +49,7 @@ export class DeploymentDetails extends React.Component<Props> {
const selectors = deployment.getSelectors(); const selectors = deployment.getSelectors();
const childPods = deploymentStore.getChildPods(deployment); const childPods = deploymentStore.getChildPods(deployment);
const metrics = deploymentStore.metrics; const metrics = deploymentStore.metrics;
const isMetricHidden = clusterStore.isMetricHidden(ResourceType.Deployment); const isMetricHidden = getHostedCluster().isMetricHidden(ResourceType.Deployment);
return ( return (
<div className="DeploymentDetails"> <div className="DeploymentDetails">

View File

@ -12,7 +12,7 @@ import { ResourceMetrics } from "../resource-metrics";
import { IMetrics } from "../../api/endpoints/metrics.api"; import { IMetrics } from "../../api/endpoints/metrics.api";
import { ContainerCharts } from "./container-charts"; import { ContainerCharts } from "./container-charts";
import { ResourceType } from "../+cluster-settings/components/cluster-metrics-setting"; import { ResourceType } from "../+cluster-settings/components/cluster-metrics-setting";
import { clusterStore } from "../../../common/cluster-store"; import { getHostedCluster } from "../../../common/cluster-store";
interface Props { interface Props {
pod: Pod; pod: Pod;
@ -65,7 +65,7 @@ export class PodDetailsContainer extends React.Component<Props> {
"Memory", "Memory",
"Filesystem", "Filesystem",
]; ];
const isMetricHidden = clusterStore.isMetricHidden(ResourceType.Container); const isMetricHidden = getHostedCluster().isMetricHidden(ResourceType.Container);
return ( return (
<div className="PodDetailsContainer"> <div className="PodDetailsContainer">

View File

@ -23,7 +23,7 @@ import { PodCharts, podMetricTabs } from "./pod-charts";
import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
import { ResourceType } from "../+cluster-settings/components/cluster-metrics-setting"; 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<Pod> { interface Props extends KubeObjectDetailsProps<Pod> {
} }
@ -68,7 +68,7 @@ export class PodDetails extends React.Component<Props> {
const nodeSelector = pod.getNodeSelectors(); const nodeSelector = pod.getNodeSelectors();
const volumes = pod.getVolumes(); const volumes = pod.getVolumes();
const metrics = podsStore.metrics; const metrics = podsStore.metrics;
const isMetricHidden = clusterStore.isMetricHidden(ResourceType.Pod); const isMetricHidden = getHostedCluster().isMetricHidden(ResourceType.Pod);
return ( return (
<div className="PodDetails"> <div className="PodDetails">

View File

@ -18,7 +18,7 @@ import { PodDetailsList } from "../+workloads-pods/pod-details-list";
import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
import { ResourceType } from "../+cluster-settings/components/cluster-metrics-setting"; 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<ReplicaSet> { interface Props extends KubeObjectDetailsProps<ReplicaSet> {
} }
@ -49,7 +49,7 @@ export class ReplicaSetDetails extends React.Component<Props> {
const nodeSelector = replicaSet.getNodeSelectors(); const nodeSelector = replicaSet.getNodeSelectors();
const images = replicaSet.getImages(); const images = replicaSet.getImages();
const childPods = replicaSetStore.getChildPods(replicaSet); const childPods = replicaSetStore.getChildPods(replicaSet);
const isMetricHidden = clusterStore.isMetricHidden(ResourceType.ReplicaSet); const isMetricHidden = getHostedCluster().isMetricHidden(ResourceType.ReplicaSet);
return ( return (
<div className="ReplicaSetDetails"> <div className="ReplicaSetDetails">

View File

@ -19,7 +19,7 @@ import { PodDetailsList } from "../+workloads-pods/pod-details-list";
import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
import { ResourceType } from "../+cluster-settings/components/cluster-metrics-setting"; 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<StatefulSet> { interface Props extends KubeObjectDetailsProps<StatefulSet> {
} }
@ -48,7 +48,7 @@ export class StatefulSetDetails extends React.Component<Props> {
const nodeSelector = statefulSet.getNodeSelectors(); const nodeSelector = statefulSet.getNodeSelectors();
const childPods = statefulSetStore.getChildPods(statefulSet); const childPods = statefulSetStore.getChildPods(statefulSet);
const metrics = statefulSetStore.metrics; const metrics = statefulSetStore.metrics;
const isMetricHidden = clusterStore.isMetricHidden(ResourceType.StatefulSet); const isMetricHidden = getHostedCluster().isMetricHidden(ResourceType.StatefulSet);
return ( return (
<div className="StatefulSetDetails"> <div className="StatefulSetDetails">

View File

@ -7,7 +7,6 @@ import { Input, InputValidator } from "../input";
import { navigate } from "../../navigation"; import { navigate } from "../../navigation";
import { CommandOverlay } from "../command-palette/command-container"; import { CommandOverlay } from "../command-palette/command-container";
import { landingURL } from "../+landing-page"; import { landingURL } from "../+landing-page";
import { clusterStore } from "../../../common/cluster-store";
const uniqueWorkspaceName: InputValidator = { const uniqueWorkspaceName: InputValidator = {
condition: ({ required }) => required, condition: ({ required }) => required,
@ -31,7 +30,6 @@ export class AddWorkspace extends React.Component {
} }
workspaceStore.setActive(workspace.id); workspaceStore.setActive(workspace.id);
clusterStore.setActive(null);
navigate(landingURL()); navigate(landingURL());
CommandOverlay.close(); CommandOverlay.close();
} }

View File

@ -3,7 +3,7 @@ import { observer } from "mobx-react";
import { computed} from "mobx"; import { computed} from "mobx";
import { WorkspaceStore, workspaceStore } from "../../../common/workspace-store"; import { WorkspaceStore, workspaceStore } from "../../../common/workspace-store";
import { commandRegistry } from "../../../extensions/registries/command-registry"; import { commandRegistry } from "../../../extensions/registries/command-registry";
import { Select } from "../select"; import { Select, SelectOption } from "../select";
import { navigate } from "../../navigation"; import { navigate } from "../../navigation";
import { CommandOverlay } from "../command-palette/command-container"; import { CommandOverlay } from "../command-palette/command-container";
import { AddWorkspace } from "./add-workspace"; import { AddWorkspace } from "./add-workspace";
@ -20,8 +20,8 @@ export class ChooseWorkspace extends React.Component {
private static editActionId = "__edit__"; private static editActionId = "__edit__";
@computed get options() { @computed get options() {
const options = workspaceStore.enabledWorkspacesList.map((workspace) => { const options: SelectOption<string | symbol>[] = workspaceStore.enabledWorkspacesList.map((workspace) => {
return { value: workspace.id, label: workspace.name }; return { value: workspace.id, label: workspace.name, isDisabled: workspaceStore.isActive(workspace) };
}); });
options.push({ value: ChooseWorkspace.overviewActionId, label: "Show current workspace overview ..." }); options.push({ value: ChooseWorkspace.overviewActionId, label: "Show current workspace overview ..." });
@ -39,42 +39,30 @@ export class ChooseWorkspace extends React.Component {
return options; return options;
} }
onChange(id: string) { onChange(idOrAction: string): void {
if (id === ChooseWorkspace.overviewActionId) { switch (idOrAction) {
navigate(landingURL()); // overview of active workspace. TODO: change name from landing case ChooseWorkspace.overviewActionId:
CommandOverlay.close(); navigate(landingURL()); // overview of active workspace. TODO: change name from landing
return; return CommandOverlay.close();
case ChooseWorkspace.addActionId:
return CommandOverlay.open(<AddWorkspace />);
case ChooseWorkspace.removeActionId:
return CommandOverlay.open(<RemoveWorkspace />);
case ChooseWorkspace.editActionId:
return CommandOverlay.open(<EditWorkspace />);
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(<AddWorkspace />);
return;
}
if (id === ChooseWorkspace.removeActionId) {
CommandOverlay.open(<RemoveWorkspace />);
return;
}
if (id === ChooseWorkspace.editActionId) {
CommandOverlay.open(<EditWorkspace />);
return;
}
workspaceStore.setActive(id);
const clusterId = workspaceStore.getById(id).lastActiveClusterId;
if (clusterId) {
navigate(clusterViewURL({ params: { clusterId } }));
} else {
navigate(landingURL());
}
CommandOverlay.close();
} }
render() { render() {

View File

@ -9,7 +9,8 @@ import { cssNames, IClassName } from "../../utils";
import { Badge } from "../badge"; import { Badge } from "../badge";
import { Tooltip } from "../tooltip"; import { Tooltip } from "../tooltip";
import { subscribeToBroadcast } from "../../../common/ipc"; import { subscribeToBroadcast } from "../../../common/ipc";
import { observable } from "mobx"; import { computed, observable } from "mobx";
import { workspaceStore } from "../../../common/workspace-store";
interface Props extends DOMAttributes<HTMLElement> { interface Props extends DOMAttributes<HTMLElement> {
cluster: Cluster; cluster: Cluster;
@ -18,7 +19,6 @@ interface Props extends DOMAttributes<HTMLElement> {
showErrors?: boolean; showErrors?: boolean;
showTooltip?: boolean; showTooltip?: boolean;
interactive?: boolean; interactive?: boolean;
isActive?: boolean;
options?: HashiconParams; options?: HashiconParams;
} }
@ -33,8 +33,16 @@ export class ClusterIcon extends React.Component<Props> {
@observable eventCount = 0; @observable eventCount = 0;
get eventCountBroadcast() { @computed get eventCountBroadcast() {
return `cluster-warning-event-count:${this.props.cluster.id}`; 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() { componentDidMount() {
@ -48,8 +56,9 @@ export class ClusterIcon extends React.Component<Props> {
} }
render() { render() {
const { isActive } = this;
const { const {
cluster, showErrors, showTooltip, errorClass, options, interactive, isActive, cluster, showErrors, showTooltip, errorClass, options, interactive,
children, ...elemProps children, ...elemProps
} = this.props; } = this.props;
const { name, preferences, id: clusterId, online } = cluster; const { name, preferences, id: clusterId, online } = cluster;

View File

@ -17,6 +17,7 @@ import { hasLoadedView, initView, lensViews, refreshViews } from "./lens-views";
import { globalPageRegistry } from "../../../extensions/registries/page-registry"; import { globalPageRegistry } from "../../../extensions/registries/page-registry";
import { Extensions, extensionsRoute } from "../+extensions"; import { Extensions, extensionsRoute } from "../+extensions";
import { getMatchedClusterId } from "../../navigation"; import { getMatchedClusterId } from "../../navigation";
import { workspaceStore } from "../../../common/workspace-store";
@observer @observer
export class ClusterManager extends React.Component { export class ClusterManager extends React.Component {
@ -44,12 +45,12 @@ export class ClusterManager extends React.Component {
} }
get startUrl() { get startUrl() {
const { activeClusterId } = clusterStore; const { currentWorkspace } = workspaceStore;
if (activeClusterId) { if (currentWorkspace.activeClusterId) {
return clusterViewURL({ return clusterViewURL({
params: { params: {
clusterId: activeClusterId clusterId: currentWorkspace.activeClusterId
} }
}); });
} }

View File

@ -8,6 +8,7 @@ import { ClusterStatus } from "./cluster-status";
import { hasLoadedView } from "./lens-views"; import { hasLoadedView } from "./lens-views";
import { Cluster } from "../../../main/cluster"; import { Cluster } from "../../../main/cluster";
import { clusterStore } from "../../../common/cluster-store"; import { clusterStore } from "../../../common/cluster-store";
import { workspaceStore } from "../../../common/workspace-store";
interface Props extends RouteComponentProps<IClusterViewRouteParams> { interface Props extends RouteComponentProps<IClusterViewRouteParams> {
} }
@ -24,7 +25,9 @@ export class ClusterView extends React.Component<Props> {
async componentDidMount() { async componentDidMount() {
disposeOnUnmount(this, [ disposeOnUnmount(this, [
reaction(() => this.clusterId, clusterId => clusterStore.setActive(clusterId), { reaction(() => this.cluster, cluster => {
workspaceStore.getById(cluster.workspace).setActiveCluster(cluster);
}, {
fireImmediately: true, fireImmediately: true,
}) })
]); ]);

View File

@ -32,6 +32,9 @@ interface Props {
@observer @observer
export class ClustersMenu extends React.Component<Props> { export class ClustersMenu extends React.Component<Props> {
@observable workspaceMenuVisible = false; @observable workspaceMenuVisible = false;
@computed get workspace() {
return workspaceStore.currentWorkspace;
}
showCluster = (clusterId: ClusterId) => { showCluster = (clusterId: ClusterId) => {
navigate(clusterViewURL({ params: { clusterId } })); navigate(clusterViewURL({ params: { clusterId } }));
@ -56,10 +59,10 @@ export class ClustersMenu extends React.Component<Props> {
menu.append(new MenuItem({ menu.append(new MenuItem({
label: `Disconnect`, label: `Disconnect`,
click: async () => { click: async () => {
if (clusterStore.isActive(cluster.id)) { if (workspaceStore.tryClearAsWorkspaceActiveCluster(cluster)) {
navigate(landingURL()); navigate(landingURL());
clusterStore.setActive(null);
} }
await requestMain(clusterDisconnectHandler, cluster.id); await requestMain(clusterDisconnectHandler, cluster.id);
} }
})); }));
@ -76,11 +79,8 @@ export class ClustersMenu extends React.Component<Props> {
label: `Remove`, label: `Remove`,
}, },
ok: () => { ok: () => {
if (clusterStore.activeClusterId === cluster.id) {
navigate(landingURL());
clusterStore.setActive(null);
}
clusterStore.removeById(cluster.id); clusterStore.removeById(cluster.id);
navigate(landingURL());
}, },
message: <p>Are you sure want to remove cluster <b title={cluster.id}>{cluster.contextName}</b>?</p>, message: <p>Are you sure want to remove cluster <b title={cluster.id}>{cluster.contextName}</b>?</p>,
}); });
@ -107,9 +107,7 @@ export class ClustersMenu extends React.Component<Props> {
render() { render() {
const { className } = this.props; const { className } = this.props;
const workspace = workspaceStore.getById(workspaceStore.currentWorkspaceId); const clusters = clusterStore.getByWorkspaceId(this.workspace.id).filter(cluster => cluster.enabled);
const clusters = clusterStore.getByWorkspaceId(workspace.id).filter(cluster => cluster.enabled);
const activeClusterId = clusterStore.activeCluster;
return ( return (
<div className={cssNames("ClustersMenu flex column", className)}> <div className={cssNames("ClustersMenu flex column", className)}>
@ -118,26 +116,21 @@ export class ClustersMenu extends React.Component<Props> {
<Droppable droppableId="cluster-menu" type="CLUSTER"> <Droppable droppableId="cluster-menu" type="CLUSTER">
{({ innerRef, droppableProps, placeholder }: DroppableProvided) => ( {({ innerRef, droppableProps, placeholder }: DroppableProvided) => (
<div ref={innerRef} {...droppableProps}> <div ref={innerRef} {...droppableProps}>
{clusters.map((cluster, index) => { {clusters.map((cluster, index) => (
const isActive = cluster.id === activeClusterId; <Draggable draggableId={cluster.id} index={index} key={cluster.id}>
{({ draggableProps, dragHandleProps, innerRef }: DraggableProvided) => (
return ( <div ref={innerRef} {...draggableProps} {...dragHandleProps}>
<Draggable draggableId={cluster.id} index={index} key={cluster.id}> <ClusterIcon
{({ draggableProps, dragHandleProps, innerRef }: DraggableProvided) => ( key={cluster.id}
<div ref={innerRef} {...draggableProps} {...dragHandleProps}> showErrors={true}
<ClusterIcon cluster={cluster}
key={cluster.id} onClick={() => this.showCluster(cluster.id)}
showErrors={true} onContextMenu={() => this.showContextMenu(cluster)}
cluster={cluster} />
isActive={isActive} </div>
onClick={() => this.showCluster(cluster.id)} )}
onContextMenu={() => this.showContextMenu(cluster)} </Draggable>
/> ))}
</div>
)}
</Draggable>
);
})}
{placeholder} {placeholder}
</div> </div>
)} )}

View File

@ -8,7 +8,6 @@ import { EventEmitter } from "../../../common/event-emitter";
import { subscribeToBroadcast } from "../../../common/ipc"; import { subscribeToBroadcast } from "../../../common/ipc";
import { CommandDialog } from "./command-dialog"; import { CommandDialog } from "./command-dialog";
import { CommandRegistration, commandRegistry } from "../../../extensions/registries/command-registry"; import { CommandRegistration, commandRegistry } from "../../../extensions/registries/command-registry";
import { clusterStore } from "../../../common/cluster-store";
import { workspaceStore } from "../../../common/workspace-store"; import { workspaceStore } from "../../../common/workspace-store";
export type CommandDialogEvent = { export type CommandDialogEvent = {
@ -49,7 +48,7 @@ export class CommandContainer extends React.Component<{ clusterId?: string }> {
private runCommand(command: CommandRegistration) { private runCommand(command: CommandRegistration) {
command.action({ command.action({
cluster: clusterStore.active, cluster: workspaceStore.currentWorkspace.activeCluster,
workspace: workspaceStore.currentWorkspace workspace: workspaceStore.currentWorkspace
}); });
} }

View File

@ -4,7 +4,6 @@ import { computed, observable, toJS } from "mobx";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import React from "react"; import React from "react";
import { commandRegistry } from "../../../extensions/registries/command-registry"; import { commandRegistry } from "../../../extensions/registries/command-registry";
import { clusterStore } from "../../../common/cluster-store";
import { workspaceStore } from "../../../common/workspace-store"; import { workspaceStore } from "../../../common/workspace-store";
import { CommandOverlay } from "./command-container"; import { CommandOverlay } from "./command-container";
import { broadcastMessage } from "../../../common/ipc"; import { broadcastMessage } from "../../../common/ipc";
@ -16,30 +15,31 @@ export class CommandDialog extends React.Component {
@observable menuIsOpen = true; @observable menuIsOpen = true;
@computed get options() { @computed get options() {
const context = { const activeCluster = workspaceStore.currentWorkspace.activeCluster;
cluster: clusterStore.active,
workspace: workspaceStore.currentWorkspace
};
return commandRegistry.getItems().filter((command) => { return commandRegistry.getItems()
if (command.scope === "cluster" && !clusterStore.active) { .filter(command => {
return false; if (command.scope === "cluster" && !activeCluster) {
} return false;
}
if (!command.isActive) { if (!command.isActive) {
return true; return true;
} }
try { try {
return command.isActive(context); return command.isActive({
} catch(e) { cluster: activeCluster,
console.error(e); workspace: workspaceStore.currentWorkspace
});
} catch(e) {
console.error(e);
return false; return false;
} }
}).map((command) => { })
return { value: command.id, label: command.title }; .map(({ id, title }) => ({ value: id, label: title }))
}).sort((a, b) => a.label > b.label ? 1 : -1); .sort((a, b) => a.label > b.label ? 1 : -1);
} }
private onChange(value: string) { private onChange(value: string) {
@ -49,6 +49,7 @@ export class CommandDialog extends React.Component {
return; return;
} }
const activeCluster = workspaceStore.currentWorkspace.activeCluster;
const action = toJS(command.action); const action = toJS(command.action);
try { try {
@ -56,16 +57,16 @@ export class CommandDialog extends React.Component {
if (command.scope === "global") { if (command.scope === "global") {
action({ action({
cluster: clusterStore.active, cluster: activeCluster,
workspace: workspaceStore.currentWorkspace workspace: workspaceStore.currentWorkspace
}); });
} else if(clusterStore.active) { } else if(activeCluster) {
navigate(clusterViewURL({ navigate(clusterViewURL({
params: { 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) { } catch(error) {
console.error("[COMMAND-DIALOG] failed to execute command", command.id, error); console.error("[COMMAND-DIALOG] failed to execute command", command.id, error);

View File

@ -104,9 +104,7 @@ html {
&--is-disabled { &--is-disabled {
cursor: not-allowed; cursor: not-allowed;
background: none !important; opacity: .33;
color: $contentColor;
opacity: .75;
} }
.Icon { .Icon {