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

Add 'onClickDetailIcon' event when user clicks the icon detail panel

Signed-off-by: Hung-Han (Henry) Chen <chenhungh@gmail.com>
This commit is contained in:
Hung-Han (Henry) Chen 2021-09-29 15:42:00 +03:00
parent e2f59781c8
commit b9c485d0af
No known key found for this signature in database
GPG Key ID: 54B44603D251B788
12 changed files with 455 additions and 100 deletions

View File

@ -39,6 +39,10 @@ export class GeneralEntity extends CatalogEntity<CatalogEntityMetadata, CatalogE
navigate(this.spec.path);
}
onClickDetailIcon() {
return;
}
public onSettingsOpen(): void {
return;
}

View File

@ -100,6 +100,12 @@ export class KubernetesCluster extends CatalogEntity<KubernetesClusterMetadata,
//
}
onClickDetailIcon(context: CatalogEntityActionContext) {
catalogCategoryRegistry
.getCategoryForEntity<KubernetesClusterCategory>(this)
?.emit("onClickDetailIcon", this, context);
}
async onContextMenuOpen(context: CatalogEntityContextMenuContext) {
if (!this.metadata.source || this.metadata.source === "local") {
context.menuItems.push({

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, CatalogEntityAddMenuContext, CatalogEntityContextMenuContext, CatalogEntityActionContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog";
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
import { productName } from "../vars";
import { WeblinkStore } from "../weblink-store";
@ -46,6 +46,12 @@ export class WebLink extends CatalogEntity<CatalogEntityMetadata, WebLinkStatus,
return;
}
onClickDetailIcon(context: CatalogEntityActionContext) {
catalogCategoryRegistry
.getCategoryForEntity<WebLinkCategory>(this)
?.emit("onClickDetailIcon", this, context);
}
async onContextMenuOpen(context: CatalogEntityContextMenuContext) {
if (this.metadata.source === "local") {
context.menuItems.push({

View File

@ -59,6 +59,7 @@ export interface CatalogCategoryEvents {
load: () => void;
catalogAddMenu: (context: CatalogEntityAddMenuContext) => void;
contextMenuOpen: (entity: CatalogEntity, context: CatalogEntityContextMenuContext) => void;
onClickDetailIcon: (entity: CatalogEntity, context: CatalogEntityActionContext) => void;
}
export abstract class CatalogCategory extends (EventEmitter as new () => TypedEmitter<CatalogCategoryEvents>) {
@ -230,7 +231,21 @@ export abstract class CatalogEntity<
return this.metadata.name;
}
/**
* Trigger when user click on the icon in details panel
*
* @remarks
* Priority < this.onClickDetailIcon, if this.onClickDetailIcon presents, onRun won't be called.
*/
public abstract onRun?(context: CatalogEntityActionContext): void | Promise<void>;
public abstract onContextMenuOpen(context: CatalogEntityContextMenuContext): void | Promise<void>;
/**
* Trigger when user click on the icon in details panel
*
* @remarks
* Priority > this.onRun, if presents, onRun won't be called.
*/
public abstract onClickDetailIcon?(context: CatalogEntityActionContext): void;
public abstract onSettingsOpen(context: CatalogEntitySettingsContext): void | Promise<void>;
}

View File

@ -32,6 +32,10 @@ class InvalidEntity extends CatalogEntity<CatalogEntityMetadata, WebLinkStatus,
return;
}
public onClickDetailIcon(): void {
return;
}
public onSettingsOpen(): void {
return;
}

View File

@ -0,0 +1,112 @@
/**
* 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 { CatalogEntityDetails } from "./catalog-entity-details";
import { CatalogEntityItem } from "./catalog-entity-item";
import { CatalogEntityDetailRegistry } from "../../../extensions/registries";
// avoid TypeError: window.requestIdleCallback is not a function
import { mockWindow } from "../../../../__mocks__/windowMock";
mockWindow();
describe("<CatalogEntityDetails />", () => {
beforeEach(() => {
CatalogEntityDetailRegistry.createInstance();
});
it("trigger onRun when click on detail panel icon", () => {
const onRun = jest.fn();
const item = new CatalogEntityItem({
enabled: true,
apiVersion: "",
kind: "",
metadata: {
uid: "",
name: "",
labels: {},
},
status: {
phase: "",
},
spec: {},
getId: () => "",
getName: () => "",
onContextMenuOpen: () => {},
onSettingsOpen: () => {},
onRun,
});
render(
<CatalogEntityDetails
item={item}
hideDetails={() => {}}
/>
);
userEvent.click(screen.getByTestId("detail-panel-hot-bar-icon"));
expect(onRun).toHaveBeenCalledTimes(1);
});
it("prioritize onClickDetailIcon over onRun", () => {
const onRun = jest.fn();
const onClickDetailIcon = jest.fn();
const item = new CatalogEntityItem({
enabled: true,
apiVersion: "",
kind: "",
metadata: {
uid: "",
name: "",
labels: {},
},
status: {
phase: "",
},
spec: {},
getId: () => "",
getName: () => "",
onContextMenuOpen: () => {},
onSettingsOpen: () => {},
onRun,
onClickDetailIcon,
});
render(
<CatalogEntityDetails
item={item}
hideDetails={() => {}}
/>
);
userEvent.click(screen.getByTestId("detail-panel-hot-bar-icon"));
expect(onRun).toHaveBeenCalledTimes(0);
expect(onClickDetailIcon).toHaveBeenCalledTimes(1);
});
});

View File

@ -29,7 +29,7 @@ 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> {
@ -68,8 +68,16 @@ 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={() => {
if (typeof item.entity.onClickDetailIcon === "function") {
item.onClickDetailIcon(catalogEntityRunContext);
} else {
item.onRun(catalogEntityRunContext);
}
}}
size={128}
data-testid="detail-panel-hot-bar-icon"
/>
{item?.enabled && (
<div className="IconHint">
Click to open

View File

@ -0,0 +1,129 @@
/**
* 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, CatalogEntityActionContext } 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";
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,
];
}
/**
* Trigger when user click on the icon in details panel
*
* @remarks
* Priority < this.onClickDetailIcon, if this.onClickDetailIcon presents, onRun won't be called.
*/
onRun(ctx: CatalogEntityActionContext) {
this.entity.onRun(ctx);
}
@action
async onContextMenuOpen(ctx: any) {
return this.entity.onContextMenuOpen(ctx);
}
/**
* Trigger when user click on the icon in details panel
*
* @remarks
* Priority > this.onRun, if presents, onRun won't be called.
*/
onClickDetailIcon(ctx: CatalogEntityActionContext) {
this.entity.onClickDetailIcon?.(ctx);
}
}

View File

@ -19,103 +19,13 @@
* 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, IReactionDisposer, makeObservable, observable, reaction } from "mobx";
import { computed, IReactionDisposer, makeObservable, observable, reaction } from "mobx";
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
import type { CatalogEntity, CatalogEntityActionContext } from "../../api/catalog-entity";
import { ItemObject, ItemStore } from "../../../common/item.store";
import type { CatalogEntity } from "../../api/catalog-entity";
import { ItemStore } from "../../../common/item.store";
import { CatalogCategory, catalogCategoryRegistry } from "../../../common/catalog";
import { autoBind } from "../../../common/utils";
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";
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);
}
}
import { CatalogEntityItem } from "./catalog-entity-item";
export class CatalogEntityStore extends ItemStore<CatalogEntityItem<CatalogEntity>> {
constructor() {

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 { CatalogEntityDetailRegistry } from "../../../extensions/registries";
import { createMemoryHistory } from "history";
import { mockWindow } from "../../../../__mocks__/windowMock";
import {
KubernetesCluster,
kubernetesClusterCategory,
} from "../../../common/catalog-entities/kubernetes-cluster";
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 "";
},
},
};
});
// avoid crashese in <CatalogMenu />
jest.mock("../../api/catalog-category-registry", () => {
return {
catalogCategoryRegistry: {
filteredItems: [],
},
};
});
describe("<Catalog />", () => {
beforeEach(() => {
CatalogEntityDetailRegistry.createInstance();
});
beforeAll(() => {
// mock the return of getting CatalogEntityStore.selectedItem
jest
.spyOn(CatalogEntityStore.prototype, "selectedItem", "get")
.mockImplementation(() => {
const mockCluster = new KubernetesCluster({
metadata: {
uid: "",
name: "",
source: "",
labels: {},
distro: "",
kubeVersion: "",
},
spec: {
kubeconfigPath: "",
kubeconfigContext: "",
icon: {},
},
status: {
phase: "disconnected",
reason: "",
message: "",
active: false,
},
});
const entityItem = new CatalogEntityItem(mockCluster);
return entityItem;
});
});
afterAll(() => {
jest.restoreAllMocks();
});
it("emits ", () => {
const history = createMemoryHistory();
const mockLocation = {
pathname: "",
search: "",
state: "",
hash: "",
};
const mockMatch = {
params: {},
isExact: true,
path: "",
url: "",
};
render(
<Catalog history={history} location={mockLocation} match={mockMatch} />
);
const emit = jest.spyOn(kubernetesClusterCategory, "emit");
userEvent.click(screen.getByTestId("detail-panel-hot-bar-icon"));
expect(emit).toHaveBeenCalledTimes(1);
expect(emit.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
"onClickDetailIcon",
KubernetesCluster {
"apiVersion": "entity.k8slens.dev/v1alpha1",
"kind": "KubernetesCluster",
"metadata": Object {
"distro": "",
"kubeVersion": "",
"labels": Object {},
"name": "",
"source": "",
"uid": "",
},
"spec": Object {
"icon": Object {},
"kubeconfigContext": "",
"kubeconfigPath": "",
},
"status": Object {
"active": false,
"message": "",
"phase": "disconnected",
"reason": "",
},
},
Object {
"navigate": [Function],
"setCommandPaletteContext": [Function],
},
],
]
`);
});
});

View File

@ -25,7 +25,8 @@ import React from "react";
import { disposeOnUnmount, observer } from "mobx-react";
import { ItemListLayout } from "../item-object-list";
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 { MenuItem, MenuActions } from "../menu";
import { CatalogEntityContextMenu, CatalogEntityContextMenuContext, catalogEntityRunContext } from "../../api/catalog-entity";