diff --git a/integration/__tests__/app-preferences.tests.ts b/integration/__tests__/app-preferences.tests.ts index fe3817bf0c..4939686e41 100644 --- a/integration/__tests__/app-preferences.tests.ts +++ b/integration/__tests__/app-preferences.tests.ts @@ -25,16 +25,24 @@ TEST_NAMESPACE namespace. This is done to minimize destructive impact of the cluster tests on an existing minikube cluster and vice versa. */ -import type { Page } from "playwright"; +import type { ElectronApplication, Page } from "playwright"; import * as utils from "../helpers/utils"; describe("preferences page tests", () => { let window: Page, cleanup: () => Promise; beforeEach(async () => { - ({ window, cleanup } = await utils.start()); + let app: ElectronApplication; + + ({ window, cleanup, app } = await utils.start()); await utils.clickWelcomeButton(window); - await window.keyboard.press("Meta+,"); + + await app.evaluate(async ({ app }) => { + await app.applicationMenu + .getMenuItemById(process.platform === "darwin" ? "root" : "file") + .submenu.getMenuItemById("preferences") + .click(); + }); }, 10*60*1000); afterEach(async () => { diff --git a/integration/__tests__/command-palette.tests.ts b/integration/__tests__/command-palette.tests.ts index 1f2e49114b..da87b95a41 100644 --- a/integration/__tests__/command-palette.tests.ts +++ b/integration/__tests__/command-palette.tests.ts @@ -19,14 +19,14 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import type { Page } from "playwright"; +import type { ElectronApplication, Page } from "playwright"; import * as utils from "../helpers/utils"; describe("Lens command palette", () => { - let window: Page, cleanup: () => Promise; + let window: Page, cleanup: () => Promise, app: ElectronApplication; beforeEach(async () => { - ({ window, cleanup } = await utils.start()); + ({ window, cleanup, app } = await utils.start()); await utils.clickWelcomeButton(window); }, 10*60*1000); @@ -35,8 +35,13 @@ describe("Lens command palette", () => { }, 10*60*1000); describe("menu", () => { - it("opens command dialog from keyboard shortcut", async () => { - await window.keyboard.press("Meta+Shift+p"); + it("opens command dialog from menu", async () => { + await app.evaluate(async ({ app }) => { + await app.applicationMenu + .getMenuItemById("view") + .submenu.getMenuItemById("command-palette") + .click(); + }); await window.waitForSelector(".Select__option >> text=Hotbar: Switch"); }, 10*60*1000); }); diff --git a/src/common/utils/app-version.ts b/src/common/utils/app-version.ts index 84d4221498..f05877a7ec 100644 --- a/src/common/utils/app-version.ts +++ b/src/common/utils/app-version.ts @@ -33,8 +33,9 @@ export function getBundledKubectlVersion(): string { export async function getAppVersionFromProxyServer(proxyPort: number): Promise { const response = await requestPromise({ method: "GET", - uri: `http://localhost:${proxyPort}/version`, - resolveWithFullResponse: true + uri: `http://127.0.0.1:${proxyPort}/version`, + resolveWithFullResponse: true, + proxy: undefined, }); return JSON.parse(response.body).version; diff --git a/src/main/menu.ts b/src/main/menu.ts index 92782cd3d0..088f06e06e 100644 --- a/src/main/menu.ts +++ b/src/main/menu.ts @@ -19,19 +19,23 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { app, BrowserWindow, dialog, IpcMainEvent, Menu, MenuItem, MenuItemConstructorOptions, webContents, shell } from "electron"; +import { app, BrowserWindow, dialog, Menu, MenuItem, MenuItemConstructorOptions, webContents, shell } from "electron"; import { autorun } from "mobx"; import type { WindowManager } from "./window-manager"; -import { appName, isMac, isWindows, isTestEnv, docsUrl, supportUrl, productName } from "../common/vars"; +import { appName, isMac, isWindows, docsUrl, supportUrl, productName } from "../common/vars"; import { MenuRegistry } from "../extensions/registries/menu-registry"; import logger from "./logger"; import { exitApp } from "./exit-app"; -import { broadcastMessage, ipcMainOn } from "../common/ipc"; +import { broadcastMessage } from "../common/ipc"; import * as packageJson from "../../package.json"; import { preferencesURL, extensionsURL, addClusterURL, catalogURL, welcomeURL } from "../common/routes"; export type MenuTopId = "mac" | "file" | "edit" | "view" | "help"; +interface MenuItemsOpts extends MenuItemConstructorOptions { + submenu?: MenuItemConstructorOptions[]; +} + export function initMenu(windowManager: WindowManager) { return autorun(() => buildMenu(windowManager), { delay: 100 @@ -68,11 +72,13 @@ export function buildMenu(windowManager: WindowManager) { await windowManager.navigate(url); } - const macAppMenu: MenuItemConstructorOptions = { + const macAppMenu: MenuItemsOpts = { label: app.getName(), + id: "root", submenu: [ { label: `About ${productName}`, + id: "about", click(menuItem: MenuItem, browserWindow: BrowserWindow) { showAbout(browserWindow); } @@ -81,6 +87,7 @@ export function buildMenu(windowManager: WindowManager) { { label: "Preferences", accelerator: "CmdOrCtrl+,", + id: "preferences", click() { navigate(preferencesURL()); }, @@ -88,6 +95,7 @@ export function buildMenu(windowManager: WindowManager) { { label: "Extensions", accelerator: "CmdOrCtrl+Shift+E", + id: "extensions", click() { navigate(extensionsURL()); } @@ -102,18 +110,21 @@ export function buildMenu(windowManager: WindowManager) { { label: "Quit", accelerator: "Cmd+Q", + id: "quit", click() { exitApp(); } } ], }; - const fileMenu: MenuItemConstructorOptions = { + const fileMenu: MenuItemsOpts = { label: "File", + id: "file", submenu: [ { label: "Add Cluster", accelerator: "CmdOrCtrl+Shift+A", + id: "add-cluster", click() { navigate(addClusterURL()); } @@ -122,6 +133,7 @@ export function buildMenu(windowManager: WindowManager) { { type: "separator" }, { label: "Preferences", + id: "preferences", accelerator: "Ctrl+,", click() { navigate(preferencesURL()); @@ -145,6 +157,7 @@ export function buildMenu(windowManager: WindowManager) { { label: "Exit", accelerator: "Alt+F4", + id: "quit", click() { exitApp(); } @@ -152,8 +165,9 @@ export function buildMenu(windowManager: WindowManager) { ]) ], }; - const editMenu: MenuItemConstructorOptions = { + const editMenu: MenuItemsOpts = { label: "Edit", + id: "edit", submenu: [ { role: "undo" }, { role: "redo" }, @@ -166,12 +180,14 @@ export function buildMenu(windowManager: WindowManager) { { role: "selectAll" }, ] }; - const viewMenu: MenuItemConstructorOptions = { + const viewMenu: MenuItemsOpts = { label: "View", + id: "view", submenu: [ { label: "Catalog", accelerator: "Shift+CmdOrCtrl+C", + id: "catalog", click() { navigate(catalogURL()); } @@ -179,6 +195,7 @@ export function buildMenu(windowManager: WindowManager) { { label: "Command Palette...", accelerator: "Shift+CmdOrCtrl+P", + id: "command-palette", click() { broadcastMessage("command-palette:open"); } @@ -187,6 +204,7 @@ export function buildMenu(windowManager: WindowManager) { { label: "Back", accelerator: "CmdOrCtrl+[", + id: "go-back", click() { webContents.getAllWebContents().filter(wc => wc.getType() === "window").forEach(wc => wc.goBack()); } @@ -194,6 +212,7 @@ export function buildMenu(windowManager: WindowManager) { { label: "Forward", accelerator: "CmdOrCtrl+]", + id: "go-forward", click() { webContents.getAllWebContents().filter(wc => wc.getType() === "window").forEach(wc => wc.goForward()); } @@ -201,6 +220,7 @@ export function buildMenu(windowManager: WindowManager) { { label: "Reload", accelerator: "CmdOrCtrl+R", + id: "reload", click() { windowManager.reload(); } @@ -214,23 +234,27 @@ export function buildMenu(windowManager: WindowManager) { { role: "togglefullscreen" } ] }; - const helpMenu: MenuItemConstructorOptions = { + const helpMenu: MenuItemsOpts = { role: "help", + id: "help", submenu: [ { label: "Welcome", + id: "welcome", click() { navigate(welcomeURL()); }, }, { label: "Documentation", + id: "documentation", click: async () => { shell.openExternal(docsUrl); }, }, { label: "Support", + id: "support", click: async () => { shell.openExternal(supportUrl); }, @@ -238,6 +262,7 @@ export function buildMenu(windowManager: WindowManager) { ...ignoreOnMac([ { label: `About ${productName}`, + id: "about", click(menuItem: MenuItem, browserWindow: BrowserWindow) { showAbout(browserWindow); } @@ -246,69 +271,28 @@ export function buildMenu(windowManager: WindowManager) { ] }; // Prepare menu items order - const appMenu: Record = { - mac: macAppMenu, - file: fileMenu, - edit: editMenu, - view: viewMenu, - help: helpMenu, - }; + const appMenu = new Map([ + ["mac", macAppMenu], + ["file", fileMenu], + ["edit", editMenu], + ["view", viewMenu], + ["help", helpMenu], + ]); // Modify menu from extensions-api - MenuRegistry.getInstance().getItems().forEach(({ parentId, ...menuItem }) => { - try { - const topMenu = appMenu[parentId as MenuTopId].submenu as MenuItemConstructorOptions[]; + for (const { parentId, ...menuItem } of MenuRegistry.getInstance().getItems()) { + if (!appMenu.has(parentId)) { + logger.error(`[MENU]: cannot register menu item for parentId=${parentId}, parent item doesn't exist`, { menuItem }); - topMenu.push(menuItem); - } catch (err) { - logger.error(`[MENU]: can't register menu item, parentId=${parentId}`, { menuItem }); + continue; } - }); + + appMenu.get(parentId).submenu.push(menuItem); + } if (!isMac) { - delete appMenu.mac; + appMenu.delete("mac"); } - const menu = Menu.buildFromTemplate(Object.values(appMenu)); - - Menu.setApplicationMenu(menu); - - if (isTestEnv) { - // this is a workaround for the test environment (spectron) not being able to directly access - // the application menus (https://github.com/electron-userland/spectron/issues/21) - ipcMainOn("test-menu-item-click", (event: IpcMainEvent, ...names: string[]) => { - let menu: Menu = Menu.getApplicationMenu(); - const parentLabels: string[] = []; - let menuItem: MenuItem; - - for (const name of names) { - parentLabels.push(name); - menuItem = menu?.items?.find(item => item.label === name); - - if (!menuItem) { - break; - } - menu = menuItem.submenu; - } - - const menuPath: string = parentLabels.join(" -> "); - - if (!menuItem) { - logger.info(`[MENU:test-menu-item-click] Cannot find menu item ${menuPath}`); - - return; - } - - const { enabled, visible, click } = menuItem; - - if (enabled === false || visible === false || typeof click !== "function") { - logger.info(`[MENU:test-menu-item-click] Menu item ${menuPath} not clickable`); - - return; - } - - logger.info(`[MENU:test-menu-item-click] Menu item ${menuPath} click!`); - menuItem.click(); - }); - } + Menu.setApplicationMenu(Menu.buildFromTemplate([...appMenu.values()])); } diff --git a/src/renderer/keyboard-shortcuts/index.ts b/src/renderer/keyboard-shortcuts/index.ts deleted file mode 100644 index 7a3757ea7a..0000000000 --- a/src/renderer/keyboard-shortcuts/index.ts +++ /dev/null @@ -1,88 +0,0 @@ -/** - * Copyright (c) 2021 OpenLens Authors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to - * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -import { ipcRenderer } from "electron"; -import { navigate } from "../navigation"; - -/** - * The definition of a keyboard shortcut - */ -interface Shortcut { - code?: string; - key?: string; - metaKey?: boolean; - altKey?: boolean; - shiftKey?: boolean; - ctrlKey?: boolean; - action: () => void; -} - -const shortcuts: Shortcut[] = [ - { - key: "p", - metaKey: true, - shiftKey: true, - action: () => ipcRenderer.emit("command-palette:open"), - }, - { - code: "Comma", - metaKey: true, - action: () => navigate("/preferences"), - }, -]; - -function shortcutMatches(shortcut: Shortcut, event: KeyboardEvent): boolean { - if (typeof shortcut.metaKey === "boolean" && shortcut.metaKey !== event.metaKey) { - return false; - } - - if (typeof shortcut.altKey === "boolean" && shortcut.altKey !== event.altKey) { - return false; - } - - if (typeof shortcut.shiftKey === "boolean" && shortcut.shiftKey !== event.shiftKey) { - return false; - } - - if (typeof shortcut.ctrlKey === "boolean" && shortcut.ctrlKey !== event.ctrlKey) { - return false; - } - - if (typeof shortcut.code === "string" && shortcut.code !== event.code) { - return false; - } - - if (typeof shortcut.key === "string" && shortcut.key !== event.key) { - return false; - } - - return true; -} - -export function registerKeyboardShortcuts() { - window.addEventListener("keydown", event => { - for (const shortcut of shortcuts) { - if (shortcutMatches(shortcut, event)) { - shortcut.action(); - } - } - }); -} diff --git a/src/renderer/lens-app.tsx b/src/renderer/lens-app.tsx index 9bf6545c24..18be54e8e0 100644 --- a/src/renderer/lens-app.tsx +++ b/src/renderer/lens-app.tsx @@ -36,7 +36,6 @@ import { registerIpcListeners } from "./ipc"; import { ipcRenderer } from "electron"; import { IpcRendererNavigationEvents } from "./navigation/events"; import { catalogEntityRegistry } from "./api/catalog-entity-registry"; -import { registerKeyboardShortcuts } from "./keyboard-shortcuts"; import logger from "../common/logger"; import { unmountComponentAtNode } from "react-dom"; @@ -51,7 +50,6 @@ export class LensApp extends React.Component { window.addEventListener("offline", () => broadcastMessage("network:offline")); window.addEventListener("online", () => broadcastMessage("network:online")); - registerKeyboardShortcuts(); registerIpcListeners(); window.onbeforeunload = () => {