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

Merge pull request #4424 from lensapp/issue-3258

Command Palette Improvements
This commit is contained in:
Janne Savolainen 2022-01-11 09:20:02 +02:00 committed by GitHub
commit f7f27a8304
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 1266 additions and 919 deletions

View File

@ -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();

View File

@ -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());

View File

@ -0,0 +1,29 @@
/**
* 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 { HotbarStore } from "./hotbar-store";
const hotbarManagerInjectable = getInjectable({
instantiate: () => HotbarStore.getInstance(),
lifecycle: lifecycleEnum.singleton,
});
export default hotbarManagerInjectable;

View File

@ -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,7 @@ 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";
export interface HotbarStoreModel {
hotbars: Hotbar[];
@ -52,22 +52,40 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
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 +105,11 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
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 +134,7 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
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 +142,29 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
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;
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 +278,7 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
index = hotbarStore.hotbars.length - 1;
}
hotbarStore.activeHotbarId = hotbarStore.hotbars[index].id;
hotbarStore.setActiveHotbar(index);
}
switchToNext() {
@ -274,7 +289,7 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
index = 0;
}
hotbarStore.activeHotbarId = hotbarStore.hotbars[index].id;
hotbarStore.setActiveHotbar(index);
}
/**
@ -284,6 +299,20 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
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 +321,7 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
* @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);
}

View File

@ -33,14 +33,18 @@ export interface HotbarItem {
}
}
export type Hotbar = Required<HotbarCreateOptions>;
export type Hotbar = Required<CreateHotbarData>;
export interface HotbarCreateOptions {
export interface CreateHotbarData {
id?: string;
name: string;
items?: Tuple<HotbarItem | null, typeof defaultHotbarCells>;
}
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 {

View File

@ -20,6 +20,7 @@
*/
export const dialogShowOpenDialogHandler = "dialog:show-open-dialog";
export const catalogEntityRunListener = "catalog-entity:run";
export * from "./ipc";
export * from "./invalid-kubeconfig";

View File

@ -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<typeof watch>;
const mockedFse = fse as jest.Mocked<typeof fse>;
describe("ExtensionDiscovery", () => {
let extensionLoader: ExtensionLoader;
@ -77,22 +79,20 @@ describe("ExtensionDiscovery", () => {
extensionLoader = di.inject(extensionLoaderInjectable);
});
describe("with mockFs", () => {
beforeEach(() => {
mockFs({
[`${os.homedir()}/.k8slens/extensions/my-extension/package.json`]: JSON.stringify({
name: "my-extension",
}),
});
});
afterEach(() => {
mockFs.restore();
});
it("emits add for added extension", async (done) => {
let addHandler: (filePath: string) => void;
mockedFse.readJson.mockImplementation((p) => {
expect(p).toBe(path.join(os.homedir(), ".k8slens/extensions/my-extension/package.json"));
return {
name: "my-extension",
version: "1.0.0",
};
});
mockedFse.pathExists.mockImplementation(() => true);
const mockWatchInstance: any = {
on: jest.fn((event: string, handler: typeof addHandler) => {
if (event === "add") {
@ -106,7 +106,6 @@ describe("ExtensionDiscovery", () => {
mockedWatch.mockImplementationOnce(() =>
(mockWatchInstance) as any,
);
const extensionDiscovery = ExtensionDiscovery.createInstance(
extensionLoader,
);
@ -125,6 +124,7 @@ describe("ExtensionDiscovery", () => {
isCompatible: false,
manifest: {
name: "my-extension",
version: "1.0.0",
},
manifestPath: path.normalize("node_modules/my-extension/package.json"),
});
@ -133,7 +133,6 @@ describe("ExtensionDiscovery", () => {
addHandler(path.join(extensionDiscovery.localFolderPath, "/my-extension/package.json"));
});
});
it("doesn't emit add for added file under extension", async done => {
let addHandler: (filePath: string) => void;
@ -172,3 +171,4 @@ describe("ExtensionDiscovery", () => {
}, 10);
});
});

View File

@ -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) => {

View File

@ -41,7 +41,6 @@ export const getDiForUnitTesting = () => {
aliases: [injectable, ...(injectable.aliases || [])],
};
})
.forEach(injectable => di.register(injectable));
di.preventSideEffects();

View File

@ -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<CatalogEntity>[] = [];

View File

@ -54,7 +54,7 @@ export class EntitySettingRegistry extends BaseRegistry<EntitySettingRegistratio
};
}
getItemsForKind(kind: string, apiVersion: string, source?: string) {
getItemsForKind = (kind: string, apiVersion: string, source?: string) => {
let items = this.getItems().filter((item) => {
return item.kind === kind && item.apiVersions.includes(apiVersion);
});
@ -66,5 +66,5 @@ export class EntitySettingRegistry extends BaseRegistry<EntitySettingRegistratio
}
return items.sort((a, b) => (b.priority ?? 50) - (a.priority ?? 50));
}
};
}

View File

@ -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";

View File

@ -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",

View File

@ -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)$/);

View File

@ -216,8 +216,16 @@ export function getAppMenu(
label: "Command Palette...",
accelerator: "Shift+CmdOrCtrl+P",
id: "command-palette",
click() {
click(_m, _b, event) {
/**
* 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");
}
},
},
{ type: "separator" },

View File

@ -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";
@ -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<void>;
@ -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(catalogEntityRunListener, (event, id: string) => {
const entity = this.getById(id);
if (entity) {
this.onRun(entity);
}
});
}
}
@action updateItems(items: (CatalogEntityData & CatalogEntityKindData)[]) {

View File

@ -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) {

View File

@ -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";
@ -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();
@ -102,9 +103,6 @@ export async function bootstrap(comp: () => Promise<AppComponent>, di: Dependenc
logger.info(`${logPrefix} initializing Registries`);
initializers.initRegistries();
logger.info(`${logPrefix} initializing CommandRegistry`);
initializers.initCommandRegistry();
logger.info(`${logPrefix} initializing EntitySettingsRegistry`);
initializers.initEntitySettingsRegistry();
@ -124,7 +122,9 @@ export async function bootstrap(comp: () => Promise<AppComponent>, di: Dependenc
initializers.initCatalogCategoryRegistryEntries();
logger.info(`${logPrefix} initializing Catalog`);
initializers.initCatalog();
initializers.initCatalog({
openCommandDialog: di.inject(commandOverlayInjectable).open,
});
const extensionLoader = di.inject(extensionLoaderInjectable);

View File

@ -66,22 +66,15 @@ export class CRDStore extends KubeObjectStore<CustomResourceDefinition> {
@computed get groups() {
const groups: Record<string, CustomResourceDefinition[]> = {};
return this.items.reduce((groups, crd) => {
const group = crd.getGroup();
if (!groups[group]) groups[group] = [];
groups[group].push(crd);
for (const crd of this.items) {
(groups[crd.getGroup()] ??= []).push(crd);
}
return groups;
}, 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) {

View File

@ -19,18 +19,14 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { HotbarStore } from "../../../common/hotbar-store";
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { computed } from "mobx";
import { crdStore } from "./crd.store";
function hotbarIndex(id: string) {
return HotbarStore.getInstance().hotbarIndex(id) + 1;
}
const customResourceDefinitionsInjectable = getInjectable({
instantiate: () => computed(() => [...crdStore.items]),
export function hotbarDisplayLabel(id: string) : string {
const hotbar = HotbarStore.getInstance().getById(id);
lifecycle: lifecycleEnum.singleton,
});
return `${hotbarIndex(id)}: ${hotbar.name}`;
}
export function hotbarDisplayIndex(id: string) : string {
return hotbarIndex(id).toString();
}
export default customResourceDefinitionsInjectable;

View File

@ -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();

View File

@ -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";

View File

@ -19,40 +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, 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 => ({
interface Dependencies {
closeCommandOverlay: () => void;
entities: IComputedValue<CatalogEntity[]>;
}
const NonInjectedActivateEntityCommand = observer(({ closeCommandOverlay, entities }: Dependencies) => {
const options = entities.get().map(entity => ({
label: `${entity.kind}: ${entity.getName()}`,
value: entity,
}));
}
onSelect(entity: CatalogEntity): void {
catalogEntityRegistry.onRun(entity);
CommandOverlay.close();
}
const onSelect = (entity: CatalogEntity): void => {
broadcastMessage(catalogEntityRunListener, entity.getId());
closeCommandOverlay();
};
render() {
return (
<Select
menuPortalTarget={null}
onChange={(v) => this.onSelect(v.value)}
onChange={(v) => onSelect(v.value)}
components={{ DropdownIndicator: null, IndicatorSeparator: null }}
menuIsOpen={true}
options={this.options}
options={options}
autoFocus={true}
escapeClearsValue={false}
placeholder="Activate entity ..."
/>
);
}
}
});
export const ActivateEntityCommand = withInjectables<Dependencies>(NonInjectedActivateEntityCommand, {
getProps: di => ({
closeCommandOverlay: di.inject(commandOverlayInjectable).close,
entities: computed(() => [...catalogEntityRegistry.items]),
}),
});

View File

@ -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<Dependencies> {
@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<Dependencies>(NonInjectedWeblinkAddCommand, {
getProps: (di, props) => ({
closeCommandOverlay: di.inject(commandOverlayInjectable).close,
...props,
}),
});

View File

@ -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 (
interface Dependencies {
openCommandOverlay: (component: React.ReactElement) => void;
activeHotbarName: () => string | undefined;
}
const NonInjectedActiveHotbarName = observer(({ openCommandOverlay, activeHotbarName }: Dependencies) => (
<div
className="flex items-center"
data-testid="current-hotbar-name"
onClick={() => CommandOverlay.open(<HotbarSwitchCommand />)}
onClick={() => openCommandOverlay(<HotbarSwitchCommand />)}
>
<Icon material="bookmarks" className="mr-2" size={14} />
{HotbarStore.getInstance().getActive()?.name}
{activeHotbarName()}
</div>
);
));
export const ActiveHotbarName = withInjectables<Dependencies>(NonInjectedActiveHotbarName, {
getProps: (di, props) => ({
activeHotbarName: () => di.inject(hotbarManagerInjectable).getActive()?.name,
openCommandOverlay: di.inject(commandOverlayInjectable).open,
...props,
}),
});

View File

@ -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("<BottomBar />", () => {
let di: ConfigurableDependencyInjectionContainer;
let render: DiRender;
beforeEach(() => {
const mockOpts = {
"tmp": {
@ -63,14 +66,19 @@ describe("<BottomBar />", () => {
},
};
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("<BottomBar />", () => {
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(<BottomBar />)).not.toThrow();
StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => "hello");
expect(() => render(<BottomBar />)).not.toThrow();
StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => 6);
expect(() => render(<BottomBar />)).not.toThrow();
StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => null);
expect(() => render(<BottomBar />)).not.toThrow();
StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => []);
expect(() => render(<BottomBar />)).not.toThrow();
StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [{}]);
expect(() => render(<BottomBar />)).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(<BottomBar />)).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("<BottomBar />", () => {
]);
const { getByTestId } = render(<BottomBar />);
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("<BottomBar />", () => {
]);
const { getByTestId } = render(<BottomBar />);
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: () => <ActiveHotbarName/> },
]);
const { getByTestId } = render(<BottomBar />);
expect(getByTestId("current-hotbar-name")).toHaveTextContent("default");
});
it("show active hotbar name", () => {
StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [
{ item: () => <ActiveHotbarName/> },
]);
const { getByTestId } = render(<BottomBar />);
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: () => <ActiveHotbarName/> },
]);
@ -153,7 +149,8 @@ describe("<BottomBar />", () => {
fireEvent.click(activeHotbar);
expect(CommandOverlay.open).toHaveBeenCalledWith(<HotbarSwitchCommand />);
expect(mockOpen).toHaveBeenCalledWith(<HotbarSwitchCommand />);
});
it("sort positioned items properly", () => {

View File

@ -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";

View File

@ -21,68 +21,100 @@
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 commandOverlayInjectable, { CommandOverlay } from "./command-overlay.injectable";
import { isMac } from "../../../common/vars";
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
import { CommandRegistration, CommandRegistry } from "../../../extensions/registries/command-registry";
import { CommandOverlay } from "./command-overlay";
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;
}
interface Dependencies {
addWindowEventListener: <K extends keyof WindowEventMap>(type: K, listener: (this: Window, ev: WindowEventMap[K]) => any, options?: boolean | AddEventListenerOptions) => Disposer;
commandOverlay: CommandOverlay,
}
@observer
export class CommandContainer extends React.Component<CommandContainerProps> {
class NonInjectedCommandContainer extends React.Component<CommandContainerProps & Dependencies> {
private escHandler(event: KeyboardEvent) {
const { commandOverlay } = this.props;
if (event.key === "Escape") {
event.stopPropagation();
CommandOverlay.close();
commandOverlay.close();
}
}
private findCommandById(commandId: string) {
return CommandRegistry.getInstance().getItems().find((command) => command.id === commandId);
}
handleCommandPalette = () => {
const { commandOverlay } = this.props;
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(<CommandDialog />);
}
};
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 { clusterId, addWindowEventListener, commandOverlay } = this.props;
if (command) {
this.runCommand(command);
}
});
} else {
ipcRendererOn("command-palette:open", () => {
CommandOverlay.open(<CommandDialog />);
});
}
window.addEventListener("keyup", (e) => this.escHandler(e), true);
const action = clusterId
? () => commandOverlay.open(<CommandDialog />)
: this.handleCommandPalette;
const ipcChannel = clusterId
? `command-palette:${clusterId}:open`
: "command-palette:open";
disposeOnUnmount(this, [
ipcRendererOn(ipcChannel, action),
addWindowEventListener("keydown", this.onKeyboardShortcut(action)),
addWindowEventListener("keyup", (e) => this.escHandler(e), true),
]);
}
render() {
const { commandOverlay } = this.props;
return (
<Dialog
isOpen={CommandOverlay.isOpen}
isOpen={commandOverlay.isOpen}
animated={true}
onClose={CommandOverlay.close}
onClose={commandOverlay.close}
modal={false}
>
<div id="command-container">
{CommandOverlay.component}
{commandOverlay.component}
</div>
</Dialog>
);
}
}
export const CommandContainer = withInjectables<Dependencies, CommandContainerProps>(NonInjectedCommandContainer, {
getProps: (di, props) => ({
addWindowEventListener: di.inject(windowAddEventListenerInjectable),
commandOverlay: di.inject(commandOverlayInjectable),
...props,
}),
});

View File

@ -21,108 +21,107 @@
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 { CommandOverlay } from "./command-overlay";
import { broadcastMessage } from "../../../common/ipc";
import { navigate } from "../../navigation";
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
import React, { useState } from "react";
import commandOverlayInjectable from "./command-overlay.injectable";
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;
constructor(props: {}) {
super(props);
makeObservable(this);
interface Dependencies {
commands: IComputedValue<Map<string, RegisteredCommand>>;
activeEntity?: CatalogEntity;
closeCommandOverlay: () => void;
}
@computed get activeEntity(): CatalogEntity | undefined {
return catalogEntityRegistry.activeEntity;
}
const NonInjectedCommandDialog = observer(({ commands, activeEntity, closeCommandOverlay }: Dependencies) => {
const [searchValue, setSearchValue] = useState("");
@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;
}
try {
CommandOverlay.close();
if (command.scope === "global") {
closeCommandOverlay();
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);
entity: activeEntity,
navigate: (url, opts = {}) => {
const { forceRootFrame = false } = opts;
if (forceRootFrame) {
broadcastMessage(IpcRendererNavigationEvents.NAVIGATE_IN_APP, url);
} else {
navigate(url);
}
},
});
} catch (error) {
console.error("[COMMAND-DIALOG] failed to execute command", command.id, error);
}
};
const context = {
entity: activeEntity,
};
const activeCommands = iter.filter(commands.get().values(), command => {
try {
return command.isActive(context);
} catch (error) {
console.error(`[COMMAND-DIALOG]: isActive for ${command.id} threw an error, defaulting to false`, error);
}
render() {
return false;
});
const options = Array.from(activeCommands, ({ id, title }) => ({
value: id,
label: typeof title === "function"
? title(context)
: title,
}));
// Make sure the options are in the correct order
orderBy(options, "label", "asc");
return (
<Select
menuPortalTarget={null}
onChange={v => this.onChange(v.value)}
onChange={v => executeAction(v.value)}
components={{
DropdownIndicator: null,
IndicatorSeparator: null,
}}
menuIsOpen={this.menuIsOpen}
options={this.options}
menuIsOpen
options={options}
autoFocus={true}
escapeClearsValue={false}
data-test-id="command-palette-search"
placeholder="Type a command or search&hellip;"
onInputChange={(newValue, { action }) => {
if (action === "input-change") {
this.searchValue = newValue;
setSearchValue(newValue);
}
}}
inputValue={this.searchValue}
inputValue={searchValue}
/>
);
}
}
});
export const CommandDialog = withInjectables<Dependencies>(NonInjectedCommandDialog, {
getProps: di => ({
commands: di.inject(registeredCommandsInjectable),
// TODO: replace with injection
activeEntity: catalogEntityRegistry.activeEntity,
closeCommandOverlay: di.inject(commandOverlayInjectable).close,
}),
});

View File

@ -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<React.ReactElement | null>(null, { deep: false });
#component = observable.box<React.ReactElement | null>(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);
};
close = () => {
this.#component.set(null);
};
get component(): React.ReactElement | null {
return this.#component.get();
}
}
static close() {
CommandOverlay.#component.set(null);
}
const commandOverlayInjectable = getInjectable({
instantiate: () => new CommandOverlay(),
lifecycle: lifecycleEnum.singleton,
});
static get component(): React.ReactElement | null {
return CommandOverlay.#component.get();
}
}
export default commandOverlayInjectable;

View File

@ -21,4 +21,4 @@
export * from "./command-container";
export * from "./command-dialog";
export * from "./command-overlay";
export * from "./command-overlay.injectable";

View File

@ -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<CommandRegistration> {
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<Omit<CommandRegistration, "scope">>;

View File

@ -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(<HotbarSwitchCommand />),
},
{
id: "hotbar.addHotbar",
title: "Hotbar: Add Hotbar ...",
action: () => openCommandDialog(<HotbarAddCommand />),
},
{
id: "hotbar.removeHotbar",
title: "Hotbar: Remove Hotbar ...",
action: () => openCommandDialog(<HotbarRemoveCommand />),
},
{
id: "hotbar.renameHotbar",
title: "Hotbar: Rename Hotbar ...",
action: () => openCommandDialog(<HotbarRenameCommand />),
},
{
id: "catalog.searchEntities",
title: "Catalog: Activate Entity ...",
action: () => openCommandDialog(<ActivateEntityCommand />),
},
];
}
const internalCommandsInjectable = getInjectable({
instantiate: (di) => getInternalCommands({
openCommandDialog: di.inject(commandOverlayInjectable).open,
getEntitySettingItems: EntitySettingRegistry
.getInstance()
.getItemsForKind,
}),
lifecycle: lifecycleEnum.singleton,
});
export default internalCommandsInjectable;

View File

@ -0,0 +1,69 @@
/**
* 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 "../../+custom-resources/custom-resources.injectable";
import type { LensRendererExtension } from "../../../../extensions/lens-renderer-extension";
import rendererExtensionsInjectable from "../../../../extensions/renderer-extensions.injectable";
import type { CommandRegistration, RegisteredCommand } from "./commands";
import internalCommandsInjectable, { isKubernetesClusterActive } from "./internal-commands.injectable";
interface Dependencies {
extensions: IComputedValue<LensRendererExtension[]>;
customResourceDefinitions: IComputedValue<CustomResourceDefinition[]>;
internalCommands: CommandRegistration[];
}
const instantiateRegisteredCommands = ({ extensions, customResourceDefinitions, internalCommands }: Dependencies) => computed(() => {
const result = new Map<string, RegisteredCommand>();
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)) {
result.set(command.id, { ...command, isActive });
}
}
return result;
});
const registeredCommandsInjectable = getInjectable({
instantiate: (di) => instantiateRegisteredCommands({
extensions: di.inject(rendererExtensionsInjectable),
customResourceDefinitions: di.inject(customResourceDefinitionsInjectable),
internalCommands: di.inject(internalCommandsInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default registeredCommandsInjectable;

View File

@ -21,13 +21,17 @@
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 from "../../../../common/hotbar-store.injectable";
import { UserStore } from "../../../../common/user-store";
import { ThemeStore } from "../../../theme.store";
import { ConfirmDialog } from "../../confirm-dialog";
import type { HotbarStore } from "../../../../common/hotbar-store";
jest.mock("electron", () => ({
app: {
@ -55,45 +59,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("<HotbarRemoveCommand />", () => {
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(<HotbarRemoveCommand/>);
expect(container).toBeInstanceOf(HTMLElement);
});
it("displays error notification if user tries to remove last hotbar", () => {
const spy = jest.spyOn(Notifications, "error");
const { getByText } = render(<HotbarRemoveCommand/>);
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(
<>
<HotbarRemoveCommand />
<ConfirmDialog />
</>,
);
fireEvent.click(getByText("1: Default"));
fireEvent.click(getByText("Remove Hotbar"));
expect(spy).toHaveBeenCalled();
spy.mockRestore();
expect(removeMock).toHaveBeenCalled();
});
});

View File

@ -21,28 +21,29 @@
import React from "react";
import { observer } from "mobx-react";
import { HotbarStore } from "../../../common/hotbar-store";
import { CommandOverlay } from "../command-palette";
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";
import hotbarManagerInjectable from "../../../common/hotbar-store.injectable";
import uniqueHotbarNameInjectable from "../input/validators/unique-hotbar-name.injectable";
export const uniqueHotbarName: InputValidator = {
condition: ({ required }) => required,
message: () => "Hotbar with this name already exists",
validate: value => !HotbarStore.getInstance().getByName(value),
};
interface Dependencies {
closeCommandOverlay: () => void;
addHotbar: (data: CreateHotbarData, { setActive }?: CreateHotbarOptions) => void;
uniqueHotbarName: InputValidator;
}
@observer
export class HotbarAddCommand extends React.Component {
onSubmit = (name: string) => {
const NonInjectedHotbarAddCommand = observer(({ closeCommandOverlay, addHotbar, uniqueHotbarName }: Dependencies) => {
const onSubmit = (name: string) => {
if (!name.trim()) {
return;
}
HotbarStore.getInstance().add({ name }, { setActive: true });
CommandOverlay.close();
addHotbar({ name }, { setActive: true });
closeCommandOverlay();
};
render() {
return (
<>
<Input
@ -51,7 +52,7 @@ export class HotbarAddCommand extends React.Component {
theme="round-black"
data-test-id="command-palette-hotbar-add-name"
validators={uniqueHotbarName}
onSubmit={this.onSubmit}
onSubmit={onSubmit}
dirty={true}
showValidationLine={true}
/>
@ -60,5 +61,13 @@ export class HotbarAddCommand extends React.Component {
</small>
</>
);
}
}
});
export const HotbarAddCommand = withInjectables<Dependencies>(NonInjectedHotbarAddCommand, {
getProps: (di, props) => ({
closeCommandOverlay: di.inject(commandOverlayInjectable).close,
addHotbar: di.inject(hotbarManagerInjectable).add,
uniqueHotbarName: di.inject(uniqueHotbarNameInjectable),
...props,
}),
});

View File

@ -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: (
<div className="confirm flex column gaps">
<p>
@ -75,19 +68,26 @@ export class HotbarRemoveCommand extends React.Component {
</div>
),
});
}
};
render() {
return (
<Select
menuPortalTarget={null}
onChange={(v) => this.onChange(v.value)}
onChange={(v) => onChange(v.value)}
components={{ DropdownIndicator: null, IndicatorSeparator: null }}
menuIsOpen={true}
options={this.options}
options={options}
autoFocus={true}
escapeClearsValue={false}
placeholder="Remove hotbar" />
placeholder="Remove hotbar"
/>
);
}
}
});
export const HotbarRemoveCommand = withInjectables<Dependencies>(NonInjectedHotbarRemoveCommand, {
getProps: (di, props) => ({
closeCommandOverlay: di.inject(commandOverlayInjectable).close,
hotbarManager: di.inject(hotbarManagerInjectable),
...props,
}),
});

View File

@ -19,81 +19,61 @@
* 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 { Input } from "../input";
import { uniqueHotbarName } from "./hotbar-add-command";
import { CommandOverlay } from "../command-palette";
import hotbarManagerInjectable from "../../../common/hotbar-store.injectable";
import { Input, InputValidator } from "../input";
import type { Hotbar } from "../../../common/hotbar-types";
import { withInjectables } from "@ogre-tools/injectable-react";
import commandOverlayInjectable from "../command-palette/command-overlay.injectable";
import uniqueHotbarNameInjectable from "../input/validators/unique-hotbar-name.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;
};
uniqueHotbarName: InputValidator;
}
onSubmit = (name: string) => {
const NonInjectedHotbarRenameCommand = observer(({ closeCommandOverlay, hotbarManager, uniqueHotbarName }: 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 (
<>
<Select
menuPortalTarget={null}
onChange={(v) => this.onSelect(v.value)}
components={{ DropdownIndicator: null, IndicatorSeparator: null }}
menuIsOpen={true}
options={this.options}
autoFocus={true}
escapeClearsValue={false}
placeholder="Rename hotbar"/>
</>
);
}
renderNameInput() {
if (hotbarId) {
return (
<>
<Input
trim={true}
value={this.hotbarName}
onChange={v => 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}
/>
<small className="hint">
@ -103,12 +83,25 @@ export class HotbarRenameCommand extends React.Component {
);
}
render() {
return (
<>
{!this.hotbarId ? this.renderHotbarList() : this.renderNameInput()}
</>
<Select
menuPortalTarget={null}
onChange={(v) => onSelect(v.value)}
components={{ DropdownIndicator: null, IndicatorSeparator: null }}
menuIsOpen={true}
options={options}
autoFocus={true}
escapeClearsValue={false}
placeholder="Rename hotbar"
/>
);
}
}
});
export const HotbarRenameCommand = withInjectables<Dependencies>(NonInjectedHotbarRenameCommand, {
getProps: (di, props) => ({
closeCommandOverlay: di.inject(commandOverlayInjectable).close,
hotbarManager: di.inject(hotbarManagerInjectable),
uniqueHotbarName: di.inject(uniqueHotbarNameInjectable),
...props,
}),
});

View File

@ -23,30 +23,37 @@ import "./hotbar-selector.scss";
import React from "react";
import { Icon } from "../icon";
import { Badge } from "../badge";
import { HotbarStore } from "../../../common/hotbar-store";
import { CommandOverlay } from "../command-palette";
import hotbarManagerInjectable from "../../../common/hotbar-store.injectable";
import { HotbarSwitchCommand } from "./hotbar-switch-command";
import { hotbarDisplayIndex } from "./hotbar-display-label";
import { TooltipPosition } from "../tooltip";
import { observer } from "mobx-react";
import type { Hotbar } from "../../../common/hotbar-types";
import { withInjectables } from "@ogre-tools/injectable-react";
import commandOverlayInjectable from "../command-palette/command-overlay.injectable";
interface Props {
export interface HotbarSelectorProps {
hotbar: Hotbar;
}
export const HotbarSelector = observer(({ hotbar }: Props) => {
const store = HotbarStore.getInstance();
interface Dependencies {
hotbarManager: {
switchToPrevious: () => void;
switchToNext: () => void;
getActive: () => Hotbar;
getDisplayIndex: (hotbar: Hotbar) => string;
};
openCommandOverlay: (component: React.ReactElement) => void;
}
return (
const NonInjectedHotbarSelector = observer(({ hotbar, hotbarManager, openCommandOverlay }: HotbarSelectorProps & Dependencies) => (
<div className="HotbarSelector flex align-center">
<Icon material="play_arrow" className="previous box" onClick={() => store.switchToPrevious()} />
<Icon material="play_arrow" className="previous box" onClick={() => hotbarManager.switchToPrevious()} />
<div className="box grow flex align-center">
<Badge
id="hotbarIndex"
small
label={hotbarDisplayIndex(store.activeHotbarId)}
onClick={() => CommandOverlay.open(<HotbarSwitchCommand />)}
label={hotbarManager.getDisplayIndex(hotbarManager.getActive())}
onClick={() => openCommandOverlay(<HotbarSwitchCommand />)}
tooltip={{
preferredPositions: [TooltipPosition.TOP, TooltipPosition.TOP_LEFT],
children: hotbar.name,
@ -54,7 +61,14 @@ export const HotbarSelector = observer(({ hotbar }: Props) => {
className="SelectorIndex"
/>
</div>
<Icon material="play_arrow" className="next box" onClick={() => store.switchToNext()} />
<Icon material="play_arrow" className="next box" onClick={() => hotbarManager.switchToNext()} />
</div>
);
));
export const HotbarSelector = withInjectables<Dependencies, HotbarSelectorProps>(NonInjectedHotbarSelector, {
getProps: (di, props) => ({
hotbarManager: di.inject(hotbarManagerInjectable),
openCommandOverlay: di.inject(commandOverlayInjectable).open,
...props,
}),
});

View File

@ -22,73 +22,82 @@
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 { CommandOverlay } from "../command-palette";
import hotbarManagerInjectable from "../../../common/hotbar-store.injectable";
import type { CommandOverlay } from "../command-palette";
import { HotbarAddCommand } from "./hotbar-add-command";
import { HotbarRemoveCommand } from "./hotbar-remove-command";
import { hotbarDisplayLabel } from "./hotbar-display-label";
import { HotbarRenameCommand } from "./hotbar-rename-command";
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 HotbarSwitchCommand extends React.Component {
private static addActionId = "__add__";
private static removeActionId = "__remove__";
private static renameActionId = "__rename__";
const addActionId = "__add__";
const removeActionId = "__remove__";
const renameActionId = "__rename__";
constructor(props: {}) {
super(props);
makeObservable(this);
interface HotbarManager {
hotbars: Hotbar[];
setActiveHotbar: (id: string) => void;
getDisplayLabel: (hotbar: Hotbar) => string;
}
@computed get options() {
const hotbarStore = HotbarStore.getInstance();
const options = hotbarStore.hotbars.map((hotbar) => {
return { value: hotbar.id, label: hotbarDisplayLabel(hotbar.id) };
});
options.push({ value: HotbarSwitchCommand.addActionId, label: "Add hotbar ..." });
if (hotbarStore.hotbars.length > 1) {
options.push({ value: HotbarSwitchCommand.removeActionId, label: "Remove hotbar ..." });
interface Dependencies {
hotbarManager: HotbarManager
commandOverlay: CommandOverlay;
}
options.push({ value: HotbarSwitchCommand.renameActionId, label: "Rename hotbar ..." });
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;
}
onChange(idOrAction: string): void {
const NonInjectedHotbarSwitchCommand = observer(({ hotbarManager, commandOverlay }: Dependencies) => {
const options = getHotbarSwitchOptions(hotbarManager);
const onChange = (idOrAction: string): void => {
switch (idOrAction) {
case HotbarSwitchCommand.addActionId:
CommandOverlay.open(<HotbarAddCommand />);
return;
case HotbarSwitchCommand.removeActionId:
CommandOverlay.open(<HotbarRemoveCommand />);
return;
case HotbarSwitchCommand.renameActionId:
CommandOverlay.open(<HotbarRenameCommand />);
return;
case addActionId:
return commandOverlay.open(<HotbarAddCommand />);
case removeActionId:
return commandOverlay.open(<HotbarRemoveCommand />);
case renameActionId:
return commandOverlay.open(<HotbarRenameCommand />);
default:
HotbarStore.getInstance().activeHotbarId = idOrAction;
CommandOverlay.close();
}
hotbarManager.setActiveHotbar(idOrAction);
commandOverlay.close();
}
};
render() {
return (
<Select
menuPortalTarget={null}
onChange={(v) => this.onChange(v.value)}
onChange={(v) => onChange(v.value)}
components={{ DropdownIndicator: null, IndicatorSeparator: null }}
menuIsOpen={true}
options={this.options}
options={options}
autoFocus={true}
escapeClearsValue={false}
placeholder="Switch to hotbar" />
placeholder="Switch to hotbar"
/>
);
}
}
});
export const HotbarSwitchCommand = withInjectables<Dependencies>(NonInjectedHotbarSwitchCommand, {
getProps: (di, props) => ({
hotbarManager: di.inject(hotbarManagerInjectable),
commandOverlay: di.inject(commandOverlayInjectable),
...props,
}),
});

View File

@ -103,6 +103,10 @@ export class Input extends React.Component<InputProps, State> {
submitted: false,
};
componentWillUnmount(): void {
this.setDirtyOnChange.cancel();
}
setValue(value = "") {
if (value !== this.getValue()) {
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(this.input.constructor.prototype, "value").set;

View File

@ -0,0 +1,35 @@
/**
* 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 hotbarManagerInjectable from "../../../../common/hotbar-store.injectable";
import type { InputValidator } from "../input_validators";
const uniqueHotbarNameInjectable = getInjectable({
instantiate: di => ({
condition: ({ required }) => required,
message: () => "Hotbar with this name already exists",
validate: value => !di.inject(hotbarManagerInjectable).getByName(value),
} as InputValidator),
lifecycle: lifecycleEnum.singleton,
});
export default uniqueHotbarNameInjectable;

View File

@ -29,7 +29,7 @@ import type { KubeObjectMenuRegistration } from "../../../extensions/registries"
import { KubeObjectMenuRegistry } from "../../../extensions/registries";
import { ConfirmDialog } from "../confirm-dialog";
import asyncFn, { AsyncFnMock } from "@async-fn/jest";
import { getDiForUnitTesting } from "../getDiForUnitTesting";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import clusterInjectable from "./dependencies/cluster.injectable";
import hideDetailsInjectable from "./dependencies/hide-details.injectable";
@ -60,8 +60,7 @@ describe("kube-object-menu", () => {
}) as Cluster);
di.override(apiManagerInjectable, () => ({
// eslint-disable-next-line unused-imports/no-unused-vars-ts
getStore: api => undefined,
getStore: api => void api,
}) as ApiManager);
di.override(hideDetailsInjectable, () => () => {});
@ -280,5 +279,5 @@ const addDynamicMenuItem = ({
const kubeObjectMenuRegistry = di.inject(kubeObjectMenuRegistryInjectable);
kubeObjectMenuRegistry.add(dynamicMenuItemStub);
kubeObjectMenuRegistry.add([dynamicMenuItemStub]);
};

View File

@ -26,7 +26,7 @@ import { TopBar } from "./top-bar";
import { IpcMainWindowEvents } from "../../../../main/window-manager";
import { broadcastMessage } from "../../../../common/ipc";
import * as vars from "../../../../common/vars";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
import { DiRender, renderFor } from "../../test-utils/renderFor";
const mockConfig = vars as { isWindows: boolean; isLinux: boolean };

View File

@ -23,7 +23,7 @@ import React from "react";
import { fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import { TopBar } from "./top-bar";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
import type { ConfigurableDependencyInjectionContainer } from "@ogre-tools/injectable";
import { DiRender, renderFor } from "../../test-utils/renderFor";
import topBarItemsInjectable from "./top-bar-items/top-bar-items.injectable";

View File

@ -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)$/);

View File

@ -21,27 +21,20 @@
import glob from "glob";
import { memoize } from "lodash/fp";
import {
createContainer,
ConfigurableDependencyInjectionContainer,
} from "@ogre-tools/injectable";
import { createContainer } from "@ogre-tools/injectable";
export const getDiForUnitTesting = () => {
const di: ConfigurableDependencyInjectionContainer = createContainer();
const di = createContainer();
getInjectableFilePaths()
.map(key => {
const injectable = require(key).default;
for (const filePath of getInjectableFilePaths()) {
const injectableInstance = require(filePath).default;
return {
id: key,
...injectable,
aliases: [injectable, ...(injectable.aliases || [])],
};
})
.forEach(injectable => di.register(injectable));
di.register({
id: filePath,
...injectableInstance,
aliases: [injectableInstance, ...(injectableInstance.aliases || [])],
});
}
di.preventSideEffects();
@ -50,5 +43,6 @@ export const getDiForUnitTesting = () => {
const getInjectableFilePaths = memoize(() => [
...glob.sync("./**/*.injectable.{ts,tsx}", { cwd: __dirname }),
...glob.sync("../../extensions/**/*.injectable.{ts,tsx}", { cwd: __dirname }),
...glob.sync("../common/**/*.injectable.{ts,tsx}", { cwd: __dirname }),
...glob.sync("../extensions/**/*.injectable.{ts,tsx}", { cwd: __dirname }),
]);

View File

@ -22,32 +22,12 @@
import React from "react";
import fs from "fs";
import "../../common/catalog-entities/kubernetes-cluster";
import { WebLinkCategory } from "../../common/catalog-entities";
import { ClusterStore } from "../../common/cluster-store";
import { catalogCategoryRegistry } from "../api/catalog-category-registry";
import { WeblinkAddCommand } from "../components/catalog-entities/weblink-add-command";
import { CommandOverlay } from "../components/command-palette";
import { loadConfigFromString } from "../../common/kube-helpers";
import { DeleteClusterDialog } from "../components/delete-cluster-dialog";
function initWebLinks() {
WebLinkCategory.onAdd = () => CommandOverlay.open(<WeblinkAddCommand />);
}
function initKubernetesClusters() {
catalogCategoryRegistry
.getForGroupKind("entity.k8slens.dev", "KubernetesCluster")
.on("contextMenuOpen", (entity, context) => {
if (entity.metadata?.source == "local") {
context.menuItems.push({
title: "Delete",
icon: "delete",
onClick: () => onClusterDelete(entity.metadata.uid),
});
}
});
}
async function onClusterDelete(clusterId: string) {
const cluster = ClusterStore.getInstance().getById(clusterId);
@ -64,7 +44,29 @@ async function onClusterDelete(clusterId: string) {
DeleteClusterDialog.open({ cluster, config });
}
export function initCatalog() {
initWebLinks();
initKubernetesClusters();
interface Dependencies {
openCommandDialog: (component: React.ReactElement) => void;
}
export function initCatalog({ openCommandDialog }: Dependencies) {
catalogCategoryRegistry
.getForGroupKind("entity.k8slens.dev", "WebLink")
.on("catalogAddMenu", ctx => {
ctx.menuItems.push({
title: "Add web link",
icon: "public",
onClick: () => openCommandDialog(<WeblinkAddCommand />),
});
});
catalogCategoryRegistry
.getForGroupKind("entity.k8slens.dev", "KubernetesCluster")
.on("contextMenuOpen", (entity, context) => {
if (entity.metadata?.source == "local") {
context.menuItems.push({
title: "Delete",
icon: "delete",
onClick: () => onClusterDelete(entity.metadata.uid),
});
}
});
}

View File

@ -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(<HotbarSwitchCommand />),
},
{
id: "hotbar.addHotbar",
title: "Hotbar: Add Hotbar ...",
scope: "global",
action: () => CommandOverlay.open(<HotbarAddCommand />),
},
{
id: "hotbar.removeHotbar",
title: "Hotbar: Remove Hotbar ...",
scope: "global",
action: () => CommandOverlay.open(<HotbarRemoveCommand />),
},
{
id: "hotbar.renameHotbar",
title: "Hotbar: Rename Hotbar ...",
scope: "global",
action: () => CommandOverlay.open(<HotbarRenameCommand />),
},
{
id: "catalog.searchEntities",
title: "Catalog: Activate Entity ...",
scope: "global",
action: () => CommandOverlay.open(<ActivateEntityCommand />),
},
]);
}

View File

@ -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";

View File

@ -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();

View File

@ -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<ClusterViewRouteParams>(navigation.location.pathname, {
exact: true,
path: clusterViewRoute.path,

View File

@ -0,0 +1,36 @@
/**
* 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 type { Disposer } from "../utils";
function addWindowEventListener<K extends keyof WindowEventMap>(type: K, listener: (this: Window, ev: WindowEventMap[K]) => any, options?: boolean | AddEventListenerOptions): Disposer {
window.addEventListener(type, listener, options);
return () => void window.removeEventListener(type, listener);
}
const windowAddEventListenerInjectable = getInjectable({
instantiate: () => addWindowEventListener,
lifecycle: lifecycleEnum.singleton,
});
export default windowAddEventListenerInjectable;