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

Add context menu into sidebar (#4044)

This commit is contained in:
Sebastian Malton 2021-10-19 09:19:25 -04:00 committed by GitHub
parent e21888c62c
commit 125a073007
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 243 additions and 197 deletions

View File

@ -36,7 +36,7 @@ function getSidebarSelectors(itemId: string) {
return { return {
expandSubMenu: `${root} .nav-item`, 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[] = [{ const commonPageTests: CommonPageTest[] = [{
page: { page: {
name: "Cluster", name: "Cluster",
href: "cluster", href: "/overview",
expectedSelector: "div.ClusterOverview div.label", expectedSelector: "div.ClusterOverview div.label",
expectedText: "CPU" expectedText: "CPU"
} }
@ -81,153 +81,161 @@ const commonPageTests: CommonPageTest[] = [{
{ {
page: { page: {
name: "Nodes", name: "Nodes",
href: "nodes", href: "/nodes",
expectedSelector: "h5.title", expectedSelector: "h5.title",
expectedText: "Nodes" expectedText: "Nodes"
} }
}, },
{ {
drawerId: "workloads", drawerId: "workloads",
pages: [{ pages: [
name: "Overview", {
href: "workloads", name: "Overview",
expectedSelector: "h5.box", href: "/workloads",
expectedText: "Overview" expectedSelector: "h5.box",
}, expectedText: "Overview"
{ },
name: "Pods", {
href: "pods", name: "Pods",
expectedSelector: "h5.title", href: "/pods",
expectedText: "Pods" expectedSelector: "h5.title",
}, expectedText: "Pods"
{ },
name: "Deployments", {
href: "deployments", name: "Deployments",
expectedSelector: "h5.title", href: "/deployments",
expectedText: "Deployments" expectedSelector: "h5.title",
}, expectedText: "Deployments"
{ },
name: "DaemonSets", {
href: "daemonsets", name: "DaemonSets",
expectedSelector: "h5.title", href: "/daemonsets",
expectedText: "Daemon Sets" expectedSelector: "h5.title",
}, expectedText: "Daemon Sets"
{ },
name: "StatefulSets", {
href: "statefulsets", name: "StatefulSets",
expectedSelector: "h5.title", href: "/statefulsets",
expectedText: "Stateful Sets" expectedSelector: "h5.title",
}, expectedText: "Stateful Sets"
{ },
name: "ReplicaSets", {
href: "replicasets", name: "ReplicaSets",
expectedSelector: "h5.title", href: "/replicasets",
expectedText: "Replica Sets" expectedSelector: "h5.title",
}, expectedText: "Replica Sets"
{ },
name: "Jobs", {
href: "jobs", name: "Jobs",
expectedSelector: "h5.title", href: "/jobs",
expectedText: "Jobs" expectedSelector: "h5.title",
}, expectedText: "Jobs"
{ },
name: "CronJobs", {
href: "cronjobs", name: "CronJobs",
expectedSelector: "h5.title", href: "/cronjobs",
expectedText: "Cron Jobs" expectedSelector: "h5.title",
}] expectedText: "Cron Jobs"
},
]
}, },
{ {
drawerId: "config", drawerId: "config",
pages: [{ pages: [
name: "ConfigMaps", {
href: "configmaps", name: "ConfigMaps",
expectedSelector: "h5.title", href: "/configmaps",
expectedText: "Config Maps" expectedSelector: "h5.title",
}, expectedText: "Config Maps"
{ },
name: "Secrets", {
href: "secrets", name: "Secrets",
expectedSelector: "h5.title", href: "/secrets",
expectedText: "Secrets" expectedSelector: "h5.title",
}, expectedText: "Secrets"
{ },
name: "Resource Quotas", {
href: "resourcequotas", name: "Resource Quotas",
expectedSelector: "h5.title", href: "/resourcequotas",
expectedText: "Resource Quotas" expectedSelector: "h5.title",
}, expectedText: "Resource Quotas"
{ },
name: "Limit Ranges", {
href: "limitranges", name: "Limit Ranges",
expectedSelector: "h5.title", href: "/limitranges",
expectedText: "Limit Ranges" expectedSelector: "h5.title",
}, expectedText: "Limit Ranges"
{ },
name: "HPA", {
href: "hpa", name: "HPA",
expectedSelector: "h5.title", href: "/hpa",
expectedText: "Horizontal Pod Autoscalers" expectedSelector: "h5.title",
}, expectedText: "Horizontal Pod Autoscalers"
{ },
name: "Pod Disruption Budgets", {
href: "poddisruptionbudgets", name: "Pod Disruption Budgets",
expectedSelector: "h5.title", href: "/poddisruptionbudgets",
expectedText: "Pod Disruption Budgets" expectedSelector: "h5.title",
}] expectedText: "Pod Disruption Budgets"
},
]
}, },
{ {
drawerId: "networks", drawerId: "networks",
pages: [{ pages: [
name: "Services", {
href: "services", name: "Services",
expectedSelector: "h5.title", href: "/services",
expectedText: "Services" expectedSelector: "h5.title",
}, expectedText: "Services"
{ },
name: "Endpoints", {
href: "endpoints", name: "Endpoints",
expectedSelector: "h5.title", href: "/endpoints",
expectedText: "Endpoints" expectedSelector: "h5.title",
}, expectedText: "Endpoints"
{ },
name: "Ingresses", {
href: "ingresses", name: "Ingresses",
expectedSelector: "h5.title", href: "/ingresses",
expectedText: "Ingresses" expectedSelector: "h5.title",
}, expectedText: "Ingresses"
{ },
name: "Network Policies", {
href: "network-policies", name: "Network Policies",
expectedSelector: "h5.title", href: "/network-policies",
expectedText: "Network Policies" expectedSelector: "h5.title",
}] expectedText: "Network Policies"
},
]
}, },
{ {
drawerId: "storage", drawerId: "storage",
pages: [{ pages: [
name: "Persistent Volume Claims", {
href: "persistent-volume-claims", name: "Persistent Volume Claims",
expectedSelector: "h5.title", href: "/persistent-volume-claims",
expectedText: "Persistent Volume Claims" expectedSelector: "h5.title",
}, expectedText: "Persistent Volume Claims"
{ },
name: "Persistent Volumes", {
href: "persistent-volumes", name: "Persistent Volumes",
expectedSelector: "h5.title", href: "/persistent-volumes",
expectedText: "Persistent Volumes" expectedSelector: "h5.title",
}, expectedText: "Persistent Volumes"
{ },
name: "Storage Classes", {
href: "storage-classes", name: "Storage Classes",
expectedSelector: "h5.title", href: "/storage-classes",
expectedText: "Storage Classes" expectedSelector: "h5.title",
}] expectedText: "Storage Classes"
},
]
}, },
{ {
page: { page: {
name: "Namespaces", name: "Namespaces",
href: "namespaces", href: "/namespaces",
expectedSelector: "h5.title", expectedSelector: "h5.title",
expectedText: "Namespaces" expectedText: "Namespaces"
} }
@ -235,72 +243,78 @@ const commonPageTests: CommonPageTest[] = [{
{ {
page: { page: {
name: "Events", name: "Events",
href: "events", href: "/events",
expectedSelector: "h5.title", expectedSelector: "h5.title",
expectedText: "Events" expectedText: "Events"
} }
}, },
{ {
drawerId: "apps", drawerId: "apps",
pages: [{ pages: [
name: "Charts", {
href: "apps/charts", name: "Charts",
expectedSelector: "div.HelmCharts input", href: "/apps/charts",
}, expectedSelector: "div.HelmCharts input",
{ },
name: "Releases", {
href: "apps/releases", name: "Releases",
expectedSelector: "h5.title", href: "/apps/releases",
expectedText: "Releases" expectedSelector: "h5.title",
}] expectedText: "Releases"
},
]
}, },
{ {
drawerId: "users", drawerId: "users",
pages: [{ pages: [
name: "Service Accounts", {
href: "service-accounts", name: "Service Accounts",
expectedSelector: "h5.title", href: "/service-accounts",
expectedText: "Service Accounts" expectedSelector: "h5.title",
}, expectedText: "Service Accounts"
{ },
name: "Roles", {
href: "roles", name: "Roles",
expectedSelector: "h5.title", href: "/roles",
expectedText: "Roles" expectedSelector: "h5.title",
}, expectedText: "Roles"
{ },
name: "Cluster Roles", {
href: "cluster-roles", name: "Cluster Roles",
expectedSelector: "h5.title", href: "/cluster-roles",
expectedText: "Cluster Roles" expectedSelector: "h5.title",
}, expectedText: "Cluster Roles"
{ },
name: "Role Bindings", {
href: "role-bindings", name: "Role Bindings",
expectedSelector: "h5.title", href: "/role-bindings",
expectedText: "Role Bindings" expectedSelector: "h5.title",
}, expectedText: "Role Bindings"
{ },
name: "Cluster Role Bindings", {
href: "cluster-role-bindings", name: "Cluster Role Bindings",
expectedSelector: "h5.title", href: "/cluster-role-bindings",
expectedText: "Cluster Role Bindings" expectedSelector: "h5.title",
}, expectedText: "Cluster Role Bindings"
{ },
name: "Pod Security Policies", {
href: "pod-security-policies", name: "Pod Security Policies",
expectedSelector: "h5.title", href: "/pod-security-policies",
expectedText: "Pod Security Policies" expectedSelector: "h5.title",
}] expectedText: "Pod Security Policies"
},
]
}, },
{ {
drawerId: "custom-resources", drawerId: "custom-resources",
pages: [{ pages: [
name: "Definitions", {
href: "crd/definitions", name: "Definitions",
expectedSelector: "h5.title", href: "/crd/definitions",
expectedText: "Custom Resources" expectedSelector: "h5.title",
}] expectedText: "Custom Resources"
},
]
}]; }];
utils.describeIf(minikubeReady(TEST_NAMESPACE))("Minikube based tests", () => { 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) { for (const test of commonPageTests) {
if (isTopPageTest(test)) { if (isTopPageTest(test)) {
const { href, expectedText, expectedSelector } = test.page; 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 menuButton.click();
await frame.waitForSelector(`${expectedSelector} >> text='${expectedText}'`); await frame.waitForSelector(`${expectedSelector} >> text='${expectedText}'`);

View File

@ -23,10 +23,11 @@ import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
import { CatalogEntity, CatalogEntityActionContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog"; import { CatalogEntity, CatalogEntityActionContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog";
import { clusterActivateHandler, clusterDisconnectHandler } from "../cluster-ipc"; import { clusterActivateHandler, clusterDisconnectHandler } from "../cluster-ipc";
import { ClusterStore } from "../cluster-store"; import { ClusterStore } from "../cluster-store";
import { requestMain } from "../ipc"; import { broadcastMessage, requestMain } from "../ipc";
import { CatalogCategory, CatalogCategorySpec } from "../catalog"; import { CatalogCategory, CatalogCategorySpec } from "../catalog";
import { app } from "electron"; import { app } from "electron";
import type { CatalogEntitySpec } from "../catalog/catalog-entity"; import type { CatalogEntitySpec } from "../catalog/catalog-entity";
import { IpcRendererNavigationEvents } from "../../renderer/navigation/events";
export interface KubernetesClusterPrometheusMetrics { export interface KubernetesClusterPrometheusMetrics {
address?: { address?: {
@ -114,7 +115,10 @@ export class KubernetesCluster extends CatalogEntity<KubernetesClusterMetadata,
context.menuItems.push({ context.menuItems.push({
title: "Settings", title: "Settings",
icon: "edit", icon: "edit",
onClick: () => context.navigate(`/entity/${this.metadata.uid}/settings`) onClick: () => broadcastMessage(
IpcRendererNavigationEvents.NAVIGATE_IN_APP,
`/entity/${this.metadata.uid}/settings`,
),
}); });
} }

View File

@ -172,7 +172,10 @@ export interface CatalogEntitySettingsMenu {
} }
export interface CatalogEntityContextMenuContext { export interface CatalogEntityContextMenuContext {
navigate: (url: string) => void; /**
* Navigate to the specified pathname
*/
navigate: (pathname: string) => void;
menuItems: CatalogEntityContextMenu[]; menuItems: CatalogEntityContextMenu[];
} }

View File

@ -23,7 +23,7 @@ import type { RouteProps } from "react-router";
import { buildURL } from "../utils/buildUrl"; import { buildURL } from "../utils/buildUrl";
export const clusterRoute: RouteProps = { export const clusterRoute: RouteProps = {
path: "/cluster" path: "/overview"
}; };
export const clusterURL = buildURL(clusterRoute.path); export const clusterURL = buildURL(clusterRoute.path);

View File

@ -72,6 +72,7 @@ import type { ClusterId } from "../../common/cluster-types";
import { watchHistoryState } from "../remote-helpers/history-updater"; import { watchHistoryState } from "../remote-helpers/history-updater";
import { unmountComponentAtNode } from "react-dom"; import { unmountComponentAtNode } from "react-dom";
import { PortForwardDialog } from "../port-forward"; import { PortForwardDialog } from "../port-forward";
import { DeleteClusterDialog } from "./delete-cluster-dialog";
@observer @observer
export class App extends React.Component { export class App extends React.Component {
@ -230,6 +231,7 @@ export class App extends React.Component {
<ReplicaSetScaleDialog/> <ReplicaSetScaleDialog/>
<CronJobTriggerDialog/> <CronJobTriggerDialog/>
<PortForwardDialog/> <PortForwardDialog/>
<DeleteClusterDialog/>
<CommandContainer clusterId={App.clusterId}/> <CommandContainer clusterId={App.clusterId}/>
</ErrorBoundary> </ErrorBoundary>
</Router> </Router>

View File

@ -44,20 +44,16 @@ interface Props extends DOMAttributes<HTMLElement> {
@observer @observer
export class HotbarEntityIcon extends React.Component<Props> { export class HotbarEntityIcon extends React.Component<Props> {
@observable private contextMenu: CatalogEntityContextMenuContext; @observable private contextMenu: CatalogEntityContextMenuContext = {
menuItems: [],
navigate: (url: string) => navigate(url),
};
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
makeObservable(this); makeObservable(this);
} }
componentDidMount() {
this.contextMenu = {
menuItems: [],
navigate: (url: string) => navigate(url),
};
}
get kindIcon() { get kindIcon() {
const className = "badge"; const className = "badge";
const category = catalogCategoryRegistry.getCategoryForEntity(this.props.entity); const category = catalogCategoryRegistry.getCategoryForEntity(this.props.entity);

View File

@ -42,6 +42,9 @@ import * as routes from "../../../common/routes";
import { Config } from "../+config"; import { Config } from "../+config";
import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
import { HotbarIcon } from "../hotbar/hotbar-icon"; 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 { interface Props {
className?: string; className?: string;
@ -50,6 +53,15 @@ interface Props {
@observer @observer
export class Sidebar extends React.Component<Props> { export class Sidebar extends React.Component<Props> {
static displayName = "Sidebar"; static displayName = "Sidebar";
@observable private contextMenu: CatalogEntityContextMenuContext = {
menuItems: [],
navigate,
};
constructor(props: Props) {
super(props);
makeObservable(this);
}
async componentDidMount() { async componentDidMount() {
crdStore.reloadAll(); crdStore.reloadAll();
@ -194,6 +206,20 @@ export class Sidebar extends React.Component<Props> {
src={spec.icon?.src} src={spec.icon?.src}
className="mr-5" className="mr-5"
onClick={() => navigate("/")} 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);
}}
/> />
<div className={styles.clusterName}> <div className={styles.clusterName}>
{metadata.name} {metadata.name}

View File

@ -63,6 +63,7 @@ function bindClusterManagerRouteEvents() {
ipcRendererOn(IpcRendererNavigationEvents.NAVIGATE_IN_APP, (event, url: string) => { ipcRendererOn(IpcRendererNavigationEvents.NAVIGATE_IN_APP, (event, url: string) => {
logger.info(`[IPC]: navigate to ${url}`, { currentLocation: location.href }); logger.info(`[IPC]: navigate to ${url}`, { currentLocation: location.href });
navigate(url); navigate(url);
window.focus(); // make sure that the main frame is focused
}); });
} }