From 44a88246356f9f7340faab7db22a818f17de4eda Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Tue, 5 Oct 2021 11:10:25 -0400 Subject: [PATCH] Support multiple onBeforeHooks per entity Signed-off-by: Sebastian Malton --- src/extensions/renderer-api/catalog.ts | 8 +-- src/renderer/api/catalog-entity-registry.ts | 62 ++++++++++++++++--- .../+catalog/catalog-entity-item.tsx | 32 +--------- .../+catalog/catalog-entity.store.tsx | 33 ++++------ src/renderer/components/+catalog/catalog.tsx | 10 +-- .../components/hotbar/hotbar-menu.tsx | 40 +----------- 6 files changed, 74 insertions(+), 111 deletions(-) diff --git a/src/extensions/renderer-api/catalog.ts b/src/extensions/renderer-api/catalog.ts index 0af7c17c87..675abb42e6 100644 --- a/src/extensions/renderer-api/catalog.ts +++ b/src/extensions/renderer-api/catalog.ts @@ -55,15 +55,15 @@ export class CatalogEntityRegistry { * @param onBeforeRun The function that should return a boolean if the onBeforeRun of catalog entity should be triggered. * @returns A function to remove that hook */ - addOnBeforeRun(catalogEntityUid: CatalogEntity["metadata"]["uid"], onBeforeRun: CatalogEntityOnBeforeRun) { - return registry.addOnBeforeRun(catalogEntityUid, onBeforeRun); + addOnBeforeRun(entity: CatalogEntity, onBeforeRun: CatalogEntityOnBeforeRun) { + return registry.addOnBeforeRun(entity, onBeforeRun); } /** * Returns one catalog entity onBeforeRun by catalog entity uid */ - onBeforeRun(catalogEntityUid: CatalogEntity["metadata"]["uid"]): CatalogEntityOnBeforeRun | undefined { - return registry.getOnBeforeRun(catalogEntityUid); + onBeforeRun(entity: CatalogEntity): Promise { + return registry.onBeforeRun(entity); } } diff --git a/src/renderer/api/catalog-entity-registry.ts b/src/renderer/api/catalog-entity-registry.ts index f7a55b37ee..940bbd8463 100644 --- a/src/renderer/api/catalog-entity-registry.ts +++ b/src/renderer/api/catalog-entity-registry.ts @@ -19,7 +19,7 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { computed, observable, makeObservable, action } from "mobx"; +import { computed, observable, makeObservable, action, ObservableSet } from "mobx"; import { ipcRendererOn } from "../../common/ipc"; import { CatalogCategory, CatalogEntity, CatalogEntityData, catalogCategoryRegistry, CatalogCategoryRegistry, CatalogEntityKindData } from "../../common/catalog"; import "../../common/catalog-entities"; @@ -27,6 +27,8 @@ import type { Cluster } from "../../main/cluster"; import { ClusterStore } from "../../common/cluster-store"; import { Disposer, iter } from "../utils"; import { once } from "lodash"; +import logger from "../../common/logger"; +import { catalogEntityRunContext } from "./catalog-entity"; export type EntityFilter = (entity: CatalogEntity) => any; export type CatalogEntityOnBeforeRun = (entity: CatalogEntity) => boolean | Promise; @@ -39,7 +41,7 @@ export class CatalogEntityRegistry { protected filters = observable.set([], { deep: false, }); - protected entityOnBeforeRun = observable.map({}, { + protected onBeforeRunHooks = observable.map>({}, { deep: false, }); @@ -177,22 +179,62 @@ export class CatalogEntityRegistry { } /** - * Add a onRun hook to a catalog entity. - * @param uid The uid of the catalog entity + * Add a onRun hook to a catalog entity. If `onBeforeRun` was previously added then it will not be added again + * @param catalogEntityUid The uid of the catalog entity * @param onBeforeRun The function that should return a boolean if the onRun of catalog entity should be triggered. * @returns A function to remove that hook */ - addOnBeforeRun(catalogEntityUid: CatalogEntityUid, onBeforeRun: CatalogEntityOnBeforeRun): Disposer { - this.entityOnBeforeRun.set(catalogEntityUid, onBeforeRun); + addOnBeforeRun(entityOrId: CatalogEntity | CatalogEntityUid, onBeforeRun: CatalogEntityOnBeforeRun): Disposer { + const id = typeof entityOrId === "string" + ? entityOrId + : entityOrId.getId(); + const hooks = this.onBeforeRunHooks.get(id) ?? + this.onBeforeRunHooks.set(id, observable.set([], { deep: false })).get(id); + + hooks.add(onBeforeRun); - return once(() => void this.entityOnBeforeRun.delete(catalogEntityUid)); + return once(() => void hooks.delete(onBeforeRun)); } /** - * Returns one catalog entity onBeforeRun by catalog entity uid + * Runs all the registered `onBeforeRun` hooks, short circuiting on the first falsy returned/resolved valued + * @param entity The entity to run the hooks on + * @returns Whether the entities `onRun` method should be executed */ - getOnBeforeRun(catalogEntityUid: CatalogEntityUid): CatalogEntityOnBeforeRun | undefined { - return this.entityOnBeforeRun.get(catalogEntityUid); + async onBeforeRun(entity: CatalogEntity): Promise { + const hooks = this.onBeforeRunHooks.get(entity.getId()); + + if (!hooks) { + return true; + } + + for (const onBeforeRun of hooks) { + try { + if (!await onBeforeRun(entity)) { + return false; + } + } catch (error) { + logger.warn(`[CATALOG-ENTITY-REGISTRY]: entity ${entity.getId()} onBeforeRun threw an error`, error); + + // If a handler throws treat it as if it has returned `false` + // Namely: assume that its internal logic has failed and didn't complete as expected + return false; + } + } + + return true; + } + + onRun(entity: CatalogEntity): void { + this.onBeforeRun(entity) + .then(doOnRun => { + if (doOnRun) { + return entity.onRun(catalogEntityRunContext); + } else { + logger.debug(`onBeforeRun for ${entity.getId()} returned false`); + } + }) + .catch(error => logger.error(`[CATALOG-ENTITY-REGISTRY]: entity ${entity.getId()} onRun threw an error`, error)); } } diff --git a/src/renderer/components/+catalog/catalog-entity-item.tsx b/src/renderer/components/+catalog/catalog-entity-item.tsx index 903b676cee..5ed01aded6 100644 --- a/src/renderer/components/+catalog/catalog-entity-item.tsx +++ b/src/renderer/components/+catalog/catalog-entity-item.tsx @@ -22,19 +22,15 @@ import styles from "./catalog.module.css"; import React from "react"; import { action, computed } from "mobx"; import type { CatalogEntity, CatalogEntityActionContext } from "../../api/catalog-entity"; -import type { CatalogEntityOnBeforeRun } from "../../api/catalog-entity-registry"; import type { ItemObject } from "../../../common/item.store"; import { Badge } from "../badge"; import { navigation } from "../../navigation"; import { searchUrlParam } from "../input"; import { makeCss } from "../../../common/utils/makeCss"; import { KubeObject } from "../../../common/k8s-api/kube-object"; -import { toJS } from "mobx"; const css = makeCss(styles); -const isPromise = (obj: any): obj is Promise => (obj?.then && typeof obj?.then === "function") ? true: false; - export class CatalogEntityItem implements ItemObject { constructor(public entity: T) {} @@ -106,32 +102,8 @@ export class CatalogEntityItem implements ItemObject { ]; } - onRun(onBeforeRun: CatalogEntityOnBeforeRun | undefined, ctx: CatalogEntityActionContext) { - if (!onBeforeRun) { - this.entity.onRun(ctx); - - return; - } - - if (typeof onBeforeRun === "function") { - let shouldRun; - - try { - shouldRun = onBeforeRun(toJS(this.entity)); - } catch (error) { - if (process?.env?.NODE_ENV !== "test") console.warn(`[CATALOG-ENTITY-ITEM] onBeforeRun of entity.metadata.uid ${this.entity?.metadata?.uid} throw an exception, stop before onRun`, error); - } - - if (isPromise(shouldRun)) { - Promise.resolve(shouldRun).then((shouldRun) => { - if (shouldRun) this.entity.onRun(ctx); - }).catch((error) => { - if (process?.env?.NODE_ENV !== "test") console.warn(`[CATALOG-ENTITY-ITEM] onBeforeRun of entity.metadata.uid ${this.entity?.metadata?.uid} rejects, stop before onRun`, error); - }); - } else if (shouldRun) { - this.entity.onRun(ctx); - } - } + onRun(ctx: CatalogEntityActionContext) { + this.entity.onRun(ctx); } @action diff --git a/src/renderer/components/+catalog/catalog-entity.store.tsx b/src/renderer/components/+catalog/catalog-entity.store.tsx index bace71d406..b15e82353d 100644 --- a/src/renderer/components/+catalog/catalog-entity.store.tsx +++ b/src/renderer/components/+catalog/catalog-entity.store.tsx @@ -19,28 +19,19 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { computed, IReactionDisposer, makeObservable, observable, reaction } from "mobx"; -import { catalogEntityRegistry as _catalogEntityRegistry, CatalogEntityRegistry } from "../../api/catalog-entity-registry"; +import { computed, makeObservable, observable, reaction } from "mobx"; +import { catalogEntityRegistry, CatalogEntityRegistry } from "../../api/catalog-entity-registry"; import type { CatalogEntity } from "../../api/catalog-entity"; -import type { CatalogEntityOnBeforeRun } from "../../api/catalog-entity-registry"; import { ItemStore } from "../../../common/item.store"; import { CatalogCategory, catalogCategoryRegistry } from "../../../common/catalog"; -import { autoBind } from "../../../common/utils"; +import { autoBind, disposer } from "../../../common/utils"; import { CatalogEntityItem } from "./catalog-entity-item"; export class CatalogEntityStore extends ItemStore> { - #catalogEntityRegistry: CatalogEntityRegistry; - - constructor(catalogEntityRegistry?: CatalogEntityRegistry) { + constructor(private registry: CatalogEntityRegistry = catalogEntityRegistry) { super(); makeObservable(this); autoBind(this); - - if (catalogEntityRegistry) { - this.#catalogEntityRegistry = catalogEntityRegistry; - } else { - this.#catalogEntityRegistry = _catalogEntityRegistry; - } } @observable activeCategory?: CatalogCategory; @@ -48,27 +39,25 @@ export class CatalogEntityStore extends ItemStore new CatalogEntityItem(entity)); + return this.registry.filteredItems.map(entity => new CatalogEntityItem(entity)); } - return this.#catalogEntityRegistry.getItemsForCategory(this.activeCategory, { filtered: true }).map(entity => new CatalogEntityItem(entity)); + return this.registry.getItemsForCategory(this.activeCategory, { filtered: true }).map(entity => new CatalogEntityItem(entity)); } @computed get selectedItem() { return this.entities.find(e => e.getId() === this.selectedItemId); } - getCatalogEntityOnBeforeRun(catalogEntityUid: CatalogEntity["metadata"]["uid"]): CatalogEntityOnBeforeRun | undefined { - return this.#catalogEntityRegistry.getOnBeforeRun(catalogEntityUid); + onRun(entity: CatalogEntity): void { + this.registry.onRun(entity); } watch() { - const disposers: IReactionDisposer[] = [ + return disposer( reaction(() => this.entities, () => this.loadAll()), - reaction(() => this.activeCategory, () => this.loadAll(), { delay: 100}) - ]; - - return () => disposers.forEach((dispose) => dispose()); + reaction(() => this.activeCategory, () => this.loadAll(), { delay: 100}), + ); } loadAll() { diff --git a/src/renderer/components/+catalog/catalog.tsx b/src/renderer/components/+catalog/catalog.tsx index 8793a03997..e908931f25 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 type { CatalogEntityItem } from "./catalog-entity-item"; import { navigate } from "../../navigation"; import { MenuItem, MenuActions } from "../menu"; -import { CatalogEntityContextMenu, CatalogEntityContextMenuContext, catalogEntityRunContext } from "../../api/catalog-entity"; +import type { CatalogEntityContextMenu, CatalogEntityContextMenuContext } from "../../api/catalog-entity"; import { HotbarStore } from "../../../common/hotbar-store"; import { ConfirmDialog } from "../confirm-dialog"; import { catalogCategoryRegistry, CatalogEntity } from "../../../common/catalog"; @@ -133,16 +133,12 @@ export class Catalog extends React.Component { if (this.catalogEntityStore.selectedItemId) { this.catalogEntityStore.selectedItemId = null; } else { - const onBeforeRun = this.catalogEntityStore.getCatalogEntityOnBeforeRun(item.entity.metadata.uid); - - item.onRun(onBeforeRun, catalogEntityRunContext); + this.catalogEntityStore.onRun(item.entity); } }; onClickDetailPanelIcon = (item: CatalogEntityItem) => { - const onBeforeRun = this.catalogEntityStore.getCatalogEntityOnBeforeRun(item.entity.metadata.uid); - - item.onRun(onBeforeRun, catalogEntityRunContext); + this.catalogEntityStore.onRun(item.entity); }; onMenuItemClick(menuItem: CatalogEntityContextMenu) { diff --git a/src/renderer/components/hotbar/hotbar-menu.tsx b/src/renderer/components/hotbar/hotbar-menu.tsx index 51d346df63..9055279437 100644 --- a/src/renderer/components/hotbar/hotbar-menu.tsx +++ b/src/renderer/components/hotbar/hotbar-menu.tsx @@ -26,22 +26,18 @@ import { observer } from "mobx-react"; import { HotbarEntityIcon } from "./hotbar-entity-icon"; import { cssNames, IClassName } from "../../utils"; import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; -import type { CatalogEntityOnBeforeRun} from "../../api/catalog-entity-registry"; import { HotbarStore } from "../../../common/hotbar-store"; -import { CatalogEntity, catalogEntityRunContext } from "../../api/catalog-entity"; +import type { CatalogEntity } from "../../api/catalog-entity"; import { DragDropContext, Draggable, Droppable, DropResult } from "react-beautiful-dnd"; import { HotbarSelector } from "./hotbar-selector"; import { HotbarCell } from "./hotbar-cell"; import { HotbarIcon } from "./hotbar-icon"; import { defaultHotbarCells, HotbarItem } from "../../../common/hotbar-types"; -import { toJS } from "mobx"; interface Props { className?: IClassName; } -const isPromise = (obj: any): obj is Promise => (obj?.then && typeof obj?.then === "function") ? true: false; - @observer export class HotbarMenu extends React.Component { get hotbar() { @@ -58,10 +54,6 @@ export class HotbarMenu extends React.Component { return catalogEntityRegistry.getById(item?.entity.uid) ?? null; } - getEntityOnBeforeRun(uid: string): CatalogEntityOnBeforeRun | undefined { - return catalogEntityRegistry.getOnBeforeRun(uid); - } - onDragEnd(result: DropResult) { const { source, destination } = result; @@ -132,35 +124,7 @@ export class HotbarMenu extends React.Component { key={index} index={index} entity={entity} - onClick={() => { - const onBeforeRun = this.getEntityOnBeforeRun(entity.metadata.uid); - - if (!onBeforeRun) { - entity.onRun(catalogEntityRunContext); - - return; - } - - if (typeof onBeforeRun === "function") { - let shouldRun; - - try { - shouldRun = onBeforeRun(toJS(entity)); - } catch (error) { - if (process?.env?.NODE_ENV !== "test") console.warn(`[HOT-BAR] onBeforeRun of entity.metadata.uid ${entity?.metadata?.uid} throw an exception, stop before onRun`, error); - } - - if (isPromise(shouldRun)) { - Promise.resolve(shouldRun).then((shouldRun) => { - if (shouldRun) entity.onRun(catalogEntityRunContext); - }).catch((error) => { - if (process?.env?.NODE_ENV !== "test") console.warn(`[HOT-BAR] onBeforeRun of entity.metadata.uid ${entity?.metadata?.uid} rejects, stop before onRun`, error); - }); - } else if (shouldRun) { - entity.onRun(catalogEntityRunContext); - } - } - }} + onClick={() => catalogEntityRegistry.onRun(entity)} className={cssNames({ isDragging: snapshot.isDragging })} remove={this.removeItem} add={this.addItem}