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

Add onRunHook extension api

Signed-off-by: Hung-Han (Henry) Chen <chenhungh@gmail.com>
This commit is contained in:
Hung-Han (Henry) Chen 2021-10-04 16:04:37 +03:00
parent 0f63ccbb82
commit cfdb4c93c1
No known key found for this signature in database
GPG Key ID: 54B44603D251B788
8 changed files with 289 additions and 7 deletions

View File

@ -22,7 +22,7 @@
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 { CatelogEntityOnRunHook } from "../../renderer/api/catalog-entity-registry";
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 +48,33 @@ 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 onRun hook to a catalog entity.
* @param catalogEntityUid The uid of the catalog entity
* @param onRunHook The function that should return a boolean if the onRun of catalog entity should be triggered.
* @returns A function to remove that hook
*/
addOnRunHook(catalogEntityUid: CatalogEntity["metadata"]["uid"], onRunHook: CatelogEntityOnRunHook) {
return registry.addOnRunHook(catalogEntityUid, onRunHook);
}
/**
* Returns one catalog entity onRun hook by catalog entity uid
*/
getOnRunHook(catalogEntityUid: CatalogEntity["metadata"]["uid"]): CatelogEntityOnRunHook | undefined {
return registry.getOnRunHook(catalogEntityUid);
}
/**
* Returns all catalog entities' onRun hooks
*/
getAllOnRunHooks(): Array<{
catalogEntityUid: CatalogEntity["metadata"]["uid"];
onRunHook: CatelogEntityOnRunHook;
}> {
return registry.getAllOnRunHooks();
}
} }
export const catalogEntities = new CatalogEntityRegistry(); export const catalogEntities = new CatalogEntityRegistry();

View File

@ -27,8 +27,12 @@ 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 type { CatalogEntityItem } from "../../renderer/components/+catalog/catalog-entity-item"
export type EntityFilter = (entity: CatalogEntity) => any; export type EntityFilter = (entity: CatalogEntity) => any;
export type CatelogEntityOnRunHook = (entity: CatalogEntity | CatalogEntityItem<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 +40,12 @@ export class CatalogEntityRegistry {
protected filters = observable.set<EntityFilter>([], { protected filters = observable.set<EntityFilter>([], {
deep: false, deep: false,
}); });
protected entityOnRunHooks = observable.set<{
catalogEntityUid: CatalogEntityUid;
onRunHook: CatelogEntityOnRunHook
}>([], {
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 +179,41 @@ export class CatalogEntityRegistry {
return once(() => void this.filters.delete(fn)); return once(() => void this.filters.delete(fn));
} }
/**
* Add a onRun hook to a catalog entity.
* @param uid The uid of the catalog entity
* @param onRunHook The function that should return a boolean if the onRun of catalog entity should be triggered.
* @returns A function to remove that hook
*/
addOnRunHook(catalogEntityUid: CatalogEntityUid, onRunHook: CatelogEntityOnRunHook): Disposer {
this.entityOnRunHooks.add({
catalogEntityUid,
onRunHook,
});
return once(() => void this.entityOnRunHooks.delete({
catalogEntityUid,
onRunHook,
}));
}
/**
* Returns one catalog entity onRun hook by catalog entity uid
*/
getOnRunHook(_catalogEntityUid: CatalogEntityUid): CatelogEntityOnRunHook | undefined {
return Array.from(this.entityOnRunHooks).find(({ catalogEntityUid }) => catalogEntityUid === _catalogEntityUid)?.onRunHook;
}
/**
* Returns all catalog entities' onRun hooks
*/
getAllOnRunHooks(): Array<{
catalogEntityUid: CatalogEntityUid;
onRunHook: CatelogEntityOnRunHook;
}> {
return Array.from(this.entityOnRunHooks);
}
} }
export const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry); export const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);

View File

@ -23,7 +23,6 @@ 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";
@ -35,6 +34,7 @@ import { isDevelopment } from "../../../common/vars";
interface Props<T extends CatalogEntity> { interface Props<T extends CatalogEntity> {
item: CatalogEntityItem<T> | null | undefined; item: CatalogEntityItem<T> | null | undefined;
hideDetails(): void; hideDetails(): void;
onClickDetailPanelIcon: (catalogEntitiyItem: CatalogEntityItem<T>) => void;
} }
@observer @observer
@ -69,7 +69,7 @@ export class CatalogEntityDetails<T extends CatalogEntity> extends Component<Pro
background={item.entity.spec.icon?.background} background={item.entity.spec.icon?.background}
disabled={!item?.enabled} disabled={!item?.enabled}
onClick={() => { onClick={() => {
item.onRun(catalogEntityRunContext); this.props.onClickDetailPanelIcon(item);
}} }}
size={128} size={128}
data-testid="detail-panel-hot-bar-icon" data-testid="detail-panel-hot-bar-icon"

View File

@ -22,12 +22,14 @@ import styles from "./catalog.module.css";
import React from "react"; import React from "react";
import { action, computed } from "mobx"; import { action, computed } from "mobx";
import type { CatalogEntity, CatalogEntityActionContext } from "../../api/catalog-entity"; import type { CatalogEntity, CatalogEntityActionContext } from "../../api/catalog-entity";
import type { CatelogEntityOnRunHook } from "../../api/catalog-entity-registry";
import type { ItemObject } from "../../../common/item.store"; import type { ItemObject } from "../../../common/item.store";
import { Badge } from "../badge"; import { Badge } from "../badge";
import { navigation } from "../../navigation"; import { navigation } from "../../navigation";
import { searchUrlParam } from "../input"; import { searchUrlParam } from "../input";
import { makeCss } from "../../../common/utils/makeCss"; import { makeCss } from "../../../common/utils/makeCss";
import { KubeObject } from "../../../common/k8s-api/kube-object"; import { KubeObject } from "../../../common/k8s-api/kube-object";
import { toJS } from "mobx";
const css = makeCss(styles); const css = makeCss(styles);
@ -102,8 +104,20 @@ export class CatalogEntityItem<T extends CatalogEntity> implements ItemObject {
]; ];
} }
onRun(ctx: CatalogEntityActionContext) { onRun(onRunHook: CatelogEntityOnRunHook | undefined, ctx: CatalogEntityActionContext) {
if (!onRunHook) {
this.entity.onRun(ctx); this.entity.onRun(ctx);
return;
}
if (typeof onRunHook === "function") {
// if onRunHook() returns a Promise, we wait for it to resolve
// if not, just take whatever it returns
Promise.resolve(onRunHook(toJS(this.entity))).then((shouldRun) => {
if (shouldRun) this.entity.onRun(ctx);
});
}
} }
@action @action

View File

@ -22,6 +22,7 @@
import { computed, IReactionDisposer, makeObservable, observable, reaction } from "mobx"; import { computed, IReactionDisposer, makeObservable, observable, reaction } from "mobx";
import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
import type { CatalogEntity } from "../../api/catalog-entity"; import type { CatalogEntity } from "../../api/catalog-entity";
import type { CatelogEntityOnRunHook } from "../../api/catalog-entity-registry";
import { ItemStore } from "../../../common/item.store"; import { ItemStore } from "../../../common/item.store";
import { CatalogCategory, catalogCategoryRegistry } from "../../../common/catalog"; import { CatalogCategory, catalogCategoryRegistry } from "../../../common/catalog";
import { autoBind } from "../../../common/utils"; import { autoBind } from "../../../common/utils";
@ -49,6 +50,10 @@ export class CatalogEntityStore extends ItemStore<CatalogEntityItem<CatalogEntit
return this.entities.find(e => e.getId() === this.selectedItemId); return this.entities.find(e => e.getId() === this.selectedItemId);
} }
getCatalogEntityOnRunHook(catalogEntityUid: CatalogEntity["metadata"]["uid"]): CatelogEntityOnRunHook | undefined {
return catalogEntityRegistry.getOnRunHook(catalogEntityUid);
}
watch() { watch() {
const disposers: IReactionDisposer[] = [ const disposers: IReactionDisposer[] = [
reaction(() => this.entities, () => this.loadAll()), reaction(() => this.entities, () => this.loadAll()),

View File

@ -0,0 +1,160 @@
/**
* 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, CatalogEntity } 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();
const isCatalogEntityItem = (entity: any): entity is CatalogEntityItem<CatalogEntity> => typeof entity.enable === "boolean";
// 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: () => "",
getName: () => "",
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.addOnRunHook to add hooks for catalog entities", (done) => {
const catalogEntityItem = new CatalogEntityItem({
...catalogEntity,
...catalogEntityItemMethods,
});
// mock as if there is a selected item > the detail panel opens
jest
.spyOn(CatalogEntityStore.prototype, "selectedItem", "get")
.mockImplementation(() => {
return catalogEntityItem;
});
let hookGetCalled = false;
catalogEntityRegistry.addOnRunHook(catalogEntityUid, (entity) => {
hookGetCalled = true;
expect(hookGetCalled).toBe(true);
expect(entity.apiVersion).toBe(catalogEntity.apiVersion);
expect(entity.kind).toBe(catalogEntity.kind);
if (isCatalogEntityItem(entity)) {
expect(entity.enabled).toBe(catalogEntity.enabled);
}
if (!isCatalogEntityItem(entity)) {
expect(entity.metadata).toBe(catalogEntity.metadata);
expect(entity.status).toBe(catalogEntity.status);
expect(entity.spec).toBe(catalogEntity.spec);
}
done();
return true;
});
render(
<Catalog history={history} location={mockLocation} match={mockMatch} />
);
userEvent.click(screen.getByTestId("detail-panel-hot-bar-icon"));
});
});

View File

@ -127,10 +127,18 @@ 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); const onRunHook = this.catalogEntityStore.getCatalogEntityOnRunHook(item.entity.metadata.uid);
item.onRun(onRunHook, catalogEntityRunContext);
} }
}; };
onClickDetailPanelIcon = (item: CatalogEntityItem<CatalogEntity>) => {
const onRunHook = this.catalogEntityStore.getCatalogEntityOnRunHook(item.entity.metadata.uid);
item.onRun(onRunHook, catalogEntityRunContext);
};
onMenuItemClick(menuItem: CatalogEntityContextMenu) { onMenuItemClick(menuItem: CatalogEntityContextMenu) {
if (menuItem.confirm) { if (menuItem.confirm) {
ConfirmDialog.open({ ConfirmDialog.open({
@ -276,6 +284,7 @@ export class Catalog extends React.Component<Props> {
? <CatalogEntityDetails ? <CatalogEntityDetails
item={this.catalogEntityStore.selectedItem} item={this.catalogEntityStore.selectedItem}
hideDetails={() => this.catalogEntityStore.selectedItemId = null} hideDetails={() => this.catalogEntityStore.selectedItemId = null}
onClickDetailPanelIcon={this.onClickDetailPanelIcon}
/> />
: ( : (
<RenderDelay> <RenderDelay>

View File

@ -26,6 +26,7 @@ import { observer } from "mobx-react";
import { HotbarEntityIcon } from "./hotbar-entity-icon"; 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 type { CatelogEntityOnRunHook} 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 { CatalogEntity, catalogEntityRunContext } from "../../api/catalog-entity";
import { DragDropContext, Draggable, Droppable, DropResult } from "react-beautiful-dnd"; import { DragDropContext, Draggable, Droppable, DropResult } from "react-beautiful-dnd";
@ -33,6 +34,7 @@ import { HotbarSelector } from "./hotbar-selector";
import { HotbarCell } from "./hotbar-cell"; import { HotbarCell } from "./hotbar-cell";
import { HotbarIcon } from "./hotbar-icon"; import { HotbarIcon } from "./hotbar-icon";
import { defaultHotbarCells, HotbarItem } from "../../../common/hotbar-types"; import { defaultHotbarCells, HotbarItem } from "../../../common/hotbar-types";
import { toJS } from "mobx";
interface Props { interface Props {
className?: IClassName; className?: IClassName;
@ -54,6 +56,10 @@ export class HotbarMenu extends React.Component<Props> {
return catalogEntityRegistry.getById(item?.entity.uid) ?? null; return catalogEntityRegistry.getById(item?.entity.uid) ?? null;
} }
getEntityOnRunHook(uid: string): CatelogEntityOnRunHook | undefined {
return catalogEntityRegistry.getOnRunHook(uid);
}
onDragEnd(result: DropResult) { onDragEnd(result: DropResult) {
const { source, destination } = result; const { source, destination } = result;
@ -124,7 +130,23 @@ export class HotbarMenu extends React.Component<Props> {
key={index} key={index}
index={index} index={index}
entity={entity} entity={entity}
onClick={() => entity.onRun(catalogEntityRunContext)} onClick={() => {
const onRunHook = this.getEntityOnRunHook(entity.metadata.uid);
if (!onRunHook) {
entity.onRun(catalogEntityRunContext);
return;
}
if (typeof onRunHook === "function") {
// if onRunHook() returns a Promise, we wait for it to resolve
// if not, just take whatever it returns
Promise.resolve(onRunHook(toJS(entity))).then((shouldRun) => {
if (shouldRun) entity.onRun(catalogEntityRunContext);
});
}
}}
className={cssNames({ isDragging: snapshot.isDragging })} className={cssNames({ isDragging: snapshot.isDragging })}
remove={this.removeItem} remove={this.removeItem}
add={this.addItem} add={this.addItem}