From f8ae1149fbea293625cf97a572cad60d3043e893 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Tue, 4 Jan 2022 16:43:29 -0500 Subject: [PATCH] Make all of Command* injectable, with some others too Signed-off-by: Sebastian Malton --- src/common/__tests__/hotbar-store.test.ts | 4 +- src/common/catalog-entities/web-link.ts | 17 +- ...ar-store.ts => hotbar-store.injectable.ts} | 102 +++++--- src/common/hotbar-types.ts | 8 +- src/common/ipc/index.ts | 1 + src/common/utils/iter.ts | 15 -- src/extensions/getDiForUnitTesting.ts | 1 - .../registries/entity-setting-registry.ts | 4 +- src/main/index.ts | 2 +- src/main/menu/menu.ts | 2 + src/renderer/api/catalog-entity-registry.ts | 4 +- src/renderer/bootstrap.tsx | 7 +- src/renderer/components/+catalog/catalog.tsx | 2 +- .../+catalog/hotbar-toggle-menu-item.tsx | 2 +- .../custom-resources.injectable.ts | 2 +- .../+extensions/__tests__/extensions.test.tsx | 2 +- .../+welcome/__test__/welcome.test.tsx | 2 +- .../activate-entity-command.tsx | 70 +++--- .../catalog-entities/weblink-add-command.tsx | 23 +- .../cluster-manager/active-hotbar-name.tsx | 38 ++- .../cluster-manager/bottom-bar.test.tsx | 89 ++++--- .../command-palette/command-container.tsx | 48 ++-- .../command-palette/command-dialog.tsx | 8 +- ...erlay.ts => command-overlay.injectable.ts} | 30 ++- .../components/command-palette/index.ts | 2 +- .../internal-commands.injectable.tsx | 231 ++++++++++++++++++ .../registered-commands/internal-commands.tsx | 215 ---------------- .../registered-commands.injectable.ts | 33 ++- .../delete-cluster-dialog.tsx | 2 +- .../__tests__/hotbar-remove-command.test.tsx | 68 ++++-- .../components/hotbar/hotbar-add-command.tsx | 66 +++-- .../components/hotbar/hotbar-menu.tsx | 2 +- .../hotbar/hotbar-remove-command.tsx | 92 +++---- .../hotbar/hotbar-rename-command.tsx | 115 ++++----- .../components/hotbar/hotbar-selector.tsx | 62 +++-- .../hotbar/hotbar-switch-command.tsx | 137 ++++++----- .../kube-object-menu.test.tsx | 5 +- .../layout/__tests__/sidebar-cluster.test.tsx | 2 +- .../components/layout/sidebar-cluster.tsx | 2 +- .../layout/top-bar/top-bar-win-linux.test.tsx | 2 +- .../layout/top-bar/top-bar.test.tsx | 2 +- .../{components => }/getDiForUnitTesting.tsx | 30 +-- src/renderer/initializers/catalog.tsx | 48 ++-- .../event-listener.injectable.ts} | 22 +- 44 files changed, 868 insertions(+), 753 deletions(-) rename src/common/{hotbar-store.ts => hotbar-store.injectable.ts} (77%) rename src/{common/k8s-api/endpoints/custom-resources => renderer/components/+custom-resources}/custom-resources.injectable.ts (94%) rename src/renderer/components/command-palette/{command-overlay.ts => command-overlay.injectable.ts} (68%) create mode 100644 src/renderer/components/command-palette/registered-commands/internal-commands.injectable.tsx delete mode 100644 src/renderer/components/command-palette/registered-commands/internal-commands.tsx rename src/renderer/{components => }/getDiForUnitTesting.tsx (72%) rename src/renderer/{components/hotbar/hotbar-display-label.ts => window/event-listener.injectable.ts} (64%) diff --git a/src/common/__tests__/hotbar-store.test.ts b/src/common/__tests__/hotbar-store.test.ts index ffc4e361da..9baa291acd 100644 --- a/src/common/__tests__/hotbar-store.test.ts +++ b/src/common/__tests__/hotbar-store.test.ts @@ -26,7 +26,7 @@ import logger from "../../main/logger"; import { AppPaths } from "../app-paths"; import type { CatalogEntity, CatalogEntityData, CatalogEntityKindData } from "../catalog"; import { ClusterStore } from "../cluster-store"; -import { HotbarStore } from "../hotbar-store"; +import { HotbarStore } from "../hotbar-store.injectable"; jest.mock("../../main/catalog/catalog-entity-registry", () => ({ catalogEntityRegistry: { @@ -251,7 +251,7 @@ describe("HotbarStore", () => { const hotbarStore = HotbarStore.getInstance(); hotbarStore.add({ name: "hottest", id: "hottest" }); - hotbarStore.activeHotbarId = "hottest"; + hotbarStore.setActiveHotbar("hottest"); const { error } = logger; const mocked = jest.fn(); diff --git a/src/common/catalog-entities/web-link.ts b/src/common/catalog-entities/web-link.ts index 870558e0b3..6764ca8bee 100644 --- a/src/common/catalog-entities/web-link.ts +++ b/src/common/catalog-entities/web-link.ts @@ -19,7 +19,7 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { CatalogCategory, CatalogEntity, CatalogEntityAddMenuContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog"; +import { CatalogCategory, CatalogEntity, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog"; import { catalogCategoryRegistry } from "../catalog/catalog-category-registry"; import { productName } from "../vars"; import { WeblinkStore } from "../weblink-store"; @@ -86,21 +86,6 @@ export class WebLinkCategory extends CatalogCategory { kind: "WebLink", }, }; - public static onAdd?: () => void; - - constructor() { - super(); - - this.on("catalogAddMenu", (ctx: CatalogEntityAddMenuContext) => { - ctx.menuItems.push({ - icon: "public", - title: "Add web link", - onClick: () => { - WebLinkCategory.onAdd(); - }, - }); - }); - } } catalogCategoryRegistry.add(new WebLinkCategory()); diff --git a/src/common/hotbar-store.ts b/src/common/hotbar-store.injectable.ts similarity index 77% rename from src/common/hotbar-store.ts rename to src/common/hotbar-store.injectable.ts index 4a101e6013..daad150c2e 100644 --- a/src/common/hotbar-store.ts +++ b/src/common/hotbar-store.injectable.ts @@ -19,7 +19,7 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { action, comparer, observable, makeObservable } from "mobx"; +import { action, comparer, observable, makeObservable, computed } from "mobx"; import { BaseStore } from "./base-store"; import migrations from "../migrations/hotbar-store"; import { toJS } from "./utils"; @@ -27,7 +27,8 @@ import { CatalogEntity } from "./catalog"; import { catalogEntity } from "../main/catalog-sources/general"; import logger from "../main/logger"; import { broadcastMessage, HotbarTooManyItems } from "./ipc"; -import { defaultHotbarCells, getEmptyHotbar, Hotbar, HotbarCreateOptions } from "./hotbar-types"; +import { defaultHotbarCells, getEmptyHotbar, Hotbar, CreateHotbarData, CreateHotbarOptions } from "./hotbar-types"; +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; export interface HotbarStoreModel { hotbars: Hotbar[]; @@ -52,22 +53,40 @@ export class HotbarStore extends BaseStore { this.load(); } - get activeHotbarId() { + @computed get activeHotbarId() { return this._activeHotbarId; } - set activeHotbarId(id: string) { - if (this.getById(id)) { - this._activeHotbarId = id; + /** + * If `hotbar` points to a known hotbar, make it active. Otherwise, ignore + * @param hotbar The hotbar instance, or the index, or its ID + */ + setActiveHotbar(hotbar: Hotbar | number | string) { + if (typeof hotbar === "number") { + if (hotbar >= 0 && hotbar < this.hotbars.length) { + this._activeHotbarId = this.hotbars[hotbar].id; + } + } else if (typeof hotbar === "string") { + if (this.getById(hotbar)) { + this._activeHotbarId = hotbar; + } + } else { + if (this.hotbars.indexOf(hotbar) >= 0) { + this._activeHotbarId = hotbar.id; + } } } - hotbarIndex(id: string) { + private hotbarIndexById(id: string) { return this.hotbars.findIndex((hotbar) => hotbar.id === id); } - get activeHotbarIndex() { - return this.hotbarIndex(this.activeHotbarId); + private hotbarIndex(hotbar: Hotbar) { + return this.hotbars.indexOf(hotbar); + } + + @computed get activeHotbarIndex() { + return this.hotbarIndexById(this.activeHotbarId); } @action @@ -87,13 +106,11 @@ export class HotbarStore extends BaseStore { this.hotbars.forEach(ensureExactHotbarItemLength); if (data.activeHotbarId) { - if (this.getById(data.activeHotbarId)) { - this.activeHotbarId = data.activeHotbarId; - } + this.setActiveHotbar(data.activeHotbarId); } if (!this.activeHotbarId) { - this.activeHotbarId = this.hotbars[0].id; + this.setActiveHotbar(0); } } @@ -118,8 +135,7 @@ export class HotbarStore extends BaseStore { return this.hotbars.find((hotbar) => hotbar.id === id); } - @action - add(data: HotbarCreateOptions, { setActive = false } = {}) { + add = action((data: CreateHotbarData, { setActive = false }: CreateHotbarOptions = {}) => { const hotbar = getEmptyHotbar(data.name, data.id); this.hotbars.push(hotbar); @@ -127,29 +143,29 @@ export class HotbarStore extends BaseStore { if (setActive) { this._activeHotbarId = hotbar.id; } - } + }); - @action - setHotbarName(id: string, name: string) { + setHotbarName = action((id: string, name: string) => { const index = this.hotbars.findIndex((hotbar) => hotbar.id === id); - if(index < 0) { - console.warn(`[HOTBAR-STORE]: cannot setHotbarName: unknown id`, { id }); - - return; + if (index < 0) { + return void console.warn(`[HOTBAR-STORE]: cannot setHotbarName: unknown id`, { id }); } this.hotbars[index].name = name; - } + }); + + remove = action((hotbar: Hotbar) => { + if (this.hotbars.length <= 1) { + throw new Error("Cannot remove the last hotbar"); + } - @action - remove(hotbar: Hotbar) { this.hotbars = this.hotbars.filter((h) => h !== hotbar); if (this.activeHotbarId === hotbar.id) { - this.activeHotbarId = this.hotbars[0].id; + this.setActiveHotbar(0); } - } + }); @action addToHotbar(item: CatalogEntity, cellIndex?: number) { @@ -263,7 +279,7 @@ export class HotbarStore extends BaseStore { index = hotbarStore.hotbars.length - 1; } - hotbarStore.activeHotbarId = hotbarStore.hotbars[index].id; + hotbarStore.setActiveHotbar(index); } switchToNext() { @@ -274,7 +290,7 @@ export class HotbarStore extends BaseStore { index = 0; } - hotbarStore.activeHotbarId = hotbarStore.hotbars[index].id; + hotbarStore.setActiveHotbar(index); } /** @@ -284,6 +300,20 @@ export class HotbarStore extends BaseStore { isAddedToActive(entity: CatalogEntity) { return !!this.getActive().items.find(item => item?.entity.uid === entity.metadata.uid); } + + getDisplayLabel(hotbar: Hotbar): string { + return `${this.getDisplayIndex(hotbar)}: ${hotbar.name}`; + } + + getDisplayIndex(hotbar: Hotbar): string { + const index = this.hotbarIndex(hotbar); + + if (index < 0) { + return "??"; + } + + return `${index + 1}`; + } } /** @@ -292,12 +322,7 @@ export class HotbarStore extends BaseStore { * @param hotbar The hotbar to modify */ function ensureExactHotbarItemLength(hotbar: Hotbar) { - if (hotbar.items.length === defaultHotbarCells) { - // if we already have `defaultHotbarCells` then we are good to stop - return; - } - - // otherwise, keep adding empty entries until full + // if there are not enough items while (hotbar.items.length < defaultHotbarCells) { hotbar.items.push(null); } @@ -314,3 +339,10 @@ function ensureExactHotbarItemLength(hotbar: Hotbar) { } } } + +const hotbarManagerInjectable = getInjectable({ + instantiate: () => HotbarStore.getInstance(), + lifecycle: lifecycleEnum.singleton, +}); + +export default hotbarManagerInjectable; diff --git a/src/common/hotbar-types.ts b/src/common/hotbar-types.ts index ee65071e0e..7e36a2c37c 100644 --- a/src/common/hotbar-types.ts +++ b/src/common/hotbar-types.ts @@ -33,14 +33,18 @@ export interface HotbarItem { } } -export type Hotbar = Required; +export type Hotbar = Required; -export interface HotbarCreateOptions { +export interface CreateHotbarData { id?: string; name: string; items?: Tuple; } +export interface CreateHotbarOptions { + setActive?: boolean; +} + export const defaultHotbarCells = 12; // Number is chosen to easy hit any item with keyboard export function getEmptyHotbar(name: string, id: string = uuid.v4()): Hotbar { diff --git a/src/common/ipc/index.ts b/src/common/ipc/index.ts index 774ce589e9..9a64ede7d6 100644 --- a/src/common/ipc/index.ts +++ b/src/common/ipc/index.ts @@ -20,6 +20,7 @@ */ export const dialogShowOpenDialogHandler = "dialog:show-open-dialog"; +export const catalogEntityRunListener = "catalog-entity:run"; export * from "./ipc"; export * from "./invalid-kubeconfig"; diff --git a/src/common/utils/iter.ts b/src/common/utils/iter.ts index 8ba606a881..6271a05969 100644 --- a/src/common/utils/iter.ts +++ b/src/common/utils/iter.ts @@ -201,18 +201,3 @@ 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/getDiForUnitTesting.ts b/src/extensions/getDiForUnitTesting.ts index 5460d8887b..81184da59a 100644 --- a/src/extensions/getDiForUnitTesting.ts +++ b/src/extensions/getDiForUnitTesting.ts @@ -41,7 +41,6 @@ export const getDiForUnitTesting = () => { aliases: [injectable, ...(injectable.aliases || [])], }; }) - .forEach(injectable => di.register(injectable)); di.preventSideEffects(); diff --git a/src/extensions/registries/entity-setting-registry.ts b/src/extensions/registries/entity-setting-registry.ts index 54d85ef57f..1446dff78d 100644 --- a/src/extensions/registries/entity-setting-registry.ts +++ b/src/extensions/registries/entity-setting-registry.ts @@ -54,7 +54,7 @@ export class EntitySettingRegistry extends BaseRegistry { let items = this.getItems().filter((item) => { return item.kind === kind && item.apiVersions.includes(apiVersion); }); @@ -66,5 +66,5 @@ export class EntitySettingRegistry extends BaseRegistry (b.priority ?? 50) - (a.priority ?? 50)); - } + }; } diff --git a/src/main/index.ts b/src/main/index.ts index f40b92aa21..69a42d8383 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -51,7 +51,7 @@ import configurePackages from "../common/configure-packages"; import { PrometheusProviderRegistry } from "./prometheus"; import * as initializers from "./initializers"; import { ClusterStore } from "../common/cluster-store"; -import { HotbarStore } from "../common/hotbar-store"; +import { HotbarStore } from "../common/hotbar-store.injectable"; import { UserStore } from "../common/user-store"; import { WeblinkStore } from "../common/weblink-store"; import { ExtensionsStore } from "../extensions/extensions-store"; diff --git a/src/main/menu/menu.ts b/src/main/menu/menu.ts index 512d4e916d..13cca14ac7 100644 --- a/src/main/menu/menu.ts +++ b/src/main/menu/menu.ts @@ -220,6 +220,8 @@ export function getAppMenu( /** * Don't broadcast unless it was triggered by menu iteration so that * there aren't double events in renderer + * + * NOTE: this `?` is required because of a bug in playwright. https://github.com/microsoft/playwright/issues/10554 */ if (!event?.triggeredByAccelerator) { broadcastMessage("command-palette:open"); diff --git a/src/renderer/api/catalog-entity-registry.ts b/src/renderer/api/catalog-entity-registry.ts index af2309bc4c..10eb124c45 100644 --- a/src/renderer/api/catalog-entity-registry.ts +++ b/src/renderer/api/catalog-entity-registry.ts @@ -20,7 +20,7 @@ */ import { computed, observable, makeObservable, action } from "mobx"; -import { ipcRendererOn } from "../../common/ipc"; +import { catalogEntityRunListener, ipcRendererOn } from "../../common/ipc"; import { CatalogCategory, CatalogEntity, CatalogEntityData, catalogCategoryRegistry, CatalogCategoryRegistry, CatalogEntityKindData } from "../../common/catalog"; import "../../common/catalog-entities"; import type { Cluster } from "../../main/cluster"; @@ -88,7 +88,7 @@ export class CatalogEntityRegistry { ipcRenderer.send(CatalogIpcEvents.INIT); if (isMainFrame) { - ipcRendererOn("catalog-entity:run", (event, id: string) => { + ipcRendererOn(catalogEntityRunListener, (event, id: string) => { const entity = this.getById(id); if (entity) { diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx index 210d78267b..9174d30fb2 100644 --- a/src/renderer/bootstrap.tsx +++ b/src/renderer/bootstrap.tsx @@ -40,7 +40,7 @@ import { DefaultProps } from "./mui-base-theme"; import configurePackages from "../common/configure-packages"; import * as initializers from "./initializers"; import logger from "../common/logger"; -import { HotbarStore } from "../common/hotbar-store"; +import { HotbarStore } from "../common/hotbar-store.injectable"; import { WeblinkStore } from "../common/weblink-store"; import { ExtensionsStore } from "../extensions/extensions-store"; import { FilesystemProvisionerStore } from "../main/extension-filesystem"; @@ -59,6 +59,7 @@ import bindProtocolAddRouteHandlersInjectable import type { LensProtocolRouterRenderer } from "./protocol-handler"; import lensProtocolRouterRendererInjectable from "./protocol-handler/lens-protocol-router-renderer/lens-protocol-router-renderer.injectable"; +import commandOverlayInjectable from "./components/command-palette/command-overlay.injectable"; if (process.isMainFrame) { SentryInit(); @@ -121,7 +122,9 @@ export async function bootstrap(comp: () => Promise, di: Dependenc initializers.initCatalogCategoryRegistryEntries(); logger.info(`${logPrefix} initializing Catalog`); - initializers.initCatalog(); + initializers.initCatalog({ + openCommandDialog: di.inject(commandOverlayInjectable).open, + }); const extensionLoader = di.inject(extensionLoaderInjectable); diff --git a/src/renderer/components/+catalog/catalog.tsx b/src/renderer/components/+catalog/catalog.tsx index 595ba6a81c..7774377ccc 100644 --- a/src/renderer/components/+catalog/catalog.tsx +++ b/src/renderer/components/+catalog/catalog.tsx @@ -29,7 +29,7 @@ import { CatalogEntityStore } from "./catalog-entity.store"; import { navigate } from "../../navigation"; import { MenuItem, MenuActions } from "../menu"; import type { CatalogEntityContextMenu, CatalogEntityContextMenuContext } from "../../api/catalog-entity"; -import { HotbarStore } from "../../../common/hotbar-store"; +import { HotbarStore } from "../../../common/hotbar-store.injectable"; import { ConfirmDialog } from "../confirm-dialog"; import { catalogCategoryRegistry, CatalogEntity } from "../../../common/catalog"; import { CatalogAddButton } from "./catalog-add-button"; diff --git a/src/renderer/components/+catalog/hotbar-toggle-menu-item.tsx b/src/renderer/components/+catalog/hotbar-toggle-menu-item.tsx index da89ff3f26..260b0d5301 100644 --- a/src/renderer/components/+catalog/hotbar-toggle-menu-item.tsx +++ b/src/renderer/components/+catalog/hotbar-toggle-menu-item.tsx @@ -20,7 +20,7 @@ */ import React, { ReactNode, useState } from "react"; -import { HotbarStore } from "../../../common/hotbar-store"; +import { HotbarStore } from "../../../common/hotbar-store.injectable"; import { MenuItem } from "../menu"; import type { CatalogEntity } from "../../api/catalog-entity"; diff --git a/src/common/k8s-api/endpoints/custom-resources/custom-resources.injectable.ts b/src/renderer/components/+custom-resources/custom-resources.injectable.ts similarity index 94% rename from src/common/k8s-api/endpoints/custom-resources/custom-resources.injectable.ts rename to src/renderer/components/+custom-resources/custom-resources.injectable.ts index 4a83b7de70..99a532f69f 100644 --- a/src/common/k8s-api/endpoints/custom-resources/custom-resources.injectable.ts +++ b/src/renderer/components/+custom-resources/custom-resources.injectable.ts @@ -21,7 +21,7 @@ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { computed } from "mobx"; -import { crdStore } from "../../../../renderer/components/+custom-resources/crd.store"; +import { crdStore } from "./crd.store"; const customResourceDefinitionsInjectable = getInjectable({ instantiate: () => computed(() => [...crdStore.items]), diff --git a/src/renderer/components/+extensions/__tests__/extensions.test.tsx b/src/renderer/components/+extensions/__tests__/extensions.test.tsx index 4ebdb6156f..1ad53d2fd6 100644 --- a/src/renderer/components/+extensions/__tests__/extensions.test.tsx +++ b/src/renderer/components/+extensions/__tests__/extensions.test.tsx @@ -34,7 +34,7 @@ import { mockWindow } from "../../../../../__mocks__/windowMock"; import { AppPaths } from "../../../../common/app-paths"; import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable"; -import { getDiForUnitTesting } from "../../getDiForUnitTesting"; +import { getDiForUnitTesting } from "../../../getDiForUnitTesting"; import { DiRender, renderFor } from "../../test-utils/renderFor"; mockWindow(); diff --git a/src/renderer/components/+welcome/__test__/welcome.test.tsx b/src/renderer/components/+welcome/__test__/welcome.test.tsx index 74cf65b20d..3d1ba9b7ce 100644 --- a/src/renderer/components/+welcome/__test__/welcome.test.tsx +++ b/src/renderer/components/+welcome/__test__/welcome.test.tsx @@ -24,7 +24,7 @@ import { screen } from "@testing-library/react"; import "@testing-library/jest-dom/extend-expect"; import { defaultWidth, Welcome } from "../welcome"; import { computed } from "mobx"; -import { getDiForUnitTesting } from "../../getDiForUnitTesting"; +import { getDiForUnitTesting } from "../../../getDiForUnitTesting"; import type { DiRender } from "../../test-utils/renderFor"; import { renderFor } from "../../test-utils/renderFor"; import type { ConfigurableDependencyInjectionContainer } from "@ogre-tools/injectable"; 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 46d1a8db1f..57b1cb968a 100644 --- a/src/renderer/components/activate-entity-command/activate-entity-command.tsx +++ b/src/renderer/components/activate-entity-command/activate-entity-command.tsx @@ -19,41 +19,49 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { computed } from "mobx"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import { computed, IComputedValue } from "mobx"; import { observer } from "mobx-react"; import React from "react"; -import { broadcastMessage } from "../../../common/ipc"; +import { broadcastMessage, catalogEntityRunListener } from "../../../common/ipc"; import type { CatalogEntity } from "../../api/catalog-entity"; import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; -import { CommandOverlay } from "../command-palette"; +import commandOverlayInjectable from "../command-palette/command-overlay.injectable"; import { Select } from "../select"; -@observer -export class ActivateEntityCommand extends React.Component { - @computed get options() { - return catalogEntityRegistry.items.map(entity => ({ - label: `${entity.kind}: ${entity.getName()}`, - value: entity, - })); - } - - onSelect(entity: CatalogEntity): void { - broadcastMessage("catalog-entity:run", entity.getId()); - CommandOverlay.close(); - } - - render() { - return ( - onSelect(v.value)} + components={{ DropdownIndicator: null, IndicatorSeparator: null }} + menuIsOpen={true} + options={options} + autoFocus={true} + escapeClearsValue={false} + placeholder="Activate entity ..." + /> + ); +}); + +export const ActivateEntityCommand = withInjectables(NonInjectedActivateEntityCommand, { + getProps: di => ({ + closeCommandOverlay: di.inject(commandOverlayInjectable).close, + entities: computed(() => [...catalogEntityRegistry.items]), + }), +}); diff --git a/src/renderer/components/catalog-entities/weblink-add-command.tsx b/src/renderer/components/catalog-entities/weblink-add-command.tsx index 7d48622658..230c66987a 100644 --- a/src/renderer/components/catalog-entities/weblink-add-command.tsx +++ b/src/renderer/components/catalog-entities/weblink-add-command.tsx @@ -21,21 +21,26 @@ import React from "react"; import { observer } from "mobx-react"; -import { CommandOverlay } from "../command-palette"; import { Input } from "../input"; import { isUrl } from "../input/input_validators"; import { WeblinkStore } from "../../../common/weblink-store"; import { computed, makeObservable, observable } from "mobx"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import commandOverlayInjectable from "../command-palette/command-overlay.injectable"; + +interface Dependencies { + closeCommandOverlay: () => void; +} + @observer -export class WeblinkAddCommand extends React.Component { +class NonInjectedWeblinkAddCommand extends React.Component { @observable url = ""; @observable nameHidden = true; @observable dirty = false; - constructor(props: {}) { + constructor(props: Dependencies) { super(props); - makeObservable(this); } @@ -55,8 +60,7 @@ export class WeblinkAddCommand extends React.Component { name: name || this.url, url: this.url, }); - - CommandOverlay.close(); + this.props.closeCommandOverlay(); } @computed get showValidation() { @@ -100,3 +104,10 @@ export class WeblinkAddCommand extends React.Component { ); } } + +export const WeblinkAddCommand = withInjectables(NonInjectedWeblinkAddCommand, { + getProps: (di, props) => ({ + closeCommandOverlay: di.inject(commandOverlayInjectable).close, + ...props, + }), +}); diff --git a/src/renderer/components/cluster-manager/active-hotbar-name.tsx b/src/renderer/components/cluster-manager/active-hotbar-name.tsx index 63f8b583b9..344299c110 100644 --- a/src/renderer/components/cluster-manager/active-hotbar-name.tsx +++ b/src/renderer/components/cluster-manager/active-hotbar-name.tsx @@ -22,19 +22,31 @@ import React from "react"; import { observer } from "mobx-react"; import { Icon } from "../icon"; -import { HotbarStore } from "../../../common/hotbar-store"; -import { CommandOverlay } from "../command-palette"; +import hotbarManagerInjectable from "../../../common/hotbar-store.injectable"; import { HotbarSwitchCommand } from "../hotbar/hotbar-switch-command"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import commandOverlayInjectable from "../command-palette/command-overlay.injectable"; -export const ActiveHotbarName = observer(() => { - return ( -
CommandOverlay.open()} - > - - {HotbarStore.getInstance().getActive()?.name} -
- ); +interface Dependencies { + openCommandOverlay: (component: React.ReactElement) => void; + activeHotbarName: () => string | undefined; +} + +const NonInjectedActiveHotbarName = observer(({ openCommandOverlay, activeHotbarName }: Dependencies) => ( +
openCommandOverlay()} + > + + {activeHotbarName()} +
+)); + +export const ActiveHotbarName = withInjectables(NonInjectedActiveHotbarName, { + getProps: (di, props) => ({ + activeHotbarName: () => di.inject(hotbarManagerInjectable).getActive()?.name, + openCommandOverlay: di.inject(commandOverlayInjectable).open, + ...props, + }), }); diff --git a/src/renderer/components/cluster-manager/bottom-bar.test.tsx b/src/renderer/components/cluster-manager/bottom-bar.test.tsx index 4390175889..0733747a97 100644 --- a/src/renderer/components/cluster-manager/bottom-bar.test.tsx +++ b/src/renderer/components/cluster-manager/bottom-bar.test.tsx @@ -21,21 +21,19 @@ import React from "react"; import mockFs from "mock-fs"; -import { render, fireEvent } from "@testing-library/react"; +import { fireEvent } from "@testing-library/react"; import "@testing-library/jest-dom/extend-expect"; import { BottomBar } from "./bottom-bar"; import { StatusBarRegistry } from "../../../extensions/registries"; -import { HotbarStore } from "../../../common/hotbar-store"; +import hotbarManagerInjectable from "../../../common/hotbar-store.injectable"; import { AppPaths } from "../../../common/app-paths"; -import { CommandOverlay } from "../command-palette"; import { HotbarSwitchCommand } from "../hotbar/hotbar-switch-command"; import { ActiveHotbarName } from "./active-hotbar-name"; - -jest.mock("../command-palette", () => ({ - CommandOverlay: { - open: jest.fn(), - }, -})); +import { getDiForUnitTesting } from "../../getDiForUnitTesting"; +import { DiRender, renderFor } from "../test-utils/renderFor"; +import type { ConfigurableDependencyInjectionContainer } from "@ogre-tools/injectable"; +import commandOverlayInjectable from "../command-palette/command-overlay.injectable"; +import { getEmptyHotbar } from "../../../common/hotbar-types"; AppPaths.init(); @@ -55,7 +53,12 @@ jest.mock("electron", () => ({ }, })); +const foobarHotbar = getEmptyHotbar("foobar"); + describe("", () => { + let di: ConfigurableDependencyInjectionContainer; + let render: DiRender; + beforeEach(() => { const mockOpts = { "tmp": { @@ -63,14 +66,19 @@ describe("", () => { }, }; + di = getDiForUnitTesting(); + render = renderFor(di); + mockFs(mockOpts); StatusBarRegistry.createInstance(); - HotbarStore.createInstance(); + + di.override(hotbarManagerInjectable, () => ({ + getActive: () => foobarHotbar, + } as any)); }); afterEach(() => { StatusBarRegistry.resetInstance(); - HotbarStore.resetInstance(); mockFs.restore(); }); @@ -80,24 +88,20 @@ describe("", () => { expect(container).toBeInstanceOf(HTMLElement); }); - it("renders w/o errors when .getItems() returns unexpected (not type compliant) data", async () => { - StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => undefined); - expect(() => render()).not.toThrow(); - StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => "hello"); - expect(() => render()).not.toThrow(); - StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => 6); - expect(() => render()).not.toThrow(); - StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => null); - expect(() => render()).not.toThrow(); - StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => []); - expect(() => render()).not.toThrow(); - StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [{}]); - expect(() => render()).not.toThrow(); - StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => { return {};}); + it.each([ + undefined, + "hello", + 6, + null, + [], + [{}], + {}, + ])("renders w/o errors when .getItems() returns not type compliant (%p)", val => { + StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => val); expect(() => render()).not.toThrow(); }); - it("renders items [{item: React.ReactNode}] (4.0.0-rc.1)", async () => { + it("renders items [{item: React.ReactNode}] (4.0.0-rc.1)", () => { const testId = "testId"; const text = "heee"; @@ -106,10 +110,10 @@ describe("", () => { ]); const { getByTestId } = render(); - expect(await getByTestId(testId)).toHaveTextContent(text); + expect(getByTestId(testId)).toHaveTextContent(text); }); - it("renders items [{item: () => React.ReactNode}] (4.0.0-rc.1+)", async () => { + it("renders items [{item: () => React.ReactNode}] (4.0.0-rc.1+)", () => { const testId = "testId"; const text = "heee"; @@ -118,33 +122,25 @@ describe("", () => { ]); const { getByTestId } = render(); - expect(await getByTestId(testId)).toHaveTextContent(text); + expect(getByTestId(testId)).toHaveTextContent(text); }); - it("show default hotbar name", () => { + it("shows active hotbar name", () => { StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [ { item: () => }, ]); const { getByTestId } = render(); - expect(getByTestId("current-hotbar-name")).toHaveTextContent("default"); - }); - - it("show active hotbar name", () => { - StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [ - { item: () => }, - ]); - const { getByTestId } = render(); - - HotbarStore.getInstance().add({ - id: "new", - name: "new", - }, { setActive: true }); - - expect(getByTestId("current-hotbar-name")).toHaveTextContent("new"); + expect(getByTestId("current-hotbar-name")).toHaveTextContent("foobar"); }); it("opens command palette on click", () => { + const mockOpen = jest.fn(); + + di.override(commandOverlayInjectable, () => ({ + open: mockOpen, + }) as any); + StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [ { item: () => }, ]); @@ -153,7 +149,8 @@ describe("", () => { fireEvent.click(activeHotbar); - expect(CommandOverlay.open).toHaveBeenCalledWith(); + + expect(mockOpen).toHaveBeenCalledWith(); }); it("sort positioned items properly", () => { diff --git a/src/renderer/components/command-palette/command-container.tsx b/src/renderer/components/command-palette/command-container.tsx index 8450129574..711b16b142 100644 --- a/src/renderer/components/command-palette/command-container.tsx +++ b/src/renderer/components/command-palette/command-container.tsx @@ -26,41 +26,43 @@ import React from "react"; import { Dialog } from "../dialog"; import { CommandDialog } from "./command-dialog"; import type { ClusterId } from "../../../common/cluster-types"; -import { CommandOverlay } from "./command-overlay"; +import commandOverlayInjectable, { CommandOverlay } from "./command-overlay.injectable"; 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"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import windowAddEventListenerInjectable from "../../window/event-listener.injectable"; 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); - }; +interface Dependencies { + addWindowEventListener: (type: K, listener: (this: Window, ev: WindowEventMap[K]) => any, options?: boolean | AddEventListenerOptions) => Disposer; + commandOverlay: CommandOverlay, } @observer -export class CommandContainer extends React.Component { +class NonInjectedCommandContainer extends React.Component { private escHandler(event: KeyboardEvent) { + const { commandOverlay } = this.props; + if (event.key === "Escape") { event.stopPropagation(); - CommandOverlay.close(); + commandOverlay.close(); } } handleCommandPalette = () => { + const { commandOverlay } = this.props; const clusterIsActive = getMatchedClusterId() !== undefined; if (clusterIsActive) { broadcastMessage(`command-palette:${catalogEntityRegistry.activeEntity.getId()}:open`); } else { - CommandOverlay.open(); + commandOverlay.open(); } }; @@ -75,11 +77,13 @@ export class CommandContainer extends React.Component { } componentDidMount() { - const action = this.props.clusterId - ? () => CommandOverlay.open() + const { clusterId, addWindowEventListener, commandOverlay } = this.props; + + const action = clusterId + ? () => commandOverlay.open() : this.handleCommandPalette; - const ipcChannel = this.props.clusterId - ? `command-palette:${this.props.clusterId}:open` + const ipcChannel = clusterId + ? `command-palette:${clusterId}:open` : "command-palette:open"; disposeOnUnmount(this, [ @@ -90,17 +94,27 @@ export class CommandContainer extends React.Component { } render() { + const { commandOverlay } = this.props; + return (
- {CommandOverlay.component} + {commandOverlay.component}
); } } + +export const CommandContainer = withInjectables(NonInjectedCommandContainer, { + getProps: (di, props) => ({ + addWindowEventListener: di.inject(windowAddEventListenerInjectable), + commandOverlay: di.inject(commandOverlayInjectable), + ...props, + }), +}); diff --git a/src/renderer/components/command-palette/command-dialog.tsx b/src/renderer/components/command-palette/command-dialog.tsx index 4c495ee494..01f095ec16 100644 --- a/src/renderer/components/command-palette/command-dialog.tsx +++ b/src/renderer/components/command-palette/command-dialog.tsx @@ -24,7 +24,7 @@ import { Select } from "../select"; import type { IComputedValue } from "mobx"; import { observer } from "mobx-react"; import React, { useState } from "react"; -import { CommandOverlay } from "./command-overlay"; +import commandOverlayInjectable from "./command-overlay.injectable"; import type { CatalogEntity } from "../../../common/catalog"; import { navigate } from "../../navigation"; import { broadcastMessage } from "../../../common/ipc"; @@ -39,9 +39,10 @@ import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; interface Dependencies { commands: IComputedValue>; activeEntity?: CatalogEntity; + closeCommandOverlay: () => void; } -const NonInjectedCommandDialog = observer(({ commands, activeEntity }: Dependencies) => { +const NonInjectedCommandDialog = observer(({ commands, activeEntity, closeCommandOverlay }: Dependencies) => { const [searchValue, setSearchValue] = useState(""); const executeAction = (commandId: string) => { @@ -52,7 +53,7 @@ const NonInjectedCommandDialog = observer(({ commands, activeEntity }: Dependenc } try { - CommandOverlay.close(); + closeCommandOverlay(); command.action({ entity: activeEntity, navigate: (url, opts = {}) => { @@ -121,5 +122,6 @@ export const CommandDialog = withInjectables(NonInjectedCommandDia commands: di.inject(registeredCommandsInjectable), // TODO: replace with injection activeEntity: catalogEntityRegistry.activeEntity, + closeCommandOverlay: di.inject(commandOverlayInjectable).close, }), }); diff --git a/src/renderer/components/command-palette/command-overlay.ts b/src/renderer/components/command-palette/command-overlay.injectable.ts similarity index 68% rename from src/renderer/components/command-palette/command-overlay.ts rename to src/renderer/components/command-palette/command-overlay.injectable.ts index 1058b2f9d3..643e81ac90 100644 --- a/src/renderer/components/command-palette/command-overlay.ts +++ b/src/renderer/components/command-palette/command-overlay.injectable.ts @@ -19,29 +19,37 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { observable } from "mobx"; import React from "react"; export class CommandOverlay { - static #component = observable.box(null, { deep: false }); + #component = observable.box(null, { deep: false }); - static get isOpen(): boolean { - return Boolean(CommandOverlay.#component.get()); + get isOpen(): boolean { + return Boolean(this.#component.get()); } - static open(component: React.ReactElement) { + open = (component: React.ReactElement) => { if (!React.isValidElement(component)) { throw new TypeError("CommandOverlay.open must be passed a valid ReactElement"); } - CommandOverlay.#component.set(component); - } + this.#component.set(component); + }; - static close() { - CommandOverlay.#component.set(null); - } + close = () => { + this.#component.set(null); + }; - static get component(): React.ReactElement | null { - return CommandOverlay.#component.get(); + get component(): React.ReactElement | null { + return this.#component.get(); } } + +const commandOverlayInjectable = getInjectable({ + instantiate: () => new CommandOverlay(), + lifecycle: lifecycleEnum.singleton, +}); + +export default commandOverlayInjectable; diff --git a/src/renderer/components/command-palette/index.ts b/src/renderer/components/command-palette/index.ts index 27169ddae2..8aa1da106c 100644 --- a/src/renderer/components/command-palette/index.ts +++ b/src/renderer/components/command-palette/index.ts @@ -21,4 +21,4 @@ export * from "./command-container"; export * from "./command-dialog"; -export * from "./command-overlay"; +export * from "./command-overlay.injectable"; diff --git a/src/renderer/components/command-palette/registered-commands/internal-commands.injectable.tsx b/src/renderer/components/command-palette/registered-commands/internal-commands.injectable.tsx new file mode 100644 index 0000000000..4fc37a902e --- /dev/null +++ b/src/renderer/components/command-palette/registered-commands/internal-commands.injectable.tsx @@ -0,0 +1,231 @@ +/** + * 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, RegisteredEntitySetting } from "../../../../extensions/registries"; +import { createTerminalTab } from "../../dock/terminal.store"; +import { HotbarAddCommand } from "../../hotbar/hotbar-add-command"; +import { HotbarRemoveCommand } from "../../hotbar/hotbar-remove-command"; +import { HotbarSwitchCommand } from "../../hotbar/hotbar-switch-command"; +import { HotbarRenameCommand } from "../../hotbar/hotbar-rename-command"; +import { ActivateEntityCommand } from "../../activate-entity-command"; +import type { CommandContext, CommandRegistration } from "./commands"; +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import commandOverlayInjectable from "../command-overlay.injectable"; + +export function isKubernetesClusterActive(context: CommandContext): boolean { + return context.entity?.kind === "KubernetesCluster"; +} + +interface Dependencies { + openCommandDialog: (component: React.ReactElement) => void; + getEntitySettingItems: (kind: string, apiVersion: string, source?: string) => RegisteredEntitySetting[]; +} + +function getInternalCommands({ openCommandDialog, getEntitySettingItems }: Dependencies): CommandRegistration[] { + return [ + { + 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; + } + + return getEntitySettingItems(entity.kind, entity.apiVersion, entity.metadata.source).length > 0; + }, + }, + { + id: "cluster.openTerminal", + title: "Cluster: Open terminal", + action: () => createTerminalTab(), + isActive: isKubernetesClusterActive, + }, + { + id: "hotbar.switchHotbar", + title: "Hotbar: Switch ...", + action: () => openCommandDialog(), + }, + { + id: "hotbar.addHotbar", + title: "Hotbar: Add Hotbar ...", + action: () => openCommandDialog(), + }, + { + id: "hotbar.removeHotbar", + title: "Hotbar: Remove Hotbar ...", + action: () => openCommandDialog(), + }, + { + id: "hotbar.renameHotbar", + title: "Hotbar: Rename Hotbar ...", + action: () => openCommandDialog(), + }, + { + id: "catalog.searchEntities", + title: "Catalog: Activate Entity ...", + action: () => openCommandDialog(), + }, + ]; +} + +const internalCommandsInjectable = getInjectable({ + instantiate: (di) => getInternalCommands({ + openCommandDialog: di.inject(commandOverlayInjectable).open, + getEntitySettingItems: EntitySettingRegistry + .getInstance() + .getItemsForKind, + }), + lifecycle: lifecycleEnum.singleton, +}); + +export default internalCommandsInjectable; diff --git a/src/renderer/components/command-palette/registered-commands/internal-commands.tsx b/src/renderer/components/command-palette/registered-commands/internal-commands.tsx deleted file mode 100644 index 1b40f20f0e..0000000000 --- a/src/renderer/components/command-palette/registered-commands/internal-commands.tsx +++ /dev/null @@ -1,215 +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 { 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 index 0f263720d0..a9bbd6bc57 100644 --- a/src/renderer/components/command-palette/registered-commands/registered-commands.injectable.ts +++ b/src/renderer/components/command-palette/registered-commands/registered-commands.injectable.ts @@ -22,32 +22,30 @@ 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 customResourceDefinitionsInjectable from "../../+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"; +import type { CommandRegistration, RegisteredCommand } from "./commands"; +import internalCommandsInjectable, { isKubernetesClusterActive } from "./internal-commands.injectable"; interface Dependencies { extensions: IComputedValue; customResourceDefinitions: IComputedValue; + internalCommands: CommandRegistration[]; } -const instantiateRegisteredCommands = ({ extensions, customResourceDefinitions }: Dependencies) => computed(() => { +const instantiateRegisteredCommands = ({ extensions, customResourceDefinitions, internalCommands }: 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()), - })), - ), - ); + const commands = [ + ...internalCommands, + ...extensions.get().flatMap(e => e.commands), + ...customResourceDefinitions.get().map((command): CommandRegistration => ({ + 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)) { @@ -62,6 +60,7 @@ const registeredCommandsInjectable = getInjectable({ instantiate: (di) => instantiateRegisteredCommands({ extensions: di.inject(rendererExtensionsInjectable), customResourceDefinitions: di.inject(customResourceDefinitionsInjectable), + internalCommands: di.inject(internalCommandsInjectable), }), lifecycle: lifecycleEnum.singleton, diff --git a/src/renderer/components/delete-cluster-dialog/delete-cluster-dialog.tsx b/src/renderer/components/delete-cluster-dialog/delete-cluster-dialog.tsx index e6a03444b6..41ac450aae 100644 --- a/src/renderer/components/delete-cluster-dialog/delete-cluster-dialog.tsx +++ b/src/renderer/components/delete-cluster-dialog/delete-cluster-dialog.tsx @@ -31,7 +31,7 @@ import { saveKubeconfig } from "./save-config"; import { requestMain } from "../../../common/ipc"; import { clusterClearDeletingHandler, clusterDeleteHandler, clusterSetDeletingHandler } from "../../../common/cluster-ipc"; import { Notifications } from "../notifications"; -import { HotbarStore } from "../../../common/hotbar-store"; +import { HotbarStore } from "../../../common/hotbar-store.injectable"; import { boundMethod } from "autobind-decorator"; import { Dialog } from "../dialog"; import { Icon } from "../icon"; diff --git a/src/renderer/components/hotbar/__tests__/hotbar-remove-command.test.tsx b/src/renderer/components/hotbar/__tests__/hotbar-remove-command.test.tsx index 7a292e7e5f..330bc2930d 100644 --- a/src/renderer/components/hotbar/__tests__/hotbar-remove-command.test.tsx +++ b/src/renderer/components/hotbar/__tests__/hotbar-remove-command.test.tsx @@ -21,13 +21,16 @@ import "@testing-library/jest-dom/extend-expect"; import { HotbarRemoveCommand } from "../hotbar-remove-command"; -import { render, fireEvent } from "@testing-library/react"; +import { fireEvent } from "@testing-library/react"; import React from "react"; -import { ThemeStore } from "../../../theme.store"; -import { UserStore } from "../../../../common/user-store"; -import { Notifications } from "../../notifications"; -import mockFs from "mock-fs"; import { AppPaths } from "../../../../common/app-paths"; +import type { ConfigurableDependencyInjectionContainer } from "@ogre-tools/injectable"; +import { getDiForUnitTesting } from "../../../getDiForUnitTesting"; +import { type DiRender, renderFor } from "../../test-utils/renderFor"; +import hotbarManagerInjectable, { HotbarStore } from "../../../../common/hotbar-store.injectable"; +import { UserStore } from "../../../../common/user-store"; +import { ThemeStore } from "../../../theme.store"; +import { ConfirmDialog } from "../../confirm-dialog"; jest.mock("electron", () => ({ app: { @@ -55,45 +58,58 @@ const mockHotbars: { [id: string]: any } = { }, }; -jest.mock("../../../../common/hotbar-store", () => ({ - HotbarStore: { - getInstance: () => ({ - hotbars: [mockHotbars["1"]], - getById: (id: string) => mockHotbars[id], - remove: () => {}, - hotbarIndex: () => 0, - }), - }, -})); - describe("", () => { + let di: ConfigurableDependencyInjectionContainer; + let render: DiRender; + beforeEach(() => { - mockFs({ - "tmp": {}, - }); + di = getDiForUnitTesting(); + render = renderFor(di); + UserStore.createInstance(); ThemeStore.createInstance(); }); afterEach(() => { - UserStore.resetInstance(); ThemeStore.resetInstance(); - mockFs.restore(); + UserStore.resetInstance(); }); it("renders w/o errors", () => { + di.override(hotbarManagerInjectable, () => ({ + hotbars: [mockHotbars["1"]], + getById: (id: string) => mockHotbars[id], + remove: () => { }, + hotbarIndex: () => 0, + getDisplayLabel: () => "1: Default", + }) as any as HotbarStore); + const { container } = render(); expect(container).toBeInstanceOf(HTMLElement); }); - it("displays error notification if user tries to remove last hotbar", () => { - const spy = jest.spyOn(Notifications, "error"); - const { getByText } = render(); + it("calls remove if you click on the entry", () => { + const removeMock = jest.fn(); + + di.override(hotbarManagerInjectable, () => ({ + hotbars: [mockHotbars["1"]], + getById: (id: string) => mockHotbars[id], + remove: removeMock, + hotbarIndex: () => 0, + getDisplayLabel: () => "1: Default", + }) as any as HotbarStore); + + const { getByText } = render( + <> + + + , + ); fireEvent.click(getByText("1: Default")); + fireEvent.click(getByText("Remove Hotbar")); - expect(spy).toHaveBeenCalled(); - spy.mockRestore(); + expect(removeMock).toHaveBeenCalled(); }); }); diff --git a/src/renderer/components/hotbar/hotbar-add-command.tsx b/src/renderer/components/hotbar/hotbar-add-command.tsx index 20c532e252..2380a76f10 100644 --- a/src/renderer/components/hotbar/hotbar-add-command.tsx +++ b/src/renderer/components/hotbar/hotbar-add-command.tsx @@ -21,9 +21,11 @@ import React from "react"; import { observer } from "mobx-react"; -import { HotbarStore } from "../../../common/hotbar-store"; -import { CommandOverlay } from "../command-palette"; +import hotbarManagerInjectable, { HotbarStore } from "../../../common/hotbar-store.injectable"; import { Input, InputValidator } from "../input"; +import type { CreateHotbarData, CreateHotbarOptions } from "../../../common/hotbar-types"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import commandOverlayInjectable from "../command-palette/command-overlay.injectable"; export const uniqueHotbarName: InputValidator = { condition: ({ required }) => required, @@ -31,34 +33,44 @@ export const uniqueHotbarName: InputValidator = { validate: value => !HotbarStore.getInstance().getByName(value), }; -@observer -export class HotbarAddCommand extends React.Component { - onSubmit = (name: string) => { +interface Dependencies { + closeCommandOverlay: () => void; + addHotbar: (data: CreateHotbarData, { setActive }?: CreateHotbarOptions) => void; +} + +const NonInjectedHotbarAddCommand = observer(({ closeCommandOverlay, addHotbar }: Dependencies) => { + const onSubmit = (name: string) => { if (!name.trim()) { return; } - HotbarStore.getInstance().add({ name }, { setActive: true }); - CommandOverlay.close(); + addHotbar({ name }, { setActive: true }); + closeCommandOverlay(); }; - render() { - return ( - <> - - - Please provide a new hotbar name (Press "Enter" to confirm or "Escape" to cancel) - - - ); - } -} + return ( + <> + + + Please provide a new hotbar name (Press "Enter" to confirm or "Escape" to cancel) + + + ); +}); + +export const HotbarAddCommand = withInjectables(NonInjectedHotbarAddCommand, { + getProps: (di, props) => ({ + closeCommandOverlay: di.inject(commandOverlayInjectable).close, + addHotbar: di.inject(hotbarManagerInjectable).add, + ...props, + }), +}); diff --git a/src/renderer/components/hotbar/hotbar-menu.tsx b/src/renderer/components/hotbar/hotbar-menu.tsx index 897d4c0360..c55aff4f19 100644 --- a/src/renderer/components/hotbar/hotbar-menu.tsx +++ b/src/renderer/components/hotbar/hotbar-menu.tsx @@ -26,7 +26,7 @@ import { observer } from "mobx-react"; import { HotbarEntityIcon } from "./hotbar-entity-icon"; import { cssNames, IClassName } from "../../utils"; import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; -import { HotbarStore } from "../../../common/hotbar-store"; +import { HotbarStore } from "../../../common/hotbar-store.injectable"; import type { CatalogEntity } from "../../api/catalog-entity"; import { DragDropContext, Draggable, Droppable, DropResult } from "react-beautiful-dnd"; import { HotbarSelector } from "./hotbar-selector"; diff --git a/src/renderer/components/hotbar/hotbar-remove-command.tsx b/src/renderer/components/hotbar/hotbar-remove-command.tsx index 58aa1d849d..04dfe273d1 100644 --- a/src/renderer/components/hotbar/hotbar-remove-command.tsx +++ b/src/renderer/components/hotbar/hotbar-remove-command.tsx @@ -22,51 +22,44 @@ import React from "react"; import { observer } from "mobx-react"; import { Select } from "../select"; -import { computed, makeObservable } from "mobx"; -import { HotbarStore } from "../../../common/hotbar-store"; -import { hotbarDisplayLabel } from "./hotbar-display-label"; -import { CommandOverlay } from "../command-palette"; +import hotbarManagerInjectable from "../../../common/hotbar-store.injectable"; import { ConfirmDialog } from "../confirm-dialog"; -import { Notifications } from "../notifications"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import commandOverlayInjectable from "../command-palette/command-overlay.injectable"; +import type { Hotbar } from "../../../common/hotbar-types"; -@observer -export class HotbarRemoveCommand extends React.Component { - constructor(props: {}) { - super(props); - makeObservable(this); - } +interface Dependencies { + closeCommandOverlay: () => void; + hotbarManager: { + hotbars: Hotbar[]; + getById: (id: string) => Hotbar | undefined; + remove: (hotbar: Hotbar) => void; + getDisplayLabel: (hotbar: Hotbar) => string; + }; +} - @computed get options() { - return HotbarStore.getInstance().hotbars.map((hotbar) => { - return { value: hotbar.id, label: hotbarDisplayLabel(hotbar.id) }; - }); - } +const NonInjectedHotbarRemoveCommand = observer(({ closeCommandOverlay, hotbarManager }: Dependencies) => { + const options = hotbarManager.hotbars.map(hotbar => ({ + value: hotbar.id, + label: hotbarManager.getDisplayLabel(hotbar), + })); - onChange(id: string): void { - const hotbarStore = HotbarStore.getInstance(); - const hotbar = hotbarStore.getById(id); - - CommandOverlay.close(); + const onChange = (id: string): void => { + const hotbar = hotbarManager.getById(id); if (!hotbar) { return; } - if (hotbarStore.hotbars.length === 1) { - Notifications.error("Can't remove the last hotbar"); - - return; - } - + closeCommandOverlay(); + // TODO: make confirm dialog injectable ConfirmDialog.open({ okButtonProps: { - label: `Remove Hotbar`, + label: "Remove Hotbar", primary: false, accent: true, }, - ok: () => { - hotbarStore.remove(hotbar); - }, + ok: () => hotbarManager.remove(hotbar), message: (

@@ -75,19 +68,26 @@ export class HotbarRemoveCommand extends React.Component {

), }); - } + }; - render() { - return ( - onChange(v.value)} + components={{ DropdownIndicator: null, IndicatorSeparator: null }} + menuIsOpen={true} + options={options} + autoFocus={true} + escapeClearsValue={false} + placeholder="Remove hotbar" + /> + ); +}); + +export const HotbarRemoveCommand = withInjectables(NonInjectedHotbarRemoveCommand, { + getProps: (di, props) => ({ + closeCommandOverlay: di.inject(commandOverlayInjectable).close, + hotbarManager: di.inject(hotbarManagerInjectable), + ...props, + }), +}); diff --git a/src/renderer/components/hotbar/hotbar-rename-command.tsx b/src/renderer/components/hotbar/hotbar-rename-command.tsx index 6d8784a941..6ca702bf5b 100644 --- a/src/renderer/components/hotbar/hotbar-rename-command.tsx +++ b/src/renderer/components/hotbar/hotbar-rename-command.tsx @@ -19,81 +19,60 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import React from "react"; +import React, { useState } from "react"; import { observer } from "mobx-react"; import { Select } from "../select"; -import { action, computed, makeObservable, observable } from "mobx"; -import { HotbarStore } from "../../../common/hotbar-store"; -import { hotbarDisplayLabel } from "./hotbar-display-label"; +import hotbarManagerInjectable from "../../../common/hotbar-store.injectable"; import { Input } from "../input"; import { uniqueHotbarName } from "./hotbar-add-command"; -import { CommandOverlay } from "../command-palette"; +import type { Hotbar } from "../../../common/hotbar-types"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import commandOverlayInjectable from "../command-palette/command-overlay.injectable"; -@observer -export class HotbarRenameCommand extends React.Component { - @observable hotbarId = ""; - @observable hotbarName = ""; - - constructor(props: {}) { - super(props); - makeObservable(this); - } - - @computed get options() { - return HotbarStore.getInstance().hotbars.map((hotbar) => { - return { value: hotbar.id, label: hotbarDisplayLabel(hotbar.id) }; - }); - } - - @action onSelect = (id: string) => { - this.hotbarId = id; - this.hotbarName = HotbarStore.getInstance().getById(this.hotbarId).name; +interface Dependencies { + closeCommandOverlay: () => void; + hotbarManager: { + hotbars: Hotbar[]; + getById: (id: string) => Hotbar | undefined; + setHotbarName: (id: string, name: string) => void; + getDisplayLabel: (hotbar: Hotbar) => string; }; +} - onSubmit = (name: string) => { +const NonInjectedHotbarRenameCommand = observer(({ closeCommandOverlay, hotbarManager }: Dependencies) => { + const [hotbarId, setHotbarId] = useState(""); + const [hotbarName, setHotbarName] = useState(""); + + const options = hotbarManager.hotbars.map(hotbar => ({ + value: hotbar.id, + label: hotbarManager.getDisplayLabel(hotbar), + })); + + const onSelect = (id: string) => { + setHotbarId(id); + setHotbarName(hotbarManager.getById(id).name); + }; + const onSubmit = (name: string) => { if (!name.trim()) { return; } - const hotbarStore = HotbarStore.getInstance(); - const hotbar = HotbarStore.getInstance().getById(this.hotbarId); - - if (!hotbar) { - return; - } - - hotbarStore.setHotbarName(this.hotbarId, name); - CommandOverlay.close(); + hotbarManager.setHotbarName(hotbarId, name); + closeCommandOverlay(); }; - renderHotbarList() { - return ( - <> - this.hotbarName = v} + value={hotbarName} + onChange={setHotbarName} placeholder="New hotbar name" autoFocus={true} theme="round-black" validators={uniqueHotbarName} - onSubmit={this.onSubmit} + onSubmit={onSubmit} showValidationLine={true} /> @@ -103,12 +82,24 @@ export class HotbarRenameCommand extends React.Component { ); } - render() { + return ( + this.onChange(v.value)} - components={{ DropdownIndicator: null, IndicatorSeparator: null }} - menuIsOpen={true} - options={this.options} - autoFocus={true} - escapeClearsValue={false} - placeholder="Switch to hotbar" /> - ); - } +interface HotbarManager { + hotbars: Hotbar[]; + setActiveHotbar: (id: string) => void; + getDisplayLabel: (hotbar: Hotbar) => string; } + +interface Dependencies { + hotbarManager: HotbarManager + commandOverlay: CommandOverlay; +} + +function getHotbarSwitchOptions(hotbarManager: HotbarManager) { + const options = hotbarManager.hotbars.map(hotbar => ({ + value: hotbar.id, + label: hotbarManager.getDisplayLabel(hotbar), + })); + + options.push({ value: addActionId, label: "Add hotbar ..." }); + + if (hotbarManager.hotbars.length > 1) { + options.push({ value: removeActionId, label: "Remove hotbar ..." }); + } + + options.push({ value: renameActionId, label: "Rename hotbar ..." }); + + return options; +} + +const NonInjectedHotbarSwitchCommand = observer(({ hotbarManager, commandOverlay }: Dependencies) => { + const options = getHotbarSwitchOptions(hotbarManager); + + const onChange = (idOrAction: string): void => { + switch (idOrAction) { + case addActionId: + return commandOverlay.open(); + case removeActionId: + return commandOverlay.open(); + case renameActionId: + return commandOverlay.open(); + default: + hotbarManager.setActiveHotbar(idOrAction); + commandOverlay.close(); + } + }; + + return ( +