Skip to content

Commit

Permalink
Before hook for onRun of Catalog entities (#3911)
Browse files Browse the repository at this point in the history
Co-authored-by: Sebastian Malton <[email protected]>
  • Loading branch information
chenhunghan and Nokel81 authored Oct 5, 2021
1 parent 851274d commit 4af796c
Show file tree
Hide file tree
Showing 17 changed files with 636 additions and 128 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,7 @@
"postcss": "^8.3.6",
"postcss-loader": "4.3.0",
"postinstall-postinstall": "^2.1.0",
"prettier": "^2.4.1",
"progress-bar-webpack-plugin": "^2.1.0",
"randomcolor": "^0.6.2",
"raw-loader": "^4.0.2",
Expand Down
2 changes: 1 addition & 1 deletion src/extensions/lens-renderer-extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* @returns A function to clean up the filter
*/
Expand Down
13 changes: 12 additions & 1 deletion src/extensions/renderer-api/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@

import type { CatalogCategory, CatalogEntity } from "../../common/catalog";
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 class CatalogEntityRegistry {
Expand All @@ -48,6 +49,16 @@ export class CatalogEntityRegistry {
getItemsForCategory<T extends CatalogEntity>(category: CatalogCategory): T[] {
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();
7 changes: 7 additions & 0 deletions src/jest.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,17 @@

import fetchMock from "jest-fetch-mock";
import configurePackages from "./common/configure-packages";
import { configure } from "mobx";

// setup default configuration for external npm-packages
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'
fetchMock.enableMocks();

Expand Down
77 changes: 76 additions & 1 deletion src/renderer/api/catalog-entity-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,31 @@
* 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";
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<boolean>;

type CatalogEntityUid = CatalogEntity["metadata"]["uid"];

export class CatalogEntityRegistry {
@observable protected activeEntityId: string | undefined = undefined;
protected _entities = observable.map<string, CatalogEntity>([], { deep: true });
protected filters = observable.set<EntityFilter>([], {
deep: false,
});
protected onBeforeRunHooks = observable.map<CatalogEntityUid, ObservableSet<CatalogEntityOnBeforeRun>>({}, {
deep: false,
});

/**
* Buffer for keeping entities that don't yet have CatalogCategory synced
Expand Down Expand Up @@ -169,6 +177,73 @@ export class CatalogEntityRegistry {

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);
Expand Down
9 changes: 5 additions & 4 deletions src/renderer/components/+catalog/catalog-entity-details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,12 @@ import "./catalog-entity-details.scss";
import React, { Component } from "react";
import { observer } from "mobx-react";
import { Drawer, DrawerItem } from "../drawer";
import { catalogEntityRunContext } from "../../api/catalog-entity";
import type { CatalogCategory, CatalogEntity } from "../../../common/catalog";
import { Icon } from "../icon";
import { CatalogEntityDrawerMenu } from "./catalog-entity-drawer-menu";
import { CatalogEntityDetailRegistry } from "../../../extensions/registries";
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";

interface Props<T extends CatalogEntity> {
Expand Down Expand Up @@ -68,8 +67,10 @@ export class CatalogEntityDetails<T extends CatalogEntity> extends Component<Pro
material={item.entity.spec.icon?.material}
background={item.entity.spec.icon?.background}
disabled={!item?.enabled}
onClick={() => item.onRun(catalogEntityRunContext)}
size={128} />
onClick={() => item.onRun()}
size={128}
data-testid="detail-panel-hot-bar-icon"
/>
{item?.enabled && (
<div className="IconHint">
Click to open
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { MenuItem } from "../menu";
import { ConfirmDialog } from "../confirm-dialog";
import { HotbarStore } from "../../../common/hotbar-store";
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 {
item: CatalogEntityItem<T> | null | undefined;
Expand Down
114 changes: 114 additions & 0 deletions src/renderer/components/+catalog/catalog-entity-item.tsx
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading

0 comments on commit 4af796c

Please sign in to comment.