From 125a0730074b0fc4637280b7f47f4705ff666f61 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Tue, 19 Oct 2021 09:19:25 -0400 Subject: [PATCH] Add context menu into sidebar (#4044) --- integration/__tests__/cluster-pages.tests.ts | 384 +++++++++--------- .../catalog-entities/kubernetes-cluster.ts | 8 +- src/common/catalog/catalog-entity.ts | 5 +- src/common/routes/cluster.ts | 2 +- src/renderer/components/app.tsx | 2 + .../components/hotbar/hotbar-entity-icon.tsx | 12 +- src/renderer/components/layout/sidebar.tsx | 26 ++ src/renderer/navigation/events.ts | 1 + 8 files changed, 243 insertions(+), 197 deletions(-) diff --git a/integration/__tests__/cluster-pages.tests.ts b/integration/__tests__/cluster-pages.tests.ts index be032afab3..bd31a80102 100644 --- a/integration/__tests__/cluster-pages.tests.ts +++ b/integration/__tests__/cluster-pages.tests.ts @@ -36,7 +36,7 @@ function getSidebarSelectors(itemId: string) { return { expandSubMenu: `${root} .nav-item`, - subMenuLink: (href: string) => `[data-testid=cluster-sidebar] .sub-menu a[href^="/${href}"]`, + subMenuLink: (href: string) => `[data-testid=cluster-sidebar] .sub-menu a[href^="${href}"]`, }; } @@ -73,7 +73,7 @@ function isTopPageTest(test: CommonPageTest): test is TopPageTest { const commonPageTests: CommonPageTest[] = [{ page: { name: "Cluster", - href: "cluster", + href: "/overview", expectedSelector: "div.ClusterOverview div.label", expectedText: "CPU" } @@ -81,153 +81,161 @@ const commonPageTests: CommonPageTest[] = [{ { page: { name: "Nodes", - href: "nodes", + href: "/nodes", expectedSelector: "h5.title", expectedText: "Nodes" } }, { drawerId: "workloads", - pages: [{ - name: "Overview", - href: "workloads", - expectedSelector: "h5.box", - expectedText: "Overview" - }, - { - name: "Pods", - href: "pods", - expectedSelector: "h5.title", - expectedText: "Pods" - }, - { - name: "Deployments", - href: "deployments", - expectedSelector: "h5.title", - expectedText: "Deployments" - }, - { - name: "DaemonSets", - href: "daemonsets", - expectedSelector: "h5.title", - expectedText: "Daemon Sets" - }, - { - name: "StatefulSets", - href: "statefulsets", - expectedSelector: "h5.title", - expectedText: "Stateful Sets" - }, - { - name: "ReplicaSets", - href: "replicasets", - expectedSelector: "h5.title", - expectedText: "Replica Sets" - }, - { - name: "Jobs", - href: "jobs", - expectedSelector: "h5.title", - expectedText: "Jobs" - }, - { - name: "CronJobs", - href: "cronjobs", - expectedSelector: "h5.title", - expectedText: "Cron Jobs" - }] + pages: [ + { + name: "Overview", + href: "/workloads", + expectedSelector: "h5.box", + expectedText: "Overview" + }, + { + name: "Pods", + href: "/pods", + expectedSelector: "h5.title", + expectedText: "Pods" + }, + { + name: "Deployments", + href: "/deployments", + expectedSelector: "h5.title", + expectedText: "Deployments" + }, + { + name: "DaemonSets", + href: "/daemonsets", + expectedSelector: "h5.title", + expectedText: "Daemon Sets" + }, + { + name: "StatefulSets", + href: "/statefulsets", + expectedSelector: "h5.title", + expectedText: "Stateful Sets" + }, + { + name: "ReplicaSets", + href: "/replicasets", + expectedSelector: "h5.title", + expectedText: "Replica Sets" + }, + { + name: "Jobs", + href: "/jobs", + expectedSelector: "h5.title", + expectedText: "Jobs" + }, + { + name: "CronJobs", + href: "/cronjobs", + expectedSelector: "h5.title", + expectedText: "Cron Jobs" + }, + ] }, { drawerId: "config", - pages: [{ - name: "ConfigMaps", - href: "configmaps", - expectedSelector: "h5.title", - expectedText: "Config Maps" - }, - { - name: "Secrets", - href: "secrets", - expectedSelector: "h5.title", - expectedText: "Secrets" - }, - { - name: "Resource Quotas", - href: "resourcequotas", - expectedSelector: "h5.title", - expectedText: "Resource Quotas" - }, - { - name: "Limit Ranges", - href: "limitranges", - expectedSelector: "h5.title", - expectedText: "Limit Ranges" - }, - { - name: "HPA", - href: "hpa", - expectedSelector: "h5.title", - expectedText: "Horizontal Pod Autoscalers" - }, - { - name: "Pod Disruption Budgets", - href: "poddisruptionbudgets", - expectedSelector: "h5.title", - expectedText: "Pod Disruption Budgets" - }] + pages: [ + { + name: "ConfigMaps", + href: "/configmaps", + expectedSelector: "h5.title", + expectedText: "Config Maps" + }, + { + name: "Secrets", + href: "/secrets", + expectedSelector: "h5.title", + expectedText: "Secrets" + }, + { + name: "Resource Quotas", + href: "/resourcequotas", + expectedSelector: "h5.title", + expectedText: "Resource Quotas" + }, + { + name: "Limit Ranges", + href: "/limitranges", + expectedSelector: "h5.title", + expectedText: "Limit Ranges" + }, + { + name: "HPA", + href: "/hpa", + expectedSelector: "h5.title", + expectedText: "Horizontal Pod Autoscalers" + }, + { + name: "Pod Disruption Budgets", + href: "/poddisruptionbudgets", + expectedSelector: "h5.title", + expectedText: "Pod Disruption Budgets" + }, + ] }, { drawerId: "networks", - pages: [{ - name: "Services", - href: "services", - expectedSelector: "h5.title", - expectedText: "Services" - }, - { - name: "Endpoints", - href: "endpoints", - expectedSelector: "h5.title", - expectedText: "Endpoints" - }, - { - name: "Ingresses", - href: "ingresses", - expectedSelector: "h5.title", - expectedText: "Ingresses" - }, - { - name: "Network Policies", - href: "network-policies", - expectedSelector: "h5.title", - expectedText: "Network Policies" - }] + pages: [ + { + name: "Services", + href: "/services", + expectedSelector: "h5.title", + expectedText: "Services" + }, + { + name: "Endpoints", + href: "/endpoints", + expectedSelector: "h5.title", + expectedText: "Endpoints" + }, + { + name: "Ingresses", + href: "/ingresses", + expectedSelector: "h5.title", + expectedText: "Ingresses" + }, + { + name: "Network Policies", + href: "/network-policies", + expectedSelector: "h5.title", + expectedText: "Network Policies" + }, + ] }, { drawerId: "storage", - pages: [{ - name: "Persistent Volume Claims", - href: "persistent-volume-claims", - expectedSelector: "h5.title", - expectedText: "Persistent Volume Claims" - }, - { - name: "Persistent Volumes", - href: "persistent-volumes", - expectedSelector: "h5.title", - expectedText: "Persistent Volumes" - }, - { - name: "Storage Classes", - href: "storage-classes", - expectedSelector: "h5.title", - expectedText: "Storage Classes" - }] + pages: [ + { + name: "Persistent Volume Claims", + href: "/persistent-volume-claims", + expectedSelector: "h5.title", + expectedText: "Persistent Volume Claims" + }, + { + name: "Persistent Volumes", + href: "/persistent-volumes", + expectedSelector: "h5.title", + expectedText: "Persistent Volumes" + }, + { + name: "Storage Classes", + href: "/storage-classes", + expectedSelector: "h5.title", + expectedText: "Storage Classes" + }, + ] }, { page: { name: "Namespaces", - href: "namespaces", + href: "/namespaces", expectedSelector: "h5.title", expectedText: "Namespaces" } @@ -235,72 +243,78 @@ const commonPageTests: CommonPageTest[] = [{ { page: { name: "Events", - href: "events", + href: "/events", expectedSelector: "h5.title", expectedText: "Events" } }, { drawerId: "apps", - pages: [{ - name: "Charts", - href: "apps/charts", - expectedSelector: "div.HelmCharts input", - }, - { - name: "Releases", - href: "apps/releases", - expectedSelector: "h5.title", - expectedText: "Releases" - }] + pages: [ + { + name: "Charts", + href: "/apps/charts", + expectedSelector: "div.HelmCharts input", + }, + { + name: "Releases", + href: "/apps/releases", + expectedSelector: "h5.title", + expectedText: "Releases" + }, + ] }, { drawerId: "users", - pages: [{ - name: "Service Accounts", - href: "service-accounts", - expectedSelector: "h5.title", - expectedText: "Service Accounts" - }, - { - name: "Roles", - href: "roles", - expectedSelector: "h5.title", - expectedText: "Roles" - }, - { - name: "Cluster Roles", - href: "cluster-roles", - expectedSelector: "h5.title", - expectedText: "Cluster Roles" - }, - { - name: "Role Bindings", - href: "role-bindings", - expectedSelector: "h5.title", - expectedText: "Role Bindings" - }, - { - name: "Cluster Role Bindings", - href: "cluster-role-bindings", - expectedSelector: "h5.title", - expectedText: "Cluster Role Bindings" - }, - { - name: "Pod Security Policies", - href: "pod-security-policies", - expectedSelector: "h5.title", - expectedText: "Pod Security Policies" - }] + pages: [ + { + name: "Service Accounts", + href: "/service-accounts", + expectedSelector: "h5.title", + expectedText: "Service Accounts" + }, + { + name: "Roles", + href: "/roles", + expectedSelector: "h5.title", + expectedText: "Roles" + }, + { + name: "Cluster Roles", + href: "/cluster-roles", + expectedSelector: "h5.title", + expectedText: "Cluster Roles" + }, + { + name: "Role Bindings", + href: "/role-bindings", + expectedSelector: "h5.title", + expectedText: "Role Bindings" + }, + { + name: "Cluster Role Bindings", + href: "/cluster-role-bindings", + expectedSelector: "h5.title", + expectedText: "Cluster Role Bindings" + }, + { + name: "Pod Security Policies", + href: "/pod-security-policies", + expectedSelector: "h5.title", + expectedText: "Pod Security Policies" + }, + ] }, { drawerId: "custom-resources", - pages: [{ - name: "Definitions", - href: "crd/definitions", - expectedSelector: "h5.title", - expectedText: "Custom Resources" - }] + pages: [ + { + name: "Definitions", + href: "/crd/definitions", + expectedSelector: "h5.title", + expectedText: "Custom Resources" + }, + ] }]; utils.describeIf(minikubeReady(TEST_NAMESPACE))("Minikube based tests", () => { @@ -321,7 +335,7 @@ utils.describeIf(minikubeReady(TEST_NAMESPACE))("Minikube based tests", () => { for (const test of commonPageTests) { if (isTopPageTest(test)) { const { href, expectedText, expectedSelector } = test.page; - const menuButton = await frame.waitForSelector(`a[href^="/${href}"]`); + const menuButton = await frame.waitForSelector(`a[href^="${href}"]`); await menuButton.click(); await frame.waitForSelector(`${expectedSelector} >> text='${expectedText}'`); diff --git a/src/common/catalog-entities/kubernetes-cluster.ts b/src/common/catalog-entities/kubernetes-cluster.ts index 31e0251966..8a3c3e0858 100644 --- a/src/common/catalog-entities/kubernetes-cluster.ts +++ b/src/common/catalog-entities/kubernetes-cluster.ts @@ -23,10 +23,11 @@ import { catalogCategoryRegistry } from "../catalog/catalog-category-registry"; import { CatalogEntity, CatalogEntityActionContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog"; import { clusterActivateHandler, clusterDisconnectHandler } from "../cluster-ipc"; import { ClusterStore } from "../cluster-store"; -import { requestMain } from "../ipc"; +import { broadcastMessage, requestMain } from "../ipc"; import { CatalogCategory, CatalogCategorySpec } from "../catalog"; import { app } from "electron"; import type { CatalogEntitySpec } from "../catalog/catalog-entity"; +import { IpcRendererNavigationEvents } from "../../renderer/navigation/events"; export interface KubernetesClusterPrometheusMetrics { address?: { @@ -114,7 +115,10 @@ export class KubernetesCluster extends CatalogEntity context.navigate(`/entity/${this.metadata.uid}/settings`) + onClick: () => broadcastMessage( + IpcRendererNavigationEvents.NAVIGATE_IN_APP, + `/entity/${this.metadata.uid}/settings`, + ), }); } diff --git a/src/common/catalog/catalog-entity.ts b/src/common/catalog/catalog-entity.ts index cbe59bae57..9d549f89de 100644 --- a/src/common/catalog/catalog-entity.ts +++ b/src/common/catalog/catalog-entity.ts @@ -172,7 +172,10 @@ export interface CatalogEntitySettingsMenu { } export interface CatalogEntityContextMenuContext { - navigate: (url: string) => void; + /** + * Navigate to the specified pathname + */ + navigate: (pathname: string) => void; menuItems: CatalogEntityContextMenu[]; } diff --git a/src/common/routes/cluster.ts b/src/common/routes/cluster.ts index 3a39ccd22a..2ba39ae53f 100644 --- a/src/common/routes/cluster.ts +++ b/src/common/routes/cluster.ts @@ -23,7 +23,7 @@ import type { RouteProps } from "react-router"; import { buildURL } from "../utils/buildUrl"; export const clusterRoute: RouteProps = { - path: "/cluster" + path: "/overview" }; export const clusterURL = buildURL(clusterRoute.path); diff --git a/src/renderer/components/app.tsx b/src/renderer/components/app.tsx index 59bf2ea065..d126e5260f 100755 --- a/src/renderer/components/app.tsx +++ b/src/renderer/components/app.tsx @@ -72,6 +72,7 @@ import type { ClusterId } from "../../common/cluster-types"; import { watchHistoryState } from "../remote-helpers/history-updater"; import { unmountComponentAtNode } from "react-dom"; import { PortForwardDialog } from "../port-forward"; +import { DeleteClusterDialog } from "./delete-cluster-dialog"; @observer export class App extends React.Component { @@ -230,6 +231,7 @@ export class App extends React.Component { + diff --git a/src/renderer/components/hotbar/hotbar-entity-icon.tsx b/src/renderer/components/hotbar/hotbar-entity-icon.tsx index 24405da556..b3ed9df942 100644 --- a/src/renderer/components/hotbar/hotbar-entity-icon.tsx +++ b/src/renderer/components/hotbar/hotbar-entity-icon.tsx @@ -44,20 +44,16 @@ interface Props extends DOMAttributes { @observer export class HotbarEntityIcon extends React.Component { - @observable private contextMenu: CatalogEntityContextMenuContext; + @observable private contextMenu: CatalogEntityContextMenuContext = { + menuItems: [], + navigate: (url: string) => navigate(url), + }; constructor(props: Props) { super(props); makeObservable(this); } - componentDidMount() { - this.contextMenu = { - menuItems: [], - navigate: (url: string) => navigate(url), - }; - } - get kindIcon() { const className = "badge"; const category = catalogCategoryRegistry.getCategoryForEntity(this.props.entity); diff --git a/src/renderer/components/layout/sidebar.tsx b/src/renderer/components/layout/sidebar.tsx index ed9b76a8d1..0cde9b3d62 100644 --- a/src/renderer/components/layout/sidebar.tsx +++ b/src/renderer/components/layout/sidebar.tsx @@ -42,6 +42,9 @@ import * as routes from "../../../common/routes"; import { Config } from "../+config"; import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; import { HotbarIcon } from "../hotbar/hotbar-icon"; +import { makeObservable, observable } from "mobx"; +import type { CatalogEntityContextMenuContext } from "../../../common/catalog"; +import { HotbarStore } from "../../../common/hotbar-store"; interface Props { className?: string; @@ -50,6 +53,15 @@ interface Props { @observer export class Sidebar extends React.Component { static displayName = "Sidebar"; + @observable private contextMenu: CatalogEntityContextMenuContext = { + menuItems: [], + navigate, + }; + + constructor(props: Props) { + super(props); + makeObservable(this); + } async componentDidMount() { crdStore.reloadAll(); @@ -194,6 +206,20 @@ export class Sidebar extends React.Component { src={spec.icon?.src} className="mr-5" onClick={() => navigate("/")} + menuItems={this.contextMenu.menuItems} + onMenuOpen={() => { + const hotbarStore = HotbarStore.getInstance(); + const isAddedToActive = HotbarStore.getInstance().isAddedToActive(this.clusterEntity); + const title = isAddedToActive + ? "Remove from Hotbar" + : "Add to Hotbar"; + const onClick = isAddedToActive + ? () => hotbarStore.removeFromHotbar(metadata.uid) + : () => hotbarStore.addToHotbar(this.clusterEntity); + + this.contextMenu.menuItems = [{ title, onClick }]; + this.clusterEntity.onContextMenuOpen(this.contextMenu); + }} />
{metadata.name} diff --git a/src/renderer/navigation/events.ts b/src/renderer/navigation/events.ts index eaf3741b34..6435bfde1f 100644 --- a/src/renderer/navigation/events.ts +++ b/src/renderer/navigation/events.ts @@ -63,6 +63,7 @@ function bindClusterManagerRouteEvents() { ipcRendererOn(IpcRendererNavigationEvents.NAVIGATE_IN_APP, (event, url: string) => { logger.info(`[IPC]: navigate to ${url}`, { currentLocation: location.href }); navigate(url); + window.focus(); // make sure that the main frame is focused }); }