diff --git a/src/common/cluster-store.ts b/src/common/cluster-store.ts index 7ae402eefe..d1006f0a3d 100644 --- a/src/common/cluster-store.ts +++ b/src/common/cluster-store.ts @@ -225,6 +225,12 @@ export class ClusterStore extends BaseStore { 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); diff --git a/src/renderer/components/cluster-manager/cluster-actions.tsx b/src/renderer/components/cluster-manager/cluster-actions.tsx new file mode 100644 index 0000000000..f8f6f1ba41 --- /dev/null +++ b/src/renderer/components/cluster-manager/cluster-actions.tsx @@ -0,0 +1,50 @@ +import React from "react"; +import uniqueId from "lodash/uniqueId"; +import { clusterSettingsURL } from "../+cluster-settings"; +import { landingURL } from "../+landing-page"; + +import { clusterStore } from "../../../common/cluster-store"; +import { broadcastMessage, requestMain } from "../../../common/ipc"; +import { clusterDisconnectHandler } from "../../../common/cluster-ipc"; +import { ConfirmDialog } from "../confirm-dialog"; +import { Cluster } from "../../../main/cluster"; +import { Tooltip } from "../../components//tooltip"; + +const navigate = (route: string) => + broadcastMessage("renderer:navigate", route); + +/** + * Creates handlers for high-level actions + * that could be performed on an individual cluster + * @param cluster Cluster + */ +export const ClusterActions = (cluster: Cluster) => ({ + showSettings: () => navigate(clusterSettingsURL({ + params: { clusterId: cluster.id } + })), + disconnect: async () => { + clusterStore.deactivate(cluster.id); + navigate(landingURL()); + await requestMain(clusterDisconnectHandler, cluster.id); + }, + remove: () => { + const tooltipId = uniqueId("tooltip_target_"); + + return ConfirmDialog.open({ + okButtonProps: { + primary: false, + accent: true, + label: "Remove" + }, + ok: () => { + clusterStore.deactivate(cluster.id); + clusterStore.removeById(cluster.id); + navigate(landingURL()); + }, + message:

+ Are you sure want to remove cluster {cluster.name}? + {cluster.id} +

+ }); + } +}); diff --git a/src/renderer/components/cluster-manager/clusters-menu.tsx b/src/renderer/components/cluster-manager/clusters-menu.tsx index dd36e529e1..03711e41fc 100644 --- a/src/renderer/components/cluster-manager/clusters-menu.tsx +++ b/src/renderer/components/cluster-manager/clusters-menu.tsx @@ -2,7 +2,6 @@ import "./clusters-menu.scss"; import React from "react"; import { remote } from "electron"; -import { requestMain } from "../../../common/ipc"; import type { Cluster } from "../../../main/cluster"; import { DragDropContext, Draggable, DraggableProvided, Droppable, DroppableProvided, DropResult } from "react-beautiful-dnd"; import { observer } from "mobx-react"; @@ -13,12 +12,10 @@ import { Icon } from "../icon"; import { autobind, cssNames, IClassName } from "../../utils"; import { isActiveRoute, navigate } from "../../navigation"; import { addClusterURL } from "../+add-cluster"; -import { clusterSettingsURL } from "../+cluster-settings"; import { landingURL } from "../+landing-page"; -import { ConfirmDialog } from "../confirm-dialog"; import { clusterViewURL } from "./cluster-view.route"; +import { ClusterActions } from "./cluster-actions"; import { getExtensionPageUrl, globalPageMenuRegistry, globalPageRegistry } from "../../../extensions/registries"; -import { clusterDisconnectHandler } from "../../../common/cluster-ipc"; import { commandRegistry } from "../../../extensions/registries/command-registry"; import { CommandOverlay } from "../command-palette/command-container"; import { computed, observable } from "mobx"; @@ -40,51 +37,24 @@ export class ClustersMenu extends React.Component { showContextMenu = (cluster: Cluster) => { const { Menu, MenuItem } = remote; const menu = new Menu(); + const actions = ClusterActions(cluster); menu.append(new MenuItem({ label: `Settings`, - click: () => { - navigate(clusterSettingsURL({ - params: { - clusterId: cluster.id - } - })); - } + click: actions.showSettings })); if (cluster.online) { menu.append(new MenuItem({ label: `Disconnect`, - click: async () => { - if (clusterStore.isActive(cluster.id)) { - navigate(landingURL()); - clusterStore.setActive(null); - } - await requestMain(clusterDisconnectHandler, cluster.id); - } + click: actions.disconnect })); } if (!cluster.isManaged) { menu.append(new MenuItem({ label: `Remove`, - click: () => { - ConfirmDialog.open({ - okButtonProps: { - primary: false, - accent: true, - label: `Remove`, - }, - ok: () => { - if (clusterStore.activeClusterId === cluster.id) { - navigate(landingURL()); - clusterStore.setActive(null); - } - clusterStore.removeById(cluster.id); - }, - message:

Are you sure want to remove cluster {cluster.contextName}?

, - }); - } + click: actions.remove })); } menu.popup({ diff --git a/src/renderer/components/cluster-manager/index.tsx b/src/renderer/components/cluster-manager/index.tsx index 692f1676ef..74595583aa 100644 --- a/src/renderer/components/cluster-manager/index.tsx +++ b/src/renderer/components/cluster-manager/index.tsx @@ -1 +1,2 @@ export * from "./cluster-manager"; +export * from "./cluster-actions"; diff --git a/src/renderer/components/layout/__test__/main-layout-header.test.tsx b/src/renderer/components/layout/__test__/main-layout-header.test.tsx index 499839072c..dc696592d9 100644 --- a/src/renderer/components/layout/__test__/main-layout-header.test.tsx +++ b/src/renderer/components/layout/__test__/main-layout-header.test.tsx @@ -7,15 +7,18 @@ import "@testing-library/jest-dom/extend-expect"; import { MainLayoutHeader } from "../main-layout-header"; import { Cluster } from "../../../../main/cluster"; import { workspaceStore } from "../../../../common/workspace-store"; -import { broadcastMessage } from "../../../../common/ipc"; +import { broadcastMessage, requestMain } from "../../../../common/ipc"; +import { clusterDisconnectHandler } from "../../../../common/cluster-ipc"; +import { ConfirmDialog } from "../../confirm-dialog"; const mockBroadcastIpc = broadcastMessage as jest.MockedFunction; +const mockRequestMain = requestMain as jest.MockedFunction; const cluster: Cluster = new Cluster({ id: "foo", contextName: "minikube", kubeConfigPath: "minikube-config.yml", - workspace: workspaceStore.currentWorkspaceId + workspace: workspaceStore.currentWorkspaceId, }); describe("", () => { @@ -25,20 +28,12 @@ describe("", () => { expect(container).toBeInstanceOf(HTMLElement); }); - it("renders gear icon", () => { + it("renders three dots icon", () => { const { container } = render(); const icon = container.querySelector(".Icon .icon"); expect(icon).toBeInstanceOf(HTMLElement); - expect(icon).toHaveTextContent("settings"); - }); - - it("navigates to cluster settings", () => { - const { container } = render(); - const icon = container.querySelector(".Icon"); - - fireEvent.click(icon); - expect(mockBroadcastIpc).toBeCalledWith("renderer:navigate", "/cluster/foo/settings"); + expect(icon).toHaveTextContent("more_vert"); }); it("renders cluster name", () => { @@ -46,4 +41,60 @@ describe("", () => { expect(getByText("minikube")).toBeInTheDocument(); }); + + describe("Cluster Actions Menu", () => { + let settingsBtn: Element; + let disconnectBtn: Element; + let removeBtn: Element; + + beforeEach(() => { + const { container } = render(
+ + +
); + const icon = container.querySelector(".Icon"); + + cluster.online = true; + fireEvent.click(icon); + + [settingsBtn, disconnectBtn, removeBtn] = Array.from(document.querySelectorAll("ul.ClusterActionsMenu > li")) + .map(el => el.querySelector("span")); + }); + + afterEach(() => { + cluster.online = false; + }); + + it("renders cluster menu items", () => { + expect(settingsBtn).toBeDefined(); + expect(settingsBtn.textContent).toBe("Settings"); + expect(disconnectBtn).toBeDefined(); + expect(disconnectBtn.textContent).toBe("Disconnect"); + expect(removeBtn).toBeDefined(); + expect(removeBtn.textContent).toBe("Remove"); + }); + + it("navigates to cluster settings", () => { + fireEvent.click(settingsBtn); + expect(mockBroadcastIpc).toBeCalledWith("renderer:navigate", "/cluster/foo/settings"); + }); + + it("disconnects from cluster", () => { + fireEvent.click(disconnectBtn); + expect(mockRequestMain).toBeCalledWith(clusterDisconnectHandler, cluster.id); + }); + + it("opens 'Remove cluster' dialog", async () => { + fireEvent.click(removeBtn); + + const dialog = document.querySelector(".ConfirmDialog"); + + expect(dialog).toBeDefined(); + expect(dialog).not.toBe(null); + + const okBtn = dialog.querySelector("button.ok"); + + expect(okBtn.textContent).toBe("Remove"); + }); + }); }); diff --git a/src/renderer/components/layout/main-layout-header.tsx b/src/renderer/components/layout/main-layout-header.tsx index 570c04a1f9..4fb6b69f9b 100644 --- a/src/renderer/components/layout/main-layout-header.tsx +++ b/src/renderer/components/layout/main-layout-header.tsx @@ -1,11 +1,10 @@ -import { observer } from "mobx-react"; import React from "react"; +import { observer } from "mobx-react"; -import { clusterSettingsURL } from "../+cluster-settings"; -import { broadcastMessage } from "../../../common/ipc"; +import { ClusterActions } from "../cluster-manager"; import { Cluster } from "../../../main/cluster"; import { cssNames } from "../../utils"; -import { Icon } from "../icon"; +import { MenuActions, MenuItem } from "../menu"; interface Props { cluster: Cluster @@ -13,21 +12,28 @@ interface Props { } export const MainLayoutHeader = observer(({ cluster, className }: Props) => { + const actions = ClusterActions(cluster); + const renderMenu = () => ( + + + Settings + + + Disconnect + + { + !cluster.isManaged && ( + + Remove + + ) + } + ); + return (
{cluster.name} - { - broadcastMessage("renderer:navigate", clusterSettingsURL({ - params: { - clusterId: cluster.id - } - })); - }} - /> + {renderMenu()}
); });