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

Before hook for onRun of Catalog entities (#3911)

Co-authored-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
chh 2021-10-05 22:36:44 +03:00 committed by GitHub
parent 851274d314
commit 4af796c532
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 636 additions and 128 deletions

View File

@ -358,6 +358,7 @@
"postcss": "^8.3.6", "postcss": "^8.3.6",
"postcss-loader": "4.3.0", "postcss-loader": "4.3.0",
"postinstall-postinstall": "^2.1.0", "postinstall-postinstall": "^2.1.0",
"prettier": "^2.4.1",
"progress-bar-webpack-plugin": "^2.1.0", "progress-bar-webpack-plugin": "^2.1.0",
"randomcolor": "^0.6.2", "randomcolor": "^0.6.2",
"raw-loader": "^4.0.2", "raw-loader": "^4.0.2",

View File

@ -77,7 +77,7 @@ export class LensRendererExtension extends LensExtension {
} }
/** /**
* Add a filtering function for the catalog catogries. This will be removed if the extension is disabled. * Add a filtering function for the catalog categories. This will be removed if the extension is disabled.
* @param fn The function which should return a truthy value for those categories which should be kept. * @param fn The function which should return a truthy value for those categories which should be kept.
* @returns A function to clean up the filter * @returns A function to clean up the filter
*/ */

View File

@ -22,7 +22,8 @@
import type { CatalogCategory, CatalogEntity } from "../../common/catalog"; import type { CatalogCategory, CatalogEntity } from "../../common/catalog";
import { catalogEntityRegistry as registry } from "../../renderer/api/catalog-entity-registry"; import { catalogEntityRegistry as registry } from "../../renderer/api/catalog-entity-registry";
import type { CatalogEntityOnBeforeRun } from "../../renderer/api/catalog-entity-registry";
import type { Disposer } from "../../common/utils";
export { catalogCategoryRegistry as catalogCategories } from "../../common/catalog/catalog-category-registry"; export { catalogCategoryRegistry as catalogCategories } from "../../common/catalog/catalog-category-registry";
export class CatalogEntityRegistry { export class CatalogEntityRegistry {
@ -48,6 +49,16 @@ export class CatalogEntityRegistry {
getItemsForCategory<T extends CatalogEntity>(category: CatalogCategory): T[] { getItemsForCategory<T extends CatalogEntity>(category: CatalogCategory): T[] {
return registry.getItemsForCategory(category); return registry.getItemsForCategory(category);
} }
/**
* Add a onBeforeRun 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(entity: CatalogEntity, onBeforeRun: CatalogEntityOnBeforeRun): Disposer {
return registry.addOnBeforeRun(entity, onBeforeRun);
}
} }
export const catalogEntities = new CatalogEntityRegistry(); export const catalogEntities = new CatalogEntityRegistry();

View File

@ -21,10 +21,17 @@
import fetchMock from "jest-fetch-mock"; import fetchMock from "jest-fetch-mock";
import configurePackages from "./common/configure-packages"; import configurePackages from "./common/configure-packages";
import { configure } from "mobx";
// setup default configuration for external npm-packages // setup default configuration for external npm-packages
configurePackages(); configurePackages();
configure({
// Needed because we want to use jest.spyOn()
// ref https://github.com/mobxjs/mobx/issues/2784
safeDescriptors: false,
});
// rewire global.fetch to call 'fetchMock' // rewire global.fetch to call 'fetchMock'
fetchMock.enableMocks(); fetchMock.enableMocks();

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * 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 { ipcRendererOn } from "../../common/ipc";
import { CatalogCategory, CatalogEntity, CatalogEntityData, catalogCategoryRegistry, CatalogCategoryRegistry, CatalogEntityKindData } from "../../common/catalog"; import { CatalogCategory, CatalogEntity, CatalogEntityData, catalogCategoryRegistry, CatalogCategoryRegistry, CatalogEntityKindData } from "../../common/catalog";
import "../../common/catalog-entities"; import "../../common/catalog-entities";
@ -27,8 +27,13 @@ import type { Cluster } from "../../main/cluster";
import { ClusterStore } from "../../common/cluster-store"; import { ClusterStore } from "../../common/cluster-store";
import { Disposer, iter } from "../utils"; import { Disposer, iter } from "../utils";
import { once } from "lodash"; import { once } from "lodash";
import logger from "../../common/logger";
import { catalogEntityRunContext } from "./catalog-entity";
export type EntityFilter = (entity: CatalogEntity) => any; export type EntityFilter = (entity: CatalogEntity) => any;
export type CatalogEntityOnBeforeRun = (entity: CatalogEntity) => boolean | Promise<boolean>;
type CatalogEntityUid = CatalogEntity["metadata"]["uid"];
export class CatalogEntityRegistry { export class CatalogEntityRegistry {
@observable protected activeEntityId: string | undefined = undefined; @observable protected activeEntityId: string | undefined = undefined;
@ -36,6 +41,9 @@ export class CatalogEntityRegistry {
protected filters = observable.set<EntityFilter>([], { protected filters = observable.set<EntityFilter>([], {
deep: false, deep: false,
}); });
protected onBeforeRunHooks = observable.map<CatalogEntityUid, ObservableSet<CatalogEntityOnBeforeRun>>({}, {
deep: false,
});
/** /**
* Buffer for keeping entities that don't yet have CatalogCategory synced * Buffer for keeping entities that don't yet have CatalogCategory synced
@ -169,6 +177,73 @@ export class CatalogEntityRegistry {
return once(() => void this.filters.delete(fn)); return once(() => void this.filters.delete(fn));
} }
/**
* Add a onBeforeRun 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(entityOrId: CatalogEntity | CatalogEntityUid, onBeforeRun: CatalogEntityOnBeforeRun): Disposer {
logger.debug(`[CATALOG-ENTITY-REGISTRY]: adding onBeforeRun to ${entityOrId}`);
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 hooks.delete(onBeforeRun));
}
/**
* 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
*/
async onBeforeRun(entity: CatalogEntity): Promise<boolean> {
logger.debug(`[CATALOG-ENTITY-REGISTRY]: run onBeforeRun on ${entity.getId()}`);
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;
}
/**
* Perform the onBeforeRun check and, if successful, then proceed to call `entity`'s onRun method
* @param entity The instance to invoke the hooks and then execute the onRun
*/
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));
}
} }
export const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry); export const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);

View File

@ -23,13 +23,12 @@ import "./catalog-entity-details.scss";
import React, { Component } from "react"; import React, { Component } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { Drawer, DrawerItem } from "../drawer"; import { Drawer, DrawerItem } from "../drawer";
import { catalogEntityRunContext } from "../../api/catalog-entity";
import type { CatalogCategory, CatalogEntity } from "../../../common/catalog"; import type { CatalogCategory, CatalogEntity } from "../../../common/catalog";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { CatalogEntityDrawerMenu } from "./catalog-entity-drawer-menu"; import { CatalogEntityDrawerMenu } from "./catalog-entity-drawer-menu";
import { CatalogEntityDetailRegistry } from "../../../extensions/registries"; import { CatalogEntityDetailRegistry } from "../../../extensions/registries";
import { HotbarIcon } from "../hotbar/hotbar-icon"; import { HotbarIcon } from "../hotbar/hotbar-icon";
import type { CatalogEntityItem } from "./catalog-entity.store"; import type { CatalogEntityItem } from "./catalog-entity-item";
import { isDevelopment } from "../../../common/vars"; import { isDevelopment } from "../../../common/vars";
interface Props<T extends CatalogEntity> { interface Props<T extends CatalogEntity> {
@ -68,8 +67,10 @@ export class CatalogEntityDetails<T extends CatalogEntity> extends Component<Pro
material={item.entity.spec.icon?.material} material={item.entity.spec.icon?.material}
background={item.entity.spec.icon?.background} background={item.entity.spec.icon?.background}
disabled={!item?.enabled} disabled={!item?.enabled}
onClick={() => item.onRun(catalogEntityRunContext)} onClick={() => item.onRun()}
size={128} /> size={128}
data-testid="detail-panel-hot-bar-icon"
/>
{item?.enabled && ( {item?.enabled && (
<div className="IconHint"> <div className="IconHint">
Click to open Click to open

View File

@ -30,7 +30,7 @@ import { MenuItem } from "../menu";
import { ConfirmDialog } from "../confirm-dialog"; import { ConfirmDialog } from "../confirm-dialog";
import { HotbarStore } from "../../../common/hotbar-store"; import { HotbarStore } from "../../../common/hotbar-store";
import { Icon } from "../icon"; import { Icon } from "../icon";
import type { CatalogEntityItem } from "./catalog-entity.store"; import type { CatalogEntityItem } from "./catalog-entity-item";
export interface CatalogEntityDrawerMenuProps<T extends CatalogEntity> extends MenuActionsProps { export interface CatalogEntityDrawerMenuProps<T extends CatalogEntity> extends MenuActionsProps {
item: CatalogEntityItem<T> | null | undefined; item: CatalogEntityItem<T> | null | undefined;

View File

@ -0,0 +1,114 @@
/**
* 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 styles from "./catalog.module.css";
import React from "react";
import { action, computed } from "mobx";
import type { CatalogEntity } from "../../api/catalog-entity";
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 type { CatalogEntityRegistry } from "../../api/catalog-entity-registry";
const css = makeCss(styles);
export class CatalogEntityItem<T extends CatalogEntity> implements ItemObject {
constructor(public entity: T, private registry: CatalogEntityRegistry) {}
get kind() {
return this.entity.kind;
}
get apiVersion() {
return this.entity.apiVersion;
}
get name() {
return this.entity.metadata.name;
}
getName() {
return this.entity.metadata.name;
}
get id() {
return this.entity.metadata.uid;
}
getId() {
return this.id;
}
@computed get phase() {
return this.entity.status.phase;
}
get enabled() {
return this.entity.status.enabled ?? true;
}
get labels() {
return KubeObject.stringifyLabels(this.entity.metadata.labels);
}
getLabelBadges(onClick?: React.MouseEventHandler<any>) {
return this.labels
.map(label => (
<Badge
className={css.badge}
key={label}
label={label}
title={label}
onClick={(event) => {
navigation.searchParams.set(searchUrlParam.name, label);
onClick?.(event);
event.stopPropagation();
}}
expandable={false}
/>
));
}
get source() {
return this.entity.metadata.source || "unknown";
}
get searchFields() {
return [
this.name,
this.id,
this.phase,
`source=${this.source}`,
...this.labels,
];
}
onRun() {
this.registry.onRun(this.entity);
}
@action
async onContextMenuOpen(ctx: any) {
return this.entity.onContextMenuOpen(ctx);
}
}

View File

@ -19,106 +19,16 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import styles from "./catalog.module.css"; import { computed, makeObservable, observable, reaction } from "mobx";
import { catalogEntityRegistry, CatalogEntityRegistry } from "../../api/catalog-entity-registry";
import React from "react"; import type { CatalogEntity } from "../../api/catalog-entity";
import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from "mobx"; import { ItemStore } from "../../../common/item.store";
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
import type { CatalogEntity, CatalogEntityActionContext } from "../../api/catalog-entity";
import { ItemObject, ItemStore } from "../../../common/item.store";
import { CatalogCategory, catalogCategoryRegistry } from "../../../common/catalog"; import { CatalogCategory, catalogCategoryRegistry } from "../../../common/catalog";
import { autoBind } from "../../../common/utils"; import { autoBind, disposer } from "../../../common/utils";
import { Badge } from "../badge"; import { CatalogEntityItem } from "./catalog-entity-item";
import { navigation } from "../../navigation";
import { searchUrlParam } from "../input";
import { makeCss } from "../../../common/utils/makeCss";
import { KubeObject } from "../../../common/k8s-api/kube-object";
const css = makeCss(styles);
export class CatalogEntityItem<T extends CatalogEntity> implements ItemObject {
constructor(public entity: T) {}
get kind() {
return this.entity.kind;
}
get apiVersion() {
return this.entity.apiVersion;
}
get name() {
return this.entity.metadata.name;
}
getName() {
return this.entity.metadata.name;
}
get id() {
return this.entity.metadata.uid;
}
getId() {
return this.id;
}
@computed get phase() {
return this.entity.status.phase;
}
get enabled() {
return this.entity.status.enabled ?? true;
}
get labels() {
return KubeObject.stringifyLabels(this.entity.metadata.labels);
}
getLabelBadges(onClick?: React.MouseEventHandler<any>) {
return this.labels
.map(label => (
<Badge
className={css.badge}
key={label}
label={label}
title={label}
onClick={(event) => {
navigation.searchParams.set(searchUrlParam.name, label);
onClick?.(event);
event.stopPropagation();
}}
expandable={false}
/>
));
}
get source() {
return this.entity.metadata.source || "unknown";
}
get searchFields() {
return [
this.name,
this.id,
this.phase,
`source=${this.source}`,
...this.labels,
];
}
onRun(ctx: CatalogEntityActionContext) {
this.entity.onRun(ctx);
}
@action
async onContextMenuOpen(ctx: any) {
return this.entity.onContextMenuOpen(ctx);
}
}
export class CatalogEntityStore extends ItemStore<CatalogEntityItem<CatalogEntity>> { export class CatalogEntityStore extends ItemStore<CatalogEntityItem<CatalogEntity>> {
constructor() { constructor(private registry: CatalogEntityRegistry = catalogEntityRegistry) {
super(); super();
makeObservable(this); makeObservable(this);
autoBind(this); autoBind(this);
@ -129,10 +39,10 @@ export class CatalogEntityStore extends ItemStore<CatalogEntityItem<CatalogEntit
@computed get entities() { @computed get entities() {
if (!this.activeCategory) { if (!this.activeCategory) {
return catalogEntityRegistry.filteredItems.map(entity => new CatalogEntityItem(entity)); return this.registry.filteredItems.map(entity => new CatalogEntityItem(entity, this.registry));
} }
return catalogEntityRegistry.getItemsForCategory(this.activeCategory, { filtered: true }).map(entity => new CatalogEntityItem(entity)); return this.registry.getItemsForCategory(this.activeCategory, { filtered: true }).map(entity => new CatalogEntityItem(entity, this.registry));
} }
@computed get selectedItem() { @computed get selectedItem() {
@ -140,12 +50,10 @@ export class CatalogEntityStore extends ItemStore<CatalogEntityItem<CatalogEntit
} }
watch() { watch() {
const disposers: IReactionDisposer[] = [ return disposer(
reaction(() => this.entities, () => this.loadAll()), reaction(() => this.entities, () => this.loadAll()),
reaction(() => this.activeCategory, () => this.loadAll(), { delay: 100}) reaction(() => this.activeCategory, () => this.loadAll(), { delay: 100}),
]; );
return () => disposers.forEach((dispose) => dispose());
} }
loadAll() { loadAll() {

View File

@ -32,4 +32,4 @@
.catalog { .catalog {
@apply p-5 font-bold text-2xl; @apply p-5 font-bold text-2xl;
color: var(--textColorAccent); color: var(--textColorAccent);
} }

View File

@ -41,11 +41,15 @@ function getCategories() {
} }
function getCategoryIcon(category: CatalogCategory) { function getCategoryIcon(category: CatalogCategory) {
if (!category.metadata?.icon) return null; const { icon } = category.metadata ?? {};
return category.metadata.icon.includes("<svg") if (typeof icon === "string") {
? <Icon small svg={category.metadata.icon}/> return icon.includes("<svg")
: <Icon small material={category.metadata.icon}/>; ? <Icon small svg={icon}/>
: <Icon small material={icon}/>;
}
return null;
} }
function Item(props: TreeItemProps) { function Item(props: TreeItemProps) {

View File

@ -0,0 +1,375 @@
/**
* 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 { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Catalog } from "./catalog";
import { createMemoryHistory } from "history";
import { mockWindow } from "../../../../__mocks__/windowMock";
import { kubernetesClusterCategory } from "../../../common/catalog-entities/kubernetes-cluster";
import { catalogCategoryRegistry, CatalogCategoryRegistry } from "../../../common/catalog";
import { CatalogEntityRegistry } from "../../../renderer/api/catalog-entity-registry";
import { CatalogEntityDetailRegistry } from "../../../extensions/registries";
import { CatalogEntityItem } from "./catalog-entity-item";
import { CatalogEntityStore } from "./catalog-entity.store";
mockWindow();
// avoid TypeError: Cannot read property 'getPath' of undefined
jest.mock("@electron/remote", () => {
return {
app: {
getPath: () => {
// avoid TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string. Received undefined
return "";
},
},
};
});
describe("<Catalog />", () => {
const history = createMemoryHistory();
const mockLocation = {
pathname: "",
search: "",
state: "",
hash: "",
};
const mockMatch = {
params: {
// will be used to match activeCategory
// need to be the same as property values in kubernetesClusterCategory
group: "entity.k8slens.dev",
kind: "KubernetesCluster",
},
isExact: true,
path: "",
url: "",
};
const catalogEntityUid = "a_catalogEntity_uid";
const catalogEntity = {
enabled: true,
apiVersion: "api",
kind: "kind",
metadata: {
uid: catalogEntityUid,
name: "a catalog entity",
labels: {
test: "label",
},
},
status: {
phase: "",
},
spec: {},
};
const catalogEntityItemMethods = {
getId: () => catalogEntity.metadata.uid,
getName: () => catalogEntity.metadata.name,
onContextMenuOpen: () => {},
onSettingsOpen: () => {},
onRun: () => {},
};
beforeEach(() => {
CatalogEntityDetailRegistry.createInstance();
// mock the return of getting CatalogCategoryRegistry.filteredItems
jest
.spyOn(catalogCategoryRegistry, "filteredItems", "get")
.mockImplementation(() => {
return [kubernetesClusterCategory];
});
// we don't care what this.renderList renders in this test case.
jest.spyOn(Catalog.prototype, "renderList").mockImplementation(() => {
return <span>empty renderList</span>;
});
});
afterEach(() => {
CatalogEntityDetailRegistry.resetInstance();
jest.clearAllMocks();
jest.restoreAllMocks();
});
it("can use catalogEntityRegistry.addOnBeforeRun to add hooks for catalog entities", (done) => {
const catalogCategoryRegistry = new CatalogCategoryRegistry();
const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry);
const onRun = jest.fn();
const catalogEntityItem = new CatalogEntityItem({
...catalogEntity,
...catalogEntityItemMethods,
onRun,
}, catalogEntityRegistry);
// mock as if there is a selected item > the detail panel opens
jest
.spyOn(catalogEntityStore, "selectedItem", "get")
.mockImplementation(() => catalogEntityItem);
catalogEntityRegistry.addOnBeforeRun(
catalogEntityUid,
(entity) => {
expect(entity).toMatchInlineSnapshot(`
Object {
"apiVersion": "api",
"enabled": true,
"getId": [Function],
"getName": [Function],
"kind": "kind",
"metadata": Object {
"labels": Object {
"test": "label",
},
"name": "a catalog entity",
"uid": "a_catalogEntity_uid",
},
"onContextMenuOpen": [Function],
"onRun": [MockFunction],
"onSettingsOpen": [Function],
"spec": Object {},
"status": Object {
"phase": "",
},
}
`);
setTimeout(() => {
expect(onRun).toHaveBeenCalled();
done();
}, 500);
return true;
}
);
render(
<Catalog
history={history}
location={mockLocation}
match={mockMatch}
catalogEntityStore={catalogEntityStore}
/>
);
userEvent.click(screen.getByTestId("detail-panel-hot-bar-icon"));
});
it("onBeforeRun return false => onRun wont be triggered", (done) => {
const catalogCategoryRegistry = new CatalogCategoryRegistry();
const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry);
const onRun = jest.fn();
const catalogEntityItem = new CatalogEntityItem({
...catalogEntity,
...catalogEntityItemMethods,
onRun,
}, catalogEntityRegistry);
// mock as if there is a selected item > the detail panel opens
jest
.spyOn(catalogEntityStore, "selectedItem", "get")
.mockImplementation(() => catalogEntityItem);
catalogEntityRegistry.addOnBeforeRun(
catalogEntityUid,
() => {
setTimeout(() => {
expect(onRun).not.toHaveBeenCalled();
done();
}, 500);
return false;
}
);
render(
<Catalog
history={history}
location={mockLocation}
match={mockMatch}
catalogEntityStore={catalogEntityStore}
/>
);
userEvent.click(screen.getByTestId("detail-panel-hot-bar-icon"));
});
it("addOnBeforeRun throw an exception => onRun wont be triggered", (done) => {
const catalogCategoryRegistry = new CatalogCategoryRegistry();
const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry);
const onRun = jest.fn();
const catalogEntityItem = new CatalogEntityItem({
...catalogEntity,
...catalogEntityItemMethods,
onRun,
}, catalogEntityRegistry);
// mock as if there is a selected item > the detail panel opens
jest
.spyOn(catalogEntityStore, "selectedItem", "get")
.mockImplementation(() => catalogEntityItem);
catalogEntityRegistry.addOnBeforeRun(
catalogEntityUid,
() => {
setTimeout(() => {
expect(onRun).not.toHaveBeenCalled();
done();
}, 500);
throw new Error("error!");
}
);
render(
<Catalog
history={history}
location={mockLocation}
match={mockMatch}
catalogEntityStore={catalogEntityStore}
/>
);
userEvent.click(screen.getByTestId("detail-panel-hot-bar-icon"));
});
it("addOnRunHook return a promise and resolve true => onRun()", (done) => {
const catalogCategoryRegistry = new CatalogCategoryRegistry();
const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry);
const onRun = jest.fn(() => done());
const catalogEntityItem = new CatalogEntityItem({
...catalogEntity,
...catalogEntityItemMethods,
onRun,
}, catalogEntityRegistry);
// mock as if there is a selected item > the detail panel opens
jest
.spyOn(catalogEntityStore, "selectedItem", "get")
.mockImplementation(() => catalogEntityItem);
catalogEntityRegistry.addOnBeforeRun(
catalogEntityUid,
async () => {
return true;
}
);
render(
<Catalog
history={history}
location={mockLocation}
match={mockMatch}
catalogEntityStore={catalogEntityStore}
/>
);
userEvent.click(screen.getByTestId("detail-panel-hot-bar-icon"));
});
it("addOnRunHook return a promise and resolve false => onRun() wont be triggered", (done) => {
const catalogCategoryRegistry = new CatalogCategoryRegistry();
const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry);
const onRun = jest.fn();
const catalogEntityItem = new CatalogEntityItem({
...catalogEntity,
...catalogEntityItemMethods,
onRun,
}, catalogEntityRegistry);
// mock as if there is a selected item > the detail panel opens
jest
.spyOn(catalogEntityStore, "selectedItem", "get")
.mockImplementation(() => catalogEntityItem);
catalogEntityRegistry.addOnBeforeRun(
catalogEntityUid,
async () => {
expect(onRun).not.toBeCalled();
setTimeout(() => {
expect(onRun).not.toBeCalled();
done();
}, 500);
return false;
}
);
render(
<Catalog
history={history}
location={mockLocation}
match={mockMatch}
catalogEntityStore={catalogEntityStore}
/>
);
userEvent.click(screen.getByTestId("detail-panel-hot-bar-icon"));
});
it("addOnRunHook return a promise and reject => onRun wont be triggered", (done) => {
const catalogCategoryRegistry = new CatalogCategoryRegistry();
const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry);
const onRun = jest.fn();
const catalogEntityItem = new CatalogEntityItem({
...catalogEntity,
...catalogEntityItemMethods,
onRun,
}, catalogEntityRegistry);
// mock as if there is a selected item > the detail panel opens
jest
.spyOn(catalogEntityStore, "selectedItem", "get")
.mockImplementation(() => catalogEntityItem);
catalogEntityRegistry.addOnBeforeRun(
catalogEntityUid,
async () => {
setTimeout(() => {
expect(onRun).not.toHaveBeenCalled();
done();
}, 500);
throw new Error("rejection!");
}
);
render(
<Catalog
history={history}
location={mockLocation}
match={mockMatch}
catalogEntityStore={catalogEntityStore}
/>
);
userEvent.click(screen.getByTestId("detail-panel-hot-bar-icon"));
});
});

View File

@ -25,10 +25,11 @@ import React from "react";
import { disposeOnUnmount, observer } from "mobx-react"; import { disposeOnUnmount, observer } from "mobx-react";
import { ItemListLayout } from "../item-object-list"; import { ItemListLayout } from "../item-object-list";
import { action, makeObservable, observable, reaction, runInAction, when } from "mobx"; import { action, makeObservable, observable, reaction, runInAction, when } from "mobx";
import { CatalogEntityItem, CatalogEntityStore } from "./catalog-entity.store"; import { CatalogEntityStore } from "./catalog-entity.store";
import type { CatalogEntityItem } from "./catalog-entity-item";
import { navigate } from "../../navigation"; import { navigate } from "../../navigation";
import { MenuItem, MenuActions } from "../menu"; 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 { HotbarStore } from "../../../common/hotbar-store";
import { ConfirmDialog } from "../confirm-dialog"; import { ConfirmDialog } from "../confirm-dialog";
import { catalogCategoryRegistry, CatalogEntity } from "../../../common/catalog"; import { catalogCategoryRegistry, CatalogEntity } from "../../../common/catalog";
@ -55,7 +56,10 @@ enum sortBy {
const css = makeCss(styles); const css = makeCss(styles);
interface Props extends RouteComponentProps<CatalogViewRouteParam> {} interface Props extends RouteComponentProps<CatalogViewRouteParam> {
catalogEntityStore?: CatalogEntityStore;
}
@observer @observer
export class Catalog extends React.Component<Props> { export class Catalog extends React.Component<Props> {
@observable private catalogEntityStore?: CatalogEntityStore; @observable private catalogEntityStore?: CatalogEntityStore;
@ -65,8 +69,11 @@ export class Catalog extends React.Component<Props> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
makeObservable(this); makeObservable(this);
this.catalogEntityStore = new CatalogEntityStore(); this.catalogEntityStore = props.catalogEntityStore;
} }
static defaultProps = {
catalogEntityStore: new CatalogEntityStore(),
};
get routeActiveTab(): string { get routeActiveTab(): string {
const { group, kind } = this.props.match.params ?? {}; const { group, kind } = this.props.match.params ?? {};
@ -126,7 +133,7 @@ export class Catalog extends React.Component<Props> {
if (this.catalogEntityStore.selectedItemId) { if (this.catalogEntityStore.selectedItemId) {
this.catalogEntityStore.selectedItemId = null; this.catalogEntityStore.selectedItemId = null;
} else { } else {
item.onRun(catalogEntityRunContext); item.onRun();
} }
}; };

View File

@ -22,7 +22,7 @@
import { computed } from "mobx"; import { computed } from "mobx";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import React from "react"; import React from "react";
import { CatalogEntity, catalogEntityRunContext } from "../../api/catalog-entity"; import type { CatalogEntity } from "../../api/catalog-entity";
import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
import { CommandOverlay } from "../command-palette"; import { CommandOverlay } from "../command-palette";
import { Select } from "../select"; import { Select } from "../select";
@ -37,7 +37,7 @@ export class ActivateEntityCommand extends React.Component {
} }
onSelect(entity: CatalogEntity): void { onSelect(entity: CatalogEntity): void {
entity.onRun?.(catalogEntityRunContext); catalogEntityRegistry.onRun(entity);
CommandOverlay.close(); CommandOverlay.close();
} }

View File

@ -27,7 +27,7 @@ import { HotbarEntityIcon } from "./hotbar-entity-icon";
import { cssNames, IClassName } from "../../utils"; import { cssNames, IClassName } from "../../utils";
import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
import { HotbarStore } from "../../../common/hotbar-store"; 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 { DragDropContext, Draggable, Droppable, DropResult } from "react-beautiful-dnd";
import { HotbarSelector } from "./hotbar-selector"; import { HotbarSelector } from "./hotbar-selector";
import { HotbarCell } from "./hotbar-cell"; import { HotbarCell } from "./hotbar-cell";
@ -124,7 +124,7 @@ export class HotbarMenu extends React.Component<Props> {
key={index} key={index}
index={index} index={index}
entity={entity} entity={entity}
onClick={() => entity.onRun(catalogEntityRunContext)} onClick={() => catalogEntityRegistry.onRun(entity)}
className={cssNames({ isDragging: snapshot.isDragging })} className={cssNames({ isDragging: snapshot.isDragging })}
remove={this.removeItem} remove={this.removeItem}
add={this.addItem} add={this.addItem}

View File

@ -114,14 +114,14 @@ export class Icon extends React.PureComponent<IconProps> {
}; };
// render as inline svg-icon // render as inline svg-icon
if (svg) { if (typeof svg === "string") {
const svgIconText = svg.includes("<svg") ? svg : require(`!!raw-loader!./${svg}.svg`).default; const svgIconText = svg.includes("<svg") ? svg : require(`!!raw-loader!./${svg}.svg`).default;
iconContent = <span className="icon" dangerouslySetInnerHTML={{ __html: svgIconText }}/>; iconContent = <span className="icon" dangerouslySetInnerHTML={{ __html: svgIconText }}/>;
} }
// render as material-icon // render as material-icon
if (material) { if (typeof material === "string") {
iconContent = <span className="icon" data-icon-name={material}>{material}</span>; iconContent = <span className="icon" data-icon-name={material}>{material}</span>;
} }

View File

@ -11231,6 +11231,11 @@ prepend-http@^2.0.0:
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=
prettier@^2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.4.1.tgz#671e11c89c14a4cfc876ce564106c4a6726c9f5c"
integrity sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA==
pretty-error@^2.1.1: pretty-error@^2.1.1:
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.1.tgz#5f4f87c8f91e5ae3f3ba87ab4cf5e03b1a17f1a3" resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.1.tgz#5f4f87c8f91e5ae3f3ba87ab4cf5e03b1a17f1a3"