diff --git a/src/common/k8s-api/endpoints/custom-resources/custom-resources.injectable.ts b/src/common/k8s-api/endpoints/custom-resources/custom-resources.injectable.ts new file mode 100644 index 0000000000..4a83b7de70 --- /dev/null +++ b/src/common/k8s-api/endpoints/custom-resources/custom-resources.injectable.ts @@ -0,0 +1,32 @@ +/** + * 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 { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import { crdStore } from "../../../../renderer/components/+custom-resources/crd.store"; + +const customResourceDefinitionsInjectable = getInjectable({ + instantiate: () => computed(() => [...crdStore.items]), + + lifecycle: lifecycleEnum.singleton, +}); + +export default customResourceDefinitionsInjectable; diff --git a/src/common/utils/iter.ts b/src/common/utils/iter.ts index 6271a05969..8ba606a881 100644 --- a/src/common/utils/iter.ts +++ b/src/common/utils/iter.ts @@ -201,3 +201,18 @@ export function every(src: Iterable, fn: (val: T) => any): boolean { return true; } + +/** + * Produce a new iterator that drains the first and then the second + * @param first The first iterable to iterate through + * @param second The second iterable to iterate through + */ +export function* chain(first: Iterable, second: Iterable): IterableIterator { + for (const t of first) { + yield t; + } + + for (const t of second) { + yield t; + } +} diff --git a/src/extensions/__tests__/extension-discovery.test.ts b/src/extensions/__tests__/extension-discovery.test.ts index e4ea1c4451..b39a0a0dfa 100644 --- a/src/extensions/__tests__/extension-discovery.test.ts +++ b/src/extensions/__tests__/extension-discovery.test.ts @@ -19,7 +19,6 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import mockFs from "mock-fs"; import { watch } from "chokidar"; import { ExtensionsStore } from "../extensions-store"; import path from "path"; @@ -30,6 +29,7 @@ import { AppPaths } from "../../common/app-paths"; import type { ExtensionLoader } from "../extension-loader"; import extensionLoaderInjectable from "../extension-loader/extension-loader.injectable"; import { getDiForUnitTesting } from "../getDiForUnitTesting"; +import * as fse from "fs-extra"; jest.setTimeout(60_000); @@ -43,6 +43,7 @@ jest.mock("../extension-installer", () => ({ installPackage: jest.fn(), }, })); +jest.mock("fs-extra"); jest.mock("electron", () => ({ app: { getVersion: () => "99.99.99", @@ -63,6 +64,7 @@ AppPaths.init(); console = new Console(process.stdout, process.stderr); // fix mockFS const mockedWatch = watch as jest.MockedFunction; +const mockedFse = fse as jest.Mocked; describe("ExtensionDiscovery", () => { let extensionLoader: ExtensionLoader; @@ -77,62 +79,59 @@ describe("ExtensionDiscovery", () => { extensionLoader = di.inject(extensionLoaderInjectable); }); - describe("with mockFs", () => { - beforeEach(() => { - mockFs({ - [`${os.homedir()}/.k8slens/extensions/my-extension/package.json`]: JSON.stringify({ - name: "my-extension", - }), - }); - }); + it("emits add for added extension", async (done) => { + let addHandler: (filePath: string) => void; - afterEach(() => { - mockFs.restore(); - }); + mockedFse.readJson.mockImplementation((p) => { + expect(p).toBe(path.join(os.homedir(), ".k8slens/extensions/my-extension/package.json")); - it("emits add for added extension", async (done) => { - let addHandler: (filePath: string) => void; - - const mockWatchInstance: any = { - on: jest.fn((event: string, handler: typeof addHandler) => { - if (event === "add") { - addHandler = handler; - } - - return mockWatchInstance; - }), + return { + name: "my-extension", + version: "1.0.0", }; - - mockedWatch.mockImplementationOnce(() => - (mockWatchInstance) as any, - ); - - const extensionDiscovery = ExtensionDiscovery.createInstance( - extensionLoader, - ); - - // Need to force isLoaded to be true so that the file watching is started - extensionDiscovery.isLoaded = true; - - await extensionDiscovery.watchExtensions(); - - extensionDiscovery.events.on("add", extension => { - expect(extension).toEqual({ - absolutePath: expect.any(String), - id: path.normalize("node_modules/my-extension/package.json"), - isBundled: false, - isEnabled: false, - isCompatible: false, - manifest: { - name: "my-extension", - }, - manifestPath: path.normalize("node_modules/my-extension/package.json"), - }); - done(); - }); - - addHandler(path.join(extensionDiscovery.localFolderPath, "/my-extension/package.json")); }); + + mockedFse.pathExists.mockImplementation(() => true); + + const mockWatchInstance: any = { + on: jest.fn((event: string, handler: typeof addHandler) => { + if (event === "add") { + addHandler = handler; + } + + return mockWatchInstance; + }), + }; + + mockedWatch.mockImplementationOnce(() => + (mockWatchInstance) as any, + ); + const extensionDiscovery = ExtensionDiscovery.createInstance( + extensionLoader, + ); + + // Need to force isLoaded to be true so that the file watching is started + extensionDiscovery.isLoaded = true; + + await extensionDiscovery.watchExtensions(); + + extensionDiscovery.events.on("add", extension => { + expect(extension).toEqual({ + absolutePath: expect.any(String), + id: path.normalize("node_modules/my-extension/package.json"), + isBundled: false, + isEnabled: false, + isCompatible: false, + manifest: { + name: "my-extension", + version: "1.0.0", + }, + manifestPath: path.normalize("node_modules/my-extension/package.json"), + }); + done(); + }); + + addHandler(path.join(extensionDiscovery.localFolderPath, "/my-extension/package.json")); }); it("doesn't emit add for added file under extension", async done => { @@ -149,7 +148,7 @@ describe("ExtensionDiscovery", () => { }; mockedWatch.mockImplementationOnce(() => - (mockWatchInstance) as any, + (mockWatchInstance) as any, ); const extensionDiscovery = ExtensionDiscovery.createInstance( extensionLoader, @@ -172,3 +171,4 @@ describe("ExtensionDiscovery", () => { }, 10); }); }); + diff --git a/src/extensions/extension-loader/extension-loader.ts b/src/extensions/extension-loader/extension-loader.ts index 00fb397079..33a716e03a 100644 --- a/src/extensions/extension-loader/extension-loader.ts +++ b/src/extensions/extension-loader/extension-loader.ts @@ -262,7 +262,6 @@ export class ExtensionLoader { registries.AppPreferenceRegistry.getInstance().add(extension.appPreferences), registries.EntitySettingRegistry.getInstance().add(extension.entitySettings), registries.StatusBarRegistry.getInstance().add(extension.statusBarItems), - registries.CommandRegistry.getInstance().add(extension.commands), registries.CatalogEntityDetailRegistry.getInstance().add(extension.catalogEntityDetailItems), ]; @@ -293,7 +292,6 @@ export class ExtensionLoader { registries.KubeObjectDetailRegistry.getInstance().add(extension.kubeObjectDetailItems), registries.KubeObjectStatusRegistry.getInstance().add(extension.kubeObjectStatusTexts), registries.WorkloadsOverviewDetailRegistry.getInstance().add(extension.kubeWorkloadsOverviewItems), - registries.CommandRegistry.getInstance().add(extension.commands), ]; this.events.on("remove", (removedExtension: LensRendererExtension) => { diff --git a/src/extensions/lens-renderer-extension.ts b/src/extensions/lens-renderer-extension.ts index 9f7b947d82..718f25f7c5 100644 --- a/src/extensions/lens-renderer-extension.ts +++ b/src/extensions/lens-renderer-extension.ts @@ -30,6 +30,7 @@ import type { TopBarRegistration } from "../renderer/components/layout/top-bar/t import type { KubernetesCluster } from "../common/catalog-entities"; import type { WelcomeMenuRegistration } from "../renderer/components/+welcome/welcome-menu-items/welcome-menu-registration"; import type { WelcomeBannerRegistration } from "../renderer/components/+welcome/welcome-banner-items/welcome-banner-registration"; +import type { CommandRegistration } from "../renderer/components/command-palette/registered-commands/commands"; export class LensRendererExtension extends LensExtension { globalPages: registries.PageRegistration[] = []; @@ -42,7 +43,7 @@ export class LensRendererExtension extends LensExtension { kubeObjectDetailItems: registries.KubeObjectDetailRegistration[] = []; kubeObjectMenuItems: registries.KubeObjectMenuRegistration[] = []; kubeWorkloadsOverviewItems: registries.WorkloadsOverviewDetailRegistration[] = []; - commands: registries.CommandRegistration[] = []; + commands: CommandRegistration[] = []; welcomeMenus: WelcomeMenuRegistration[] = []; welcomeBanners: WelcomeBannerRegistration[] = []; catalogEntityDetailItems: registries.CatalogEntityDetailRegistration[] = []; diff --git a/src/extensions/registries/index.ts b/src/extensions/registries/index.ts index 76f6c05d11..e8c9930176 100644 --- a/src/extensions/registries/index.ts +++ b/src/extensions/registries/index.ts @@ -28,7 +28,6 @@ export * from "./status-bar-registry"; export * from "./kube-object-detail-registry"; export * from "./kube-object-menu-registry"; export * from "./kube-object-status-registry"; -export * from "./command-registry"; export * from "./entity-setting-registry"; export * from "./catalog-entity-detail-registry"; export * from "./workloads-overview-detail-registry"; diff --git a/src/main/__test__/kubeconfig-manager.test.ts b/src/main/__test__/kubeconfig-manager.test.ts index 081beed63e..641881c38b 100644 --- a/src/main/__test__/kubeconfig-manager.test.ts +++ b/src/main/__test__/kubeconfig-manager.test.ts @@ -81,7 +81,7 @@ describe("kubeconfig manager tests", () => { let contextHandler: ContextHandler; beforeEach(() => { - const mockOpts = { + mockFs({ "minikube-config.yml": JSON.stringify({ apiVersion: "v1", clusters: [{ @@ -103,9 +103,7 @@ describe("kubeconfig manager tests", () => { kind: "Config", preferences: {}, }), - }; - - mockFs(mockOpts); + }); cluster = new Cluster({ id: "foo", diff --git a/src/main/getDi.ts b/src/main/getDi.ts index 2b59923a6c..c5fad1d470 100644 --- a/src/main/getDi.ts +++ b/src/main/getDi.ts @@ -24,11 +24,15 @@ import { createContainer } from "@ogre-tools/injectable"; export const getDi = () => createContainer( getRequireContextForMainCode, + getRequireContextForCommonCode, getRequireContextForCommonExtensionCode, ); const getRequireContextForMainCode = () => require.context("./", true, /\.injectable\.(ts|tsx)$/); +const getRequireContextForCommonCode = () => + require.context("../common", true, /\.injectable\.(ts|tsx)$/); + const getRequireContextForCommonExtensionCode = () => require.context("../extensions", true, /\.injectable\.(ts|tsx)$/); diff --git a/src/main/menu/menu.ts b/src/main/menu/menu.ts index 294c31e0cc..512d4e916d 100644 --- a/src/main/menu/menu.ts +++ b/src/main/menu/menu.ts @@ -216,8 +216,14 @@ export function getAppMenu( label: "Command Palette...", accelerator: "Shift+CmdOrCtrl+P", id: "command-palette", - click() { - broadcastMessage("command-palette:open"); + click(_m, _b, event) { + /** + * Don't broadcast unless it was triggered by menu iteration so that + * there aren't double events in renderer + */ + if (!event?.triggeredByAccelerator) { + broadcastMessage("command-palette:open"); + } }, }, { type: "separator" }, diff --git a/src/renderer/api/catalog-entity-registry.ts b/src/renderer/api/catalog-entity-registry.ts index 735e66a7a3..af2309bc4c 100644 --- a/src/renderer/api/catalog-entity-registry.ts +++ b/src/renderer/api/catalog-entity-registry.ts @@ -32,6 +32,7 @@ import { CatalogRunEvent } from "../../common/catalog/catalog-run-event"; import { ipcRenderer } from "electron"; import { CatalogIpcEvents } from "../../common/ipc/catalog"; import { navigate } from "../navigation"; +import { isMainFrame } from "process"; export type EntityFilter = (entity: CatalogEntity) => any; export type CatalogEntityOnBeforeRun = (event: CatalogRunEvent) => void | Promise; @@ -85,6 +86,16 @@ export class CatalogEntityRegistry { // Make sure that we get items ASAP and not the next time one of them changes ipcRenderer.send(CatalogIpcEvents.INIT); + + if (isMainFrame) { + ipcRendererOn("catalog-entity:run", (event, id: string) => { + const entity = this.getById(id); + + if (entity) { + this.onRun(entity); + } + }); + } } @action updateItems(items: (CatalogEntityData & CatalogEntityKindData)[]) { diff --git a/src/main/catalog-sources/helpers/general-active-sync.ts b/src/renderer/api/helpers/general-active-sync.ts similarity index 82% rename from src/main/catalog-sources/helpers/general-active-sync.ts rename to src/renderer/api/helpers/general-active-sync.ts index e46e27f827..25442e60ab 100644 --- a/src/main/catalog-sources/helpers/general-active-sync.ts +++ b/src/renderer/api/helpers/general-active-sync.ts @@ -21,13 +21,14 @@ import { when } from "mobx"; import { catalogCategoryRegistry } from "../../../common/catalog"; -import { catalogEntityRegistry } from "../../../renderer/api/catalog-entity-registry"; -import { isActiveRoute } from "../../../renderer/navigation"; +import { catalogEntityRegistry } from "../catalog-entity-registry"; +import { isActiveRoute } from "../../navigation"; +import type { GeneralEntity } from "../../../common/catalog-entities"; export async function setEntityOnRouteMatch() { await when(() => catalogEntityRegistry.entities.size > 0); - const entities = catalogEntityRegistry.getItemsForCategory(catalogCategoryRegistry.getByName("General")); + const entities: GeneralEntity[] = catalogEntityRegistry.getItemsForCategory(catalogCategoryRegistry.getByName("General")); const activeEntity = entities.find(entity => isActiveRoute(entity.spec.path)); if (activeEntity) { diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx index 6e9e9fb471..210d78267b 100644 --- a/src/renderer/bootstrap.tsx +++ b/src/renderer/bootstrap.tsx @@ -49,7 +49,7 @@ import { SentryInit } from "../common/sentry"; import { TerminalStore } from "./components/dock/terminal.store"; import { AppPaths } from "../common/app-paths"; import { registerCustomThemes } from "./components/monaco-editor"; -import { getDi } from "./components/getDi"; +import { getDi } from "./getDi"; import { DiContextProvider } from "@ogre-tools/injectable-react"; import type { DependencyInjectionContainer } from "@ogre-tools/injectable"; import extensionLoaderInjectable from "../extensions/extension-loader/extension-loader.injectable"; @@ -102,9 +102,6 @@ export async function bootstrap(comp: () => Promise, di: Dependenc logger.info(`${logPrefix} initializing Registries`); initializers.initRegistries(); - logger.info(`${logPrefix} initializing CommandRegistry`); - initializers.initCommandRegistry(); - logger.info(`${logPrefix} initializing EntitySettingsRegistry`); initializers.initEntitySettingsRegistry(); diff --git a/src/renderer/components/+custom-resources/crd.store.ts b/src/renderer/components/+custom-resources/crd.store.ts index 2cdcbee55b..71e96afe8a 100644 --- a/src/renderer/components/+custom-resources/crd.store.ts +++ b/src/renderer/components/+custom-resources/crd.store.ts @@ -66,22 +66,15 @@ export class CRDStore extends KubeObjectStore { @computed get groups() { const groups: Record = {}; - return this.items.reduce((groups, crd) => { - const group = crd.getGroup(); + for (const crd of this.items) { + (groups[crd.getGroup()] ??= []).push(crd); + } - if (!groups[group]) groups[group] = []; - groups[group].push(crd); - - return groups; - }, groups); + return groups; } getByGroup(group: string, pluralName: string) { - const crdInGroup = this.groups[group]; - - if (!crdInGroup) return null; - - return crdInGroup.find(crd => crd.getPluralName() === pluralName); + return this.groups[group]?.find(crd => crd.getPluralName() === pluralName); } getByObject(obj: KubeObject) { diff --git a/src/renderer/components/activate-entity-command/activate-entity-command.tsx b/src/renderer/components/activate-entity-command/activate-entity-command.tsx index f0b06769d8..46d1a8db1f 100644 --- a/src/renderer/components/activate-entity-command/activate-entity-command.tsx +++ b/src/renderer/components/activate-entity-command/activate-entity-command.tsx @@ -22,6 +22,7 @@ import { computed } from "mobx"; import { observer } from "mobx-react"; import React from "react"; +import { broadcastMessage } from "../../../common/ipc"; import type { CatalogEntity } from "../../api/catalog-entity"; import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; import { CommandOverlay } from "../command-palette"; @@ -37,7 +38,7 @@ export class ActivateEntityCommand extends React.Component { } onSelect(entity: CatalogEntity): void { - catalogEntityRegistry.onRun(entity); + broadcastMessage("catalog-entity:run", entity.getId()); CommandOverlay.close(); } diff --git a/src/renderer/components/cluster-manager/cluster-manager.tsx b/src/renderer/components/cluster-manager/cluster-manager.tsx index 300c9b2eb4..95a21a747e 100644 --- a/src/renderer/components/cluster-manager/cluster-manager.tsx +++ b/src/renderer/components/cluster-manager/cluster-manager.tsx @@ -38,7 +38,7 @@ import * as routes from "../../../common/routes"; import { DeleteClusterDialog } from "../delete-cluster-dialog"; import { reaction } from "mobx"; import { navigation } from "../../navigation"; -import { setEntityOnRouteMatch } from "../../../main/catalog-sources/helpers/general-active-sync"; +import { setEntityOnRouteMatch } from "../../api/helpers/general-active-sync"; import { catalogURL, getPreviousTabUrl } from "../../../common/routes"; import { TopBar } from "../layout/top-bar/top-bar"; diff --git a/src/renderer/components/command-palette/command-container.tsx b/src/renderer/components/command-palette/command-container.tsx index 2d4a7b6a83..8450129574 100644 --- a/src/renderer/components/command-palette/command-container.tsx +++ b/src/renderer/components/command-palette/command-container.tsx @@ -21,20 +21,30 @@ import "./command-container.scss"; -import { observer } from "mobx-react"; +import { disposeOnUnmount, observer } from "mobx-react"; import React from "react"; import { Dialog } from "../dialog"; -import { ipcRendererOn } from "../../../common/ipc"; import { CommandDialog } from "./command-dialog"; import type { ClusterId } from "../../../common/cluster-types"; -import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; -import { CommandRegistration, CommandRegistry } from "../../../extensions/registries/command-registry"; import { CommandOverlay } from "./command-overlay"; +import { isMac } from "../../../common/vars"; +import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; +import { broadcastMessage, ipcRendererOn } from "../../../common/ipc"; +import { getMatchedClusterId } from "../../navigation"; +import type { Disposer } from "../../utils"; export interface CommandContainerProps { clusterId?: ClusterId; } +function addWindowEventListener(type: K, listener: (this: Window, ev: WindowEventMap[K]) => any, options?: boolean | AddEventListenerOptions): Disposer { + window.addEventListener(type, listener, options); + + return () => { + window.removeEventListener(type, listener); + }; +} + @observer export class CommandContainer extends React.Component { private escHandler(event: KeyboardEvent) { @@ -44,31 +54,39 @@ export class CommandContainer extends React.Component { } } - private findCommandById(commandId: string) { - return CommandRegistry.getInstance().getItems().find((command) => command.id === commandId); - } + handleCommandPalette = () => { + const clusterIsActive = getMatchedClusterId() !== undefined; - private runCommand(command: CommandRegistration) { - command.action({ - entity: catalogEntityRegistry.activeEntity, - }); + if (clusterIsActive) { + broadcastMessage(`command-palette:${catalogEntityRegistry.activeEntity.getId()}:open`); + } else { + CommandOverlay.open(); + } + }; + + onKeyboardShortcut(action: () => void) { + return ({ key, shiftKey, ctrlKey, altKey, metaKey }: KeyboardEvent) => { + const ctrlOrCmd = isMac ? metaKey && !ctrlKey : !metaKey && ctrlKey; + + if (key === "p" && shiftKey && ctrlOrCmd && !altKey) { + action(); + } + }; } componentDidMount() { - if (this.props.clusterId) { - ipcRendererOn(`command-palette:run-action:${this.props.clusterId}`, (event, commandId: string) => { - const command = this.findCommandById(commandId); + const action = this.props.clusterId + ? () => CommandOverlay.open() + : this.handleCommandPalette; + const ipcChannel = this.props.clusterId + ? `command-palette:${this.props.clusterId}:open` + : "command-palette:open"; - if (command) { - this.runCommand(command); - } - }); - } else { - ipcRendererOn("command-palette:open", () => { - CommandOverlay.open(); - }); - } - window.addEventListener("keyup", (e) => this.escHandler(e), true); + disposeOnUnmount(this, [ + ipcRendererOn(ipcChannel, action), + addWindowEventListener("keydown", this.onKeyboardShortcut(action)), + addWindowEventListener("keyup", (e) => this.escHandler(e), true), + ]); } render() { diff --git a/src/renderer/components/command-palette/command-dialog.tsx b/src/renderer/components/command-palette/command-dialog.tsx index 3761068260..4c495ee494 100644 --- a/src/renderer/components/command-palette/command-dialog.tsx +++ b/src/renderer/components/command-palette/command-dialog.tsx @@ -21,61 +21,31 @@ import { Select } from "../select"; -import { computed, makeObservable, observable } from "mobx"; +import type { IComputedValue } from "mobx"; import { observer } from "mobx-react"; -import React from "react"; -import { CommandRegistry } from "../../../extensions/registries/command-registry"; +import React, { useState } from "react"; import { CommandOverlay } from "./command-overlay"; -import { broadcastMessage } from "../../../common/ipc"; -import { navigate } from "../../navigation"; -import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; import type { CatalogEntity } from "../../../common/catalog"; -import { clusterViewURL } from "../../../common/routes"; +import { navigate } from "../../navigation"; +import { broadcastMessage } from "../../../common/ipc"; +import { IpcRendererNavigationEvents } from "../../navigation/events"; +import type { RegisteredCommand } from "./registered-commands/commands"; +import { iter } from "../../utils"; +import { orderBy } from "lodash"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import registeredCommandsInjectable from "./registered-commands/registered-commands.injectable"; +import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; -@observer -export class CommandDialog extends React.Component { - @observable menuIsOpen = true; - @observable searchValue: any = undefined; +interface Dependencies { + commands: IComputedValue>; + activeEntity?: CatalogEntity; +} - constructor(props: {}) { - super(props); - makeObservable(this); - } +const NonInjectedCommandDialog = observer(({ commands, activeEntity }: Dependencies) => { + const [searchValue, setSearchValue] = useState(""); - @computed get activeEntity(): CatalogEntity | undefined { - return catalogEntityRegistry.activeEntity; - } - - @computed get options() { - const registry = CommandRegistry.getInstance(); - - const context = { - entity: this.activeEntity, - }; - - return registry.getItems().filter((command) => { - if (command.scope === "entity" && !this.activeEntity) { - return false; - } - - try { - return command.isActive?.(context) ?? true; - } catch(e) { - console.error(e); - } - - return false; - }) - .map((command) => ({ - value: command.id, - label: command.title, - })) - .sort((a, b) => a.label > b.label ? 1 : -1); - } - - private onChange(value: string) { - const registry = CommandRegistry.getInstance(); - const command = registry.getItems().find((cmd) => cmd.id === value); + const executeAction = (commandId: string) => { + const command = commands.get().get(commandId); if (!command) { return; @@ -83,46 +53,73 @@ export class CommandDialog extends React.Component { try { CommandOverlay.close(); + command.action({ + entity: activeEntity, + navigate: (url, opts = {}) => { + const { forceRootFrame = false } = opts; - if (command.scope === "global") { - command.action({ - entity: this.activeEntity, - }); - } else if(this.activeEntity) { - navigate(clusterViewURL({ - params: { - clusterId: this.activeEntity.metadata.uid, - }, - })); - broadcastMessage(`command-palette:run-action:${this.activeEntity.metadata.uid}`, command.id); - } - } catch(error) { + if (forceRootFrame) { + broadcastMessage(IpcRendererNavigationEvents.NAVIGATE_IN_APP, url); + } else { + navigate(url); + } + }, + }); + } catch (error) { console.error("[COMMAND-DIALOG] failed to execute command", command.id, error); } - } + }; - render() { - return ( - executeAction(v.value)} + components={{ + DropdownIndicator: null, + IndicatorSeparator: null, + }} + menuIsOpen + options={options} + autoFocus={true} + escapeClearsValue={false} + data-test-id="command-palette-search" + placeholder="Type a command or search…" + onInputChange={(newValue, { action }) => { + if (action === "input-change") { + setSearchValue(newValue); + } + }} + inputValue={searchValue} + /> + ); +}); + +export const CommandDialog = withInjectables(NonInjectedCommandDialog, { + getProps: di => ({ + commands: di.inject(registeredCommandsInjectable), + // TODO: replace with injection + activeEntity: catalogEntityRegistry.activeEntity, + }), +}); diff --git a/src/extensions/registries/command-registry.ts b/src/renderer/components/command-palette/registered-commands/commands.d.ts similarity index 54% rename from src/extensions/registries/command-registry.ts rename to src/renderer/components/command-palette/registered-commands/commands.d.ts index 3916b0f721..9b90662255 100644 --- a/src/extensions/registries/command-registry.ts +++ b/src/renderer/components/command-palette/registered-commands/commands.d.ts @@ -19,34 +19,55 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -// Extensions API -> Commands - -import { BaseRegistry } from "./base-registry"; -import type { LensExtension } from "../lens-extension"; -import type { CatalogEntity } from "../../common/catalog"; +import type { CatalogEntity } from "../../../../common/catalog"; +/** + * The context given to commands when executed + */ export interface CommandContext { entity?: CatalogEntity; } +export interface CommandActionNavigateOptions { + /** + * If `true` then the navigate will only navigate on the root frame and not + * within a cluster + * @default false + */ + forceRootFrame?: boolean; +} + +export interface CommandActionContext extends CommandContext { + navigate: (url: string, opts?: CommandActionNavigateOptions) => void; +} + export interface CommandRegistration { + /** + * The ID of the command, must be globally unique + */ id: string; - title: string; - scope: "entity" | "global"; - action: (context: CommandContext) => void; + + /** + * The display name of the command in the command pallet + */ + title: string | ((context: CommandContext) => string); + + /** + * @deprecated use `isActive` instead since there is always an entity active + */ + scope?: "global" | "entity"; + + /** + * The function to run when this command is selected + */ + action: (context: CommandActionContext) => void; + + /** + * A function that determines if the command is active. + * + * @default () => true + */ isActive?: (context: CommandContext) => boolean; } -export class CommandRegistry extends BaseRegistry { - add(items: CommandRegistration | CommandRegistration[], extension?: LensExtension) { - const itemArray = [items].flat(); - - const newIds = itemArray.map((item) => item.id); - const currentIds = this.getItems().map((item) => item.id); - - const filteredIds = newIds.filter((id) => !currentIds.includes(id)); - const filteredItems = itemArray.filter((item) => filteredIds.includes(item.id)); - - return super.add(filteredItems, extension); - } -} +export type RegisteredCommand = Required>; diff --git a/src/renderer/components/command-palette/registered-commands/internal-commands.tsx b/src/renderer/components/command-palette/registered-commands/internal-commands.tsx new file mode 100644 index 0000000000..1b40f20f0e --- /dev/null +++ b/src/renderer/components/command-palette/registered-commands/internal-commands.tsx @@ -0,0 +1,215 @@ +/** + * 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 React from "react"; +import * as routes from "../../../../common/routes"; +import { EntitySettingRegistry } from "../../../../extensions/registries"; +import { CommandOverlay } from "../../../components/command-palette"; +import { createTerminalTab } from "../../../components/dock/terminal.store"; +import { HotbarAddCommand } from "../../../components/hotbar/hotbar-add-command"; +import { HotbarRemoveCommand } from "../../../components/hotbar/hotbar-remove-command"; +import { HotbarSwitchCommand } from "../../../components/hotbar/hotbar-switch-command"; +import { HotbarRenameCommand } from "../../../components/hotbar/hotbar-rename-command"; +import { ActivateEntityCommand } from "../../../components/activate-entity-command"; +import type { CommandContext, CommandRegistration } from "./commands"; + +export function isKubernetesClusterActive(context: CommandContext): boolean { + return context.entity?.kind === "KubernetesCluster"; +} + +export const internalCommands: CommandRegistration[] = [ + { + id: "app.showPreferences", + title: "Preferences: Open", + action: ({ navigate }) => navigate(routes.preferencesURL(), { + forceRootFrame: true, + }), + }, + { + id: "cluster.viewHelmCharts", + title: "Cluster: View Helm Charts", + isActive: isKubernetesClusterActive, + action: ({ navigate }) => navigate(routes.helmChartsURL()), + }, + { + id: "cluster.viewHelmReleases", + title: "Cluster: View Helm Releases", + isActive: isKubernetesClusterActive, + action: ({ navigate }) => navigate(routes.releaseURL()), + }, + { + id: "cluster.viewConfigMaps", + title: "Cluster: View ConfigMaps", + isActive: isKubernetesClusterActive, + action: ({ navigate }) => navigate(routes.configMapsURL()), + }, + { + id: "cluster.viewSecrets", + title: "Cluster: View Secrets", + isActive: isKubernetesClusterActive, + action: ({ navigate }) => navigate(routes.secretsURL()), + }, + { + id: "cluster.viewResourceQuotas", + title: "Cluster: View ResourceQuotas", + isActive: isKubernetesClusterActive, + action: ({ navigate }) => navigate(routes.resourceQuotaURL()), + }, + { + id: "cluster.viewLimitRanges", + title: "Cluster: View LimitRanges", + isActive: isKubernetesClusterActive, + action: ({ navigate }) => navigate(routes.limitRangeURL()), + }, + { + id: "cluster.viewHorizontalPodAutoscalers", + title: "Cluster: View HorizontalPodAutoscalers (HPA)", + isActive: isKubernetesClusterActive, + action: ({ navigate }) => navigate(routes.hpaURL()), + }, + { + id: "cluster.viewPodDisruptionBudget", + title: "Cluster: View PodDisruptionBudgets", + isActive: isKubernetesClusterActive, + action: ({ navigate }) => navigate(routes.pdbURL()), + }, + { + id: "cluster.viewServices", + title: "Cluster: View Services", + isActive: isKubernetesClusterActive, + action: ({ navigate }) => navigate(routes.servicesURL()), + }, + { + id: "cluster.viewEndpoints", + title: "Cluster: View Endpoints", + isActive: isKubernetesClusterActive, + action: ({ navigate }) => navigate(routes.endpointURL()), + }, + { + id: "cluster.viewIngresses", + title: "Cluster: View Ingresses", + isActive: isKubernetesClusterActive, + action: ({ navigate }) => navigate(routes.ingressURL()), + }, + { + id: "cluster.viewNetworkPolicies", + title: "Cluster: View NetworkPolicies", + isActive: isKubernetesClusterActive, + action: ({ navigate }) => navigate(routes.networkPoliciesURL()), + }, + { + id: "cluster.viewNodes", + title: "Cluster: View Nodes", + isActive: isKubernetesClusterActive, + action: ({ navigate }) => navigate(routes.nodesURL()), + }, + { + id: "cluster.viewPods", + title: "Cluster: View Pods", + isActive: isKubernetesClusterActive, + action: ({ navigate }) => navigate(routes.podsURL()), + }, + { + id: "cluster.viewDeployments", + title: "Cluster: View Deployments", + isActive: isKubernetesClusterActive, + action: ({ navigate }) => navigate(routes.deploymentsURL()), + }, + { + id: "cluster.viewDaemonSets", + title: "Cluster: View DaemonSets", + isActive: isKubernetesClusterActive, + action: ({ navigate }) => navigate(routes.daemonSetsURL()), + }, + { + id: "cluster.viewStatefulSets", + title: "Cluster: View StatefulSets", + isActive: isKubernetesClusterActive, + action: ({ navigate }) => navigate(routes.statefulSetsURL()), + }, + { + id: "cluster.viewJobs", + title: "Cluster: View Jobs", + isActive: isKubernetesClusterActive, + action: ({ navigate }) => navigate(routes.jobsURL()), + }, + { + id: "cluster.viewCronJobs", + title: "Cluster: View CronJobs", + isActive: isKubernetesClusterActive, + action: ({ navigate }) => navigate(routes.cronJobsURL()), + }, + { + id: "cluster.viewCustomResourceDefinitions", + title: "Cluster: View Custom Resource Definitions", + isActive: isKubernetesClusterActive, + action: ({ navigate }) => navigate(routes.crdURL()), + }, + { + id: "entity.viewSettings", + title: ({ entity }) => `${entity.kind}/${entity.getName()}: View Settings`, + action: ({ entity, navigate }) => navigate(`/entity/${entity.getId()}/settings`, { + forceRootFrame: true, + }), + isActive: ({ entity }) => { + if (!entity) { + return false; + } + + // TODO: replace with injection + const entries = EntitySettingRegistry.getInstance() + .getItemsForKind(entity.kind, entity.apiVersion, entity.metadata.source); + + return entries.length > 0; + }, + }, + { + id: "cluster.openTerminal", + title: "Cluster: Open terminal", + action: () => createTerminalTab(), + isActive: isKubernetesClusterActive, + }, + { + id: "hotbar.switchHotbar", + title: "Hotbar: Switch ...", + action: () => CommandOverlay.open(), + }, + { + id: "hotbar.addHotbar", + title: "Hotbar: Add Hotbar ...", + action: () => CommandOverlay.open(), + }, + { + id: "hotbar.removeHotbar", + title: "Hotbar: Remove Hotbar ...", + action: () => CommandOverlay.open(), + }, + { + id: "hotbar.renameHotbar", + title: "Hotbar: Rename Hotbar ...", + action: () => CommandOverlay.open(), + }, + { + id: "catalog.searchEntities", + title: "Catalog: Activate Entity ...", + action: () => CommandOverlay.open(), + }, +]; diff --git a/src/renderer/components/command-palette/registered-commands/registered-commands.injectable.ts b/src/renderer/components/command-palette/registered-commands/registered-commands.injectable.ts new file mode 100644 index 0000000000..0f263720d0 --- /dev/null +++ b/src/renderer/components/command-palette/registered-commands/registered-commands.injectable.ts @@ -0,0 +1,70 @@ +/** + * 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 { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { computed, IComputedValue } from "mobx"; +import type { CustomResourceDefinition } from "../../../../common/k8s-api/endpoints"; +import customResourceDefinitionsInjectable from "../../../../common/k8s-api/endpoints/custom-resources/custom-resources.injectable"; +import type { LensRendererExtension } from "../../../../extensions/lens-renderer-extension"; +import rendererExtensionsInjectable from "../../../../extensions/renderer-extensions.injectable"; +import { iter } from "../../../utils"; +import type { RegisteredCommand } from "./commands"; +import { internalCommands, isKubernetesClusterActive } from "./internal-commands"; + +interface Dependencies { + extensions: IComputedValue; + customResourceDefinitions: IComputedValue; +} + +const instantiateRegisteredCommands = ({ extensions, customResourceDefinitions }: Dependencies) => computed(() => { + const result = new Map(); + const commands = iter.chain( + internalCommands, + iter.chain( + iter.flatMap(extensions.get(), extension => extension.commands), + iter.map(customResourceDefinitions.get(), command => ({ + id: `cluster.view.${command.getResourceKind()}`, + title: `Cluster: View ${command.getResourceKind()}`, + isActive: isKubernetesClusterActive, + action: ({ navigate }) => navigate(command.getResourceUrl()), + })), + ), + ); + + for (const { scope, isActive = () => true, ...command } of commands) { + if (!result.has(command.id)) { + result.set(command.id, { ...command, isActive }); + } + } + + return result; +}); + +const registeredCommandsInjectable = getInjectable({ + instantiate: (di) => instantiateRegisteredCommands({ + extensions: di.inject(rendererExtensionsInjectable), + customResourceDefinitions: di.inject(customResourceDefinitionsInjectable), + }), + + lifecycle: lifecycleEnum.singleton, +}); + +export default registeredCommandsInjectable; diff --git a/src/renderer/components/input/input.tsx b/src/renderer/components/input/input.tsx index f6aea6529e..e9dbc48aad 100644 --- a/src/renderer/components/input/input.tsx +++ b/src/renderer/components/input/input.tsx @@ -103,6 +103,10 @@ export class Input extends React.Component { submitted: false, }; + componentWillUnmount(): void { + this.setDirtyOnChange.cancel(); + } + setValue(value = "") { if (value !== this.getValue()) { const nativeInputValueSetter = Object.getOwnPropertyDescriptor(this.input.constructor.prototype, "value").set; diff --git a/src/renderer/components/kube-object-menu/kube-object-menu.test.tsx b/src/renderer/components/kube-object-menu/kube-object-menu.test.tsx index 02042850d8..689c368c8d 100644 --- a/src/renderer/components/kube-object-menu/kube-object-menu.test.tsx +++ b/src/renderer/components/kube-object-menu/kube-object-menu.test.tsx @@ -280,5 +280,5 @@ const addDynamicMenuItem = ({ const kubeObjectMenuRegistry = di.inject(kubeObjectMenuRegistryInjectable); - kubeObjectMenuRegistry.add(dynamicMenuItemStub); + kubeObjectMenuRegistry.add([dynamicMenuItemStub]); }; diff --git a/src/renderer/components/getDi.tsx b/src/renderer/getDi.tsx similarity index 83% rename from src/renderer/components/getDi.tsx rename to src/renderer/getDi.tsx index a0e4615a7c..4c4359fe74 100644 --- a/src/renderer/components/getDi.tsx +++ b/src/renderer/getDi.tsx @@ -24,11 +24,15 @@ import { createContainer } from "@ogre-tools/injectable"; export const getDi = () => createContainer( getRequireContextForRendererCode, + getRequireContextForCommonCode, getRequireContextForCommonExtensionCode, ); const getRequireContextForRendererCode = () => - require.context("../", true, /\.injectable\.(ts|tsx)$/); + require.context("./", true, /\.injectable\.(ts|tsx)$/); + +const getRequireContextForCommonCode = () => + require.context("../common", true, /\.injectable\.(ts|tsx)$/); const getRequireContextForCommonExtensionCode = () => - require.context("../../extensions", true, /\.injectable\.(ts|tsx)$/); + require.context("../extensions", true, /\.injectable\.(ts|tsx)$/); diff --git a/src/renderer/initializers/command-registry.tsx b/src/renderer/initializers/command-registry.tsx deleted file mode 100644 index ff24f72fbf..0000000000 --- a/src/renderer/initializers/command-registry.tsx +++ /dev/null @@ -1,207 +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 React from "react"; -import * as routes from "../../common/routes"; -import { CommandRegistry } from "../../extensions/registries"; -import { getActiveClusterEntity } from "../api/catalog-entity-registry"; -import { CommandOverlay } from "../components/command-palette"; -import { createTerminalTab } from "../components/dock/terminal.store"; -import { HotbarAddCommand } from "../components/hotbar/hotbar-add-command"; -import { HotbarRemoveCommand } from "../components/hotbar/hotbar-remove-command"; -import { HotbarSwitchCommand } from "../components/hotbar/hotbar-switch-command"; -import { navigate } from "../navigation"; -import { HotbarRenameCommand } from "../components/hotbar/hotbar-rename-command"; -import { ActivateEntityCommand } from "../components/activate-entity-command"; - -export function initCommandRegistry() { - CommandRegistry.getInstance() - .add([ - { - id: "app.showPreferences", - title: "Preferences: Open", - scope: "global", - action: () => navigate(routes.preferencesURL()), - }, - { - id: "cluster.viewHelmCharts", - title: "Cluster: View Helm Charts", - scope: "entity", - action: () => navigate(routes.helmChartsURL()), - }, - { - id: "cluster.viewHelmReleases", - title: "Cluster: View Helm Releases", - scope: "entity", - action: () => navigate(routes.releaseURL()), - }, - { - id: "cluster.viewConfigMaps", - title: "Cluster: View ConfigMaps", - scope: "entity", - action: () => navigate(routes.configMapsURL()), - }, - { - id: "cluster.viewSecrets", - title: "Cluster: View Secrets", - scope: "entity", - action: () => navigate(routes.secretsURL()), - }, - { - id: "cluster.viewResourceQuotas", - title: "Cluster: View ResourceQuotas", - scope: "entity", - action: () => navigate(routes.resourceQuotaURL()), - }, - { - id: "cluster.viewLimitRanges", - title: "Cluster: View LimitRanges", - scope: "entity", - action: () => navigate(routes.limitRangeURL()), - }, - { - id: "cluster.viewHorizontalPodAutoscalers", - title: "Cluster: View HorizontalPodAutoscalers (HPA)", - scope: "entity", - action: () => navigate(routes.hpaURL()), - }, - { - id: "cluster.viewPodDisruptionBudget", - title: "Cluster: View PodDisruptionBudgets", - scope: "entity", - action: () => navigate(routes.pdbURL()), - }, - { - id: "cluster.viewServices", - title: "Cluster: View Services", - scope: "entity", - action: () => navigate(routes.servicesURL()), - }, - { - id: "cluster.viewEndpoints", - title: "Cluster: View Endpoints", - scope: "entity", - action: () => navigate(routes.endpointURL()), - }, - { - id: "cluster.viewIngresses", - title: "Cluster: View Ingresses", - scope: "entity", - action: () => navigate(routes.ingressURL()), - }, - { - id: "cluster.viewNetworkPolicies", - title: "Cluster: View NetworkPolicies", - scope: "entity", - action: () => navigate(routes.networkPoliciesURL()), - }, - { - id: "cluster.viewNodes", - title: "Cluster: View Nodes", - scope: "entity", - action: () => navigate(routes.nodesURL()), - }, - { - id: "cluster.viewPods", - title: "Cluster: View Pods", - scope: "entity", - action: () => navigate(routes.podsURL()), - }, - { - id: "cluster.viewDeployments", - title: "Cluster: View Deployments", - scope: "entity", - action: () => navigate(routes.deploymentsURL()), - }, - { - id: "cluster.viewDaemonSets", - title: "Cluster: View DaemonSets", - scope: "entity", - action: () => navigate(routes.daemonSetsURL()), - }, - { - id: "cluster.viewStatefulSets", - title: "Cluster: View StatefulSets", - scope: "entity", - action: () => navigate(routes.statefulSetsURL()), - }, - { - id: "cluster.viewJobs", - title: "Cluster: View Jobs", - scope: "entity", - action: () => navigate(routes.jobsURL()), - }, - { - id: "cluster.viewCronJobs", - title: "Cluster: View CronJobs", - scope: "entity", - action: () => navigate(routes.cronJobsURL()), - }, - { - id: "cluster.viewCurrentClusterSettings", - title: "Cluster: View Settings", - scope: "global", - action: () => navigate(routes.entitySettingsURL({ - params: { - entityId: getActiveClusterEntity()?.id, - }, - })), - isActive: (context) => !!context.entity, - }, - { - id: "cluster.openTerminal", - title: "Cluster: Open terminal", - scope: "entity", - action: () => createTerminalTab(), - isActive: (context) => !!context.entity, - }, - { - id: "hotbar.switchHotbar", - title: "Hotbar: Switch ...", - scope: "global", - action: () => CommandOverlay.open(), - }, - { - id: "hotbar.addHotbar", - title: "Hotbar: Add Hotbar ...", - scope: "global", - action: () => CommandOverlay.open(), - }, - { - id: "hotbar.removeHotbar", - title: "Hotbar: Remove Hotbar ...", - scope: "global", - action: () => CommandOverlay.open(), - }, - { - id: "hotbar.renameHotbar", - title: "Hotbar: Rename Hotbar ...", - scope: "global", - action: () => CommandOverlay.open(), - }, - { - id: "catalog.searchEntities", - title: "Catalog: Activate Entity ...", - scope: "global", - action: () => CommandOverlay.open(), - }, - ]); -} diff --git a/src/renderer/initializers/index.ts b/src/renderer/initializers/index.ts index b865368988..758c9042ad 100644 --- a/src/renderer/initializers/index.ts +++ b/src/renderer/initializers/index.ts @@ -21,7 +21,6 @@ export * from "./catalog-entity-detail-registry"; export * from "./catalog"; -export * from "./command-registry"; export * from "./entity-settings-registry"; export * from "./ipc"; export * from "./kube-object-detail-registry"; diff --git a/src/renderer/initializers/registries.ts b/src/renderer/initializers/registries.ts index a27c47153b..a68b4e0abd 100644 --- a/src/renderer/initializers/registries.ts +++ b/src/renderer/initializers/registries.ts @@ -26,7 +26,6 @@ export function initRegistries() { registries.CatalogEntityDetailRegistry.createInstance(); registries.ClusterPageMenuRegistry.createInstance(); registries.ClusterPageRegistry.createInstance(); - registries.CommandRegistry.createInstance(); registries.EntitySettingRegistry.createInstance(); registries.GlobalPageRegistry.createInstance(); registries.KubeObjectDetailRegistry.createInstance(); diff --git a/src/renderer/navigation/helpers.ts b/src/renderer/navigation/helpers.ts index d6408cb272..1314b0973f 100644 --- a/src/renderer/navigation/helpers.ts +++ b/src/renderer/navigation/helpers.ts @@ -54,7 +54,7 @@ export function isActiveRoute(route: string | string[] | RouteProps): boolean { return !!matchRoute(route); } -export function getMatchedClusterId(): string { +export function getMatchedClusterId(): string | undefined { const matched = matchPath(navigation.location.pathname, { exact: true, path: clusterViewRoute.path,