Add context menu entry for deleting local clusters (#2923)

This commit is contained in:
Sebastian Malton authored and GitHub committed 2021-06-17 12:19:22 -04:00
1 parent e47d26e1ce
commit eb45f45a7a
30 files changed
+348 -377

No files matched your search

+18 -33
View File
@@ -125,33 +125,28 @@ describe("empty config", () => {
expect(storedCluster.preferences.terminalCWD).toBe("/tmp");
expect(storedCluster.preferences.icon).toBe("data:image/jpeg;base64, iVBORw0KGgoAAAANSUhEUgAAA1wAAAKoCAYAAABjkf5");
});
it("removes cluster from store", async () => {
await ClusterStore.getInstance().removeById("foo");
expect(ClusterStore.getInstance().getById("foo")).toBeNull();
});
});
describe("with prod and dev clusters added", () => {
beforeEach(() => {
ClusterStore.getInstance().addClusters(
new Cluster({
id: "prod",
contextName: "foo",
preferences: {
clusterName: "prod"
},
kubeConfigPath: embed("prod", kubeconfig)
}),
new Cluster({
id: "dev",
contextName: "foo2",
preferences: {
clusterName: "dev"
},
kubeConfigPath: embed("dev", kubeconfig)
})
);
const store = ClusterStore.getInstance();
store.addCluster({
id: "prod",
contextName: "foo",
preferences: {
clusterName: "prod"
},
kubeConfigPath: embed("prod", kubeconfig)
});
store.addCluster({
id: "dev",
contextName: "foo2",
preferences: {
clusterName: "dev"
},
kubeConfigPath: embed("dev", kubeconfig)
});
});
it("check if store can contain multiple clusters", () => {
@@ -222,16 +217,6 @@ describe("config with existing clusters", () => {
expect(storedCluster.preferences.terminalCWD).toBe("/foo");
});
it("allows to delete a cluster", () => {
ClusterStore.getInstance().removeById("cluster2");
const storedCluster = ClusterStore.getInstance().getById("cluster1");
expect(storedCluster).toBeTruthy();
const storedCluster2 = ClusterStore.getInstance().getById("cluster2");
expect(storedCluster2).toBeNull();
});
it("allows getting all of the clusters", async () => {
const storedClusters = ClusterStore.getInstance().clustersList;
@@ -21,13 +21,13 @@
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
import { CatalogEntity, CatalogEntityActionContext, CatalogEntityAddMenuContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog";
import { clusterActivateHandler, clusterDisconnectHandler } from "../cluster-ipc";
import { clusterActivateHandler, clusterDeleteHandler, clusterDisconnectHandler } from "../cluster-ipc";
import { ClusterStore } from "../cluster-store";
import { requestMain } from "../ipc";
import { productName } from "../vars";
import { CatalogCategory, CatalogCategorySpec } from "../catalog";
import { addClusterURL } from "../routes";
import { app } from "electron";
import { HotbarStore } from "../hotbar-store";
export type KubernetesClusterPrometheusMetrics = {
address?: {
@@ -50,7 +50,7 @@ export type KubernetesClusterSpec = {
};
export interface KubernetesClusterStatus extends CatalogEntityStatus {
phase: "connected" | "disconnected";
phase: "connected" | "disconnected" | "deleting";
}
export class KubernetesCluster extends CatalogEntity<CatalogEntityMetadata, KubernetesClusterStatus, KubernetesClusterSpec> {
@@ -103,39 +103,38 @@ export class KubernetesCluster extends CatalogEntity<CatalogEntityMetadata, Kube
async onContextMenuOpen(context: CatalogEntityContextMenuContext) {
if (!this.metadata.source || this.metadata.source === "local") {
context.menuItems.push({
title: "Settings",
icon: "edit",
onClick: async () => context.navigate(`/entity/${this.metadata.uid}/settings`)
});
}
if (this.metadata.labels["file"]?.startsWith(ClusterStore.storedKubeConfigFolder)) {
context.menuItems.push({
title: "Delete",
icon: "delete",
onClick: async () => ClusterStore.getInstance().removeById(this.metadata.uid),
confirm: {
message: `Remove Kubernetes Cluster "${this.metadata.name} from ${productName}?`
}
});
context.menuItems.push(
{
title: "Settings",
icon: "edit",
onClick: () => context.navigate(`/entity/${this.metadata.uid}/settings`)
},
{
title: "Delete",
icon: "delete",
onClick: () => {
HotbarStore.getInstance().removeAllHotbarItems(this.getId());
requestMain(clusterDeleteHandler, this.metadata.uid);
},
confirm: {
// TODO: change this to be a <p> tag with better formatting once this code can accept it.
message: `Delete the "${this.metadata.name}" context from "${this.metadata.labels.file}"?`
}
},
);
}
if (this.status.phase == "connected") {
context.menuItems.push({
title: "Disconnect",
icon: "link_off",
onClick: async () => {
requestMain(clusterDisconnectHandler, this.metadata.uid);
}
onClick: () => requestMain(clusterDisconnectHandler, this.metadata.uid)
});
} else {
context.menuItems.push({
title: "Connect",
icon: "link",
onClick: async () => {
context.navigate(`/cluster/${this.metadata.uid}`);
}
onClick: () => context.navigate(`/cluster/${this.metadata.uid}`)
});
}
+5
View File
@@ -86,6 +86,11 @@ export interface CatalogEntityMetadata {
export interface CatalogEntityStatus {
phase: string;
reason?: string;
/**
* @default true
*/
enabled?: boolean;
message?: string;
active?: boolean;
}
+1
View File
@@ -24,5 +24,6 @@ export const clusterSetFrameIdHandler = "cluster:set-frame-id";
export const clusterVisibilityHandler = "cluster:visibility";
export const clusterRefreshHandler = "cluster:refresh";
export const clusterDisconnectHandler = "cluster:disconnect";
export const clusterDeleteHandler = "cluster:delete";
export const clusterKubectlApplyAllHandler = "cluster:kubectl-apply-all";
export const clusterKubectlDeleteAllHandler = "cluster:kubectl-delete-all";
+14 -65
View File
@@ -20,8 +20,7 @@
*/
import path from "path";
import { app, ipcMain, ipcRenderer, remote, webFrame } from "electron";
import { unlink } from "fs-extra";
import { app, ipcMain, ipcRenderer, webFrame } from "electron";
import { action, comparer, computed, makeObservable, observable, reaction } from "mobx";
import { BaseStore } from "./base-store";
import { Cluster, ClusterState } from "../main/cluster";
@@ -30,7 +29,7 @@ import * as uuid from "uuid";
import logger from "../main/logger";
import { appEventBus } from "./event-bus";
import { ipcMainHandle, ipcMainOn, ipcRendererOn, requestMain } from "./ipc";
import { disposer, noop, toJS } from "./utils";
import { disposer, toJS } from "./utils";
export interface ClusterIconUpload {
clusterId: string;
@@ -83,9 +82,6 @@ export interface ClusterModel {
/** List of accessible namespaces */
accessibleNamespaces?: string[];
/** @deprecated */
kubeConfig?: string; // yaml
}
export interface ClusterPreferences extends ClusterPrometheusPreferences {
@@ -113,7 +109,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
private static StateChannel = "cluster:state";
static get storedKubeConfigFolder(): string {
return path.resolve((app || remote.app).getPath("userData"), "kubeconfigs");
return path.resolve(app.getPath("userData"), "kubeconfigs");
}
static getCustomKubeConfigPath(clusterId: ClusterId = uuid.v4()): string {
@@ -123,7 +119,6 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
@observable clusters = observable.map<ClusterId, Cluster>();
@observable removedClusters = observable.map<ClusterId, Cluster>();
private static stateRequestChannel = "cluster:states";
protected disposer = disposer();
constructor() {
@@ -142,35 +137,22 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
}
async load() {
const initialStates = "cluster:states";
await super.load();
type clusterStateSync = {
id: string;
state: ClusterState;
};
if (ipcRenderer) {
logger.info("[CLUSTER-STORE] requesting initial state sync");
const clusterStates: clusterStateSync[] = await requestMain(ClusterStore.stateRequestChannel);
clusterStates.forEach((clusterState) => {
const cluster = this.getById(clusterState.id);
if (cluster) {
cluster.setState(clusterState.state);
}
});
for (const { id, state } of await requestMain(initialStates)) {
this.getById(id)?.setState(state);
}
} else if (ipcMain) {
ipcMainHandle(ClusterStore.stateRequestChannel, (): clusterStateSync[] => {
const clusterStates: clusterStateSync[] = [];
this.clustersList.forEach((cluster) => {
clusterStates.push({
state: cluster.getState(),
id: cluster.id
});
});
return clusterStates;
ipcMainHandle(initialStates, () => {
return this.clustersList.map(cluster => ({
id: cluster.id,
state: cluster.getState(),
}));
});
}
}
@@ -178,9 +160,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
protected pushStateToViewsAutomatically() {
if (ipcMain) {
this.disposer.push(
reaction(() => this.connectedClustersList, () => {
this.pushState();
}),
reaction(() => this.connectedClustersList, () => this.pushState()),
);
}
}
@@ -229,18 +209,6 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
return this.clusters.get(id) ?? null;
}
@action
addClusters(...models: ClusterModel[]): Cluster[] {
const clusters: Cluster[] = [];
models.forEach(model => {
clusters.push(this.addCluster(model));
});
return clusters;
}
@action
addCluster(clusterOrModel: ClusterModel | Cluster): Cluster {
appEventBus.emit({ name: "cluster", action: "add" });
@@ -253,25 +221,6 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
return cluster;
}
async removeCluster(model: ClusterModel) {
await this.removeById(model.id);
}
@action
async removeById(clusterId: ClusterId) {
appEventBus.emit({ name: "cluster", action: "remove" });
const cluster = this.getById(clusterId);
if (cluster) {
this.clusters.delete(clusterId);
// remove only custom kubeconfigs (pasted as text)
if (cluster.kubeConfigPath == ClusterStore.getCustomKubeConfigPath(clusterId)) {
await unlink(cluster.kubeConfigPath).catch(noop);
}
}
}
@action
protected fromStore({ clusters = [] }: ClusterStoreModel = {}) {
const currentClusters = new Map(this.clusters);
+20 -1
View File
@@ -181,7 +181,7 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
}
@action
removeFromHotbar(uid: string) {
removeFromHotbar(uid: string): void {
const hotbar = this.getActive();
const index = hotbar.items.findIndex((i) => i?.entity.uid === uid);
@@ -192,6 +192,25 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
hotbar.items[index] = null;
}
/**
* Remvove all hotbar items that reference the `uid`.
* @param uid The `EntityId` that each hotbar item refers to
* @returns A function that will (in an action) undo the removing of the hotbar items. This function will not complete if the hotbar has changed.
*/
@action
removeAllHotbarItems(uid: string) {
const undoItems: [Hotbar, number, HotbarItem][] = [];
for (const hotbar of this.hotbars) {
const index = hotbar.items.findIndex((i) => i?.entity.uid === uid);
if (index >= 0) {
undoItems.push([hotbar, index, hotbar.items[index]]);
hotbar.items[index] = null;
}
}
}
findClosestEmptyIndex(from: number, direction = 1) {
let index = from;
+3 -2
View File
@@ -19,8 +19,9 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
// Common utils (main OR renderer)
/**
* A function that does nothing
*/
export function noop<T extends any[]>(...args: T): void {
return void args;
}