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

Allow CatalogEntityDetails to be opened anywhere (#6939)

* Extract CatalogEntityDetails to seperate root component

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Expose entity details to extension API

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add behavioural tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fixup catalog technical tests to use ApplicationBuilder

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Update snapshot

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Update test and then fix it

Signed-off-by: Sebastian Malton <sebastian@malton.name>

---------

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2023-03-07 07:10:14 -08:00 committed by GitHub
parent f88555a1d7
commit 0084af56d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 2680 additions and 405 deletions

View File

@ -34,6 +34,10 @@ export class CatalogCategoryRegistry {
}; };
} }
getById(id: string) {
return iter.find(this.categories.values(), (category) => category.getId() === id);
}
@computed get items() { @computed get items() {
return Array.from(this.categories); return Array.from(this.categories);
} }

View File

@ -3,6 +3,8 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import hideEntityDetailsInjectable from "../../renderer/components/+catalog/entity-details/hide.injectable";
import showEntityDetailsInjectable from "../../renderer/components/+catalog/entity-details/show.injectable";
import getDetailsUrlInjectable from "../../renderer/components/kube-detail-params/get-details-url.injectable"; import getDetailsUrlInjectable from "../../renderer/components/kube-detail-params/get-details-url.injectable";
import hideDetailsInjectable from "../../renderer/components/kube-detail-params/hide-details.injectable"; import hideDetailsInjectable from "../../renderer/components/kube-detail-params/hide-details.injectable";
import showDetailsInjectable from "../../renderer/components/kube-detail-params/show-details.injectable"; import showDetailsInjectable from "../../renderer/components/kube-detail-params/show-details.injectable";
@ -20,3 +22,6 @@ export const hideDetails = asLegacyGlobalFunctionForExtensionApi(hideDetailsInje
export const createPageParam = asLegacyGlobalFunctionForExtensionApi(createPageParamInjectable); export const createPageParam = asLegacyGlobalFunctionForExtensionApi(createPageParamInjectable);
export const isActiveRoute = asLegacyGlobalFunctionForExtensionApi(isActiveRouteInjectable); export const isActiveRoute = asLegacyGlobalFunctionForExtensionApi(isActiveRouteInjectable);
export const navigate = asLegacyGlobalFunctionForExtensionApi(navigateInjectable); export const navigate = asLegacyGlobalFunctionForExtensionApi(navigateInjectable);
export const showEntityDetails = asLegacyGlobalFunctionForExtensionApi(showEntityDetailsInjectable);
export const hideEntityDetails = asLegacyGlobalFunctionForExtensionApi(hideEntityDetailsInjectable);

File diff suppressed because it is too large Load Diff

View File

@ -1925,10 +1925,10 @@ exports[`opening catalog entity details panel when navigated to the catalog when
/> />
</div> </div>
<ul <ul
class="Animate opacity Menu MenuActions flex right bottom portal enter" class="Animate opacity Menu MenuActions flex bottom right portal enter"
data-testid="menu-actions-for-catalog-for-some-entity-id" data-testid="menu-actions-for-catalog-for-some-entity-id"
id="menu-actions-for-catalog-for-some-entity-id" id="menu-actions-for-catalog-for-some-entity-id"
style="--enter-duration: 100ms; --leave-duration: 100ms;" style="--enter-duration: 100ms; --leave-duration: 100ms; left: 0px; top: 8px;"
> >
<li <li
class="MenuItem" class="MenuItem"
@ -2778,10 +2778,10 @@ exports[`opening catalog entity details panel when navigated to the catalog when
/> />
</div> </div>
<ul <ul
class="Animate opacity Menu MenuActions flex right bottom portal enter" class="Animate opacity Menu MenuActions flex bottom right portal enter"
data-testid="menu-actions-for-catalog-for-some-entity-id" data-testid="menu-actions-for-catalog-for-some-entity-id"
id="menu-actions-for-catalog-for-some-entity-id" id="menu-actions-for-catalog-for-some-entity-id"
style="--enter-duration: 100ms; --leave-duration: 100ms;" style="--enter-duration: 100ms; --leave-duration: 100ms; left: 0px; top: 8px;"
> >
<li <li
class="MenuItem" class="MenuItem"
@ -6609,3 +6609,459 @@ exports[`opening catalog entity details panel when navigated to the catalog when
</div> </div>
</body> </body>
`; `;
exports[`opening catalog entity details panel when not navigated to the catalog and showEntityDetails is called from someplace renders 1`] = `
<body>
<div>
<div
class="ClusterManager"
>
<div
class="topBar"
>
<div
class="items"
>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="home-button"
>
<span
class="icon"
data-icon-name="home"
>
home
</span>
</i>
</div>
<div
class="size-sm"
/>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="history-back"
>
<span
class="icon"
data-icon-name="arrow_back"
>
arrow_back
</span>
</i>
</div>
<div
class="size-sm"
/>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="history-forward"
>
<span
class="icon"
data-icon-name="arrow_forward"
>
arrow_forward
</span>
</i>
</div>
<div
class="separator"
/>
</div>
</div>
<main>
<div
id="lens-views"
/>
<div
class="flex justify-center Welcome align-center"
data-testid="welcome-page"
>
<div
data-testid="welcome-banner-container"
style="width: 320px;"
>
<i
class="Icon logo svg focusable"
>
<span
class="icon"
/>
</i>
<div
class="flex justify-center"
>
<div
data-testid="welcome-text-container"
style="width: 320px;"
>
<h2>
Welcome to some-product-name!
</h2>
<p>
To get you started we have auto-detected your clusters in your
kubeconfig file and added them to the catalog, your centralized
view for managing all your cloud-native resources.
<br />
<br />
If you have any questions or feedback, please join our
<a
class="link"
href="https://forums.k8slens.dev"
rel="noreferrer"
target="_blank"
>
Lens Forums
</a>
.
</p>
<ul
class="block"
data-testid="welcome-menu-container"
style="width: 320px;"
>
<li
class="flex grid-12"
>
<i
class="Icon box col-1 material focusable"
>
<span
class="icon"
data-icon-name="view_list"
>
view_list
</span>
</i>
<a
class="box col-10"
>
Browse Clusters in Catalog
</a>
<i
class="Icon box col-1 material focusable"
>
<span
class="icon"
data-icon-name="navigate_next"
>
navigate_next
</span>
</i>
</li>
</ul>
</div>
</div>
</div>
</div>
</main>
<div
class="HotbarMenu flex column"
>
<div
class="HotbarItems flex column gaps"
>
<div
class="HotbarCell isDraggingOwner animateDown"
index="0"
>
<div
style="z-index: 12; position: absolute;"
>
<div
class="HotbarIcon contextMenuAvailable"
>
<div
class="Avatar rounded disabled avatar"
id="hotbarIcon-hotbar-icon-catalog-entity"
style="width: 40px; height: 40px; background: rgb(5, 1, 130);"
>
Ca
</div>
</div>
</div>
</div>
<div
class="HotbarCell isDraggingOwner animateDown"
index="1"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="2"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="3"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="4"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="5"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="6"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="7"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="8"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="9"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="10"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="11"
/>
</div>
<div
class="HotbarSelector"
>
<i
class="Icon Icon previous material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="arrow_left"
>
arrow_left
</span>
</i>
<div
class="HotbarIndex"
>
<div
class="badge Badge small clickable"
id="hotbarIndex"
>
1
</div>
</div>
<i
class="Icon material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="arrow_right"
>
arrow_right
</span>
</i>
</div>
</div>
<div
class="StatusBar"
data-testid="status-bar"
>
<div
class="leftSide"
data-testid="status-bar-left"
/>
<div
class="rightSide"
data-testid="status-bar-right"
/>
</div>
</div>
<div
class="Notifications flex column align-flex-end"
/>
</div>
<div
class="Animate slide-right Drawer entityDetails right enter"
data-testid="catalog-entity-details-drawer"
style="--size: 725px; --enter-duration: 100ms; --leave-duration: 100ms;"
>
<div
class="drawer-wrapper flex column"
>
<div
class="drawer-title flex align-center"
>
<div
class="drawer-title-text flex gaps align-center"
>
WebLink: some-weblink
<i
class="Icon material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="content_copy"
>
content_copy
</span>
</i>
<div>
Copy
</div>
</div>
<i
class="Icon material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="close"
>
close
</span>
</i>
<div>
Close
</div>
</div>
<div
class="drawer-content flex column box grow"
>
<div
class="flex"
data-testid="catalog-entity-details-content-for-some-weblink-id"
>
<div
class="entityIcon"
>
<div
class="Avatar rounded avatar"
data-testid="detail-panel-hot-bar-icon"
style="width: 128px; height: 128px; background: rgb(77, 163, 16);"
>
sw
</div>
<div
class="hint"
>
Click to open
</div>
</div>
<div
class="box grow metadata"
>
<div
class="DrawerItem"
>
<span
class="name"
>
Name
</span>
<span
class="value"
>
some-weblink
</span>
</div>
<div
class="DrawerItem"
>
<span
class="name"
>
Kind
</span>
<span
class="value"
>
WebLink
</span>
</div>
<div
class="DrawerItem"
>
<span
class="name"
>
Source
</span>
<span
class="value"
>
unknown
</span>
</div>
<div
class="DrawerItem"
>
<span
class="name"
>
Status
</span>
<span
class="value"
>
available
</span>
</div>
<div
class="DrawerItem"
>
<span
class="name"
>
Labels
</span>
<span
class="value"
/>
</div>
</div>
</div>
<div
class="box grow"
>
<div
class="DrawerTitle title"
>
More Information
</div>
<div
class="DrawerItem"
data-testid="weblink-url-for-some-weblink-id"
>
<span
class="name"
>
URL
</span>
<span
class="value"
>
https://my-websome.com
</span>
</div>
</div>
</div>
</div>
<div
class="ResizingAnchor horizontal leading"
/>
</div>
</body>
`;

View File

@ -0,0 +1,234 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import asyncFn, { type AsyncFnMock } from "@async-fn/jest";
import type { DiContainer } from "@ogre-tools/injectable";
import type { RenderResult } from "@testing-library/react";
import appEventBusInjectable from "../../common/app-event-bus/app-event-bus.injectable";
import type { AppEvent } from "../../common/app-event-bus/event-bus";
import type { CatalogEntityActionContext } from "../../common/catalog";
import { CatalogCategory, categoryVersion, CatalogEntity } from "../../common/catalog";
import catalogCategoryRegistryInjectable from "../../common/catalog/category-registry.injectable";
import navigateToCatalogInjectable from "../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable";
import { flushPromises } from "../../common/test-utils/flush-promises";
import { advanceFakeTime, testUsingFakeTime } from "../../common/test-utils/use-fake-time";
import type { CatalogEntityOnBeforeRun, CatalogEntityRegistry } from "../../renderer/api/catalog/entity/registry";
import catalogEntityRegistryInjectable from "../../renderer/api/catalog/entity/registry.injectable";
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
class MockCatalogCategory extends CatalogCategory {
apiVersion = "catalog.k8slens.dev/v1alpha1";
kind = "CatalogCategory";
metadata = {
name: "mock",
icon: "gear",
};
spec = {
group: "entity.k8slens.dev",
versions: [
categoryVersion("v1alpha1", (() => {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this;
return function (data: any) {
const entity = new MockCatalogEntity(data);
entity.onRun = self.onRun;
return entity;
} as any;
})()),
],
names: {
kind: "Mock",
},
};
constructor(private onRun: (context: CatalogEntityActionContext) => void | Promise<void>) {
super();
}
}
class MockCatalogEntity extends CatalogEntity {
public apiVersion = "entity.k8slens.dev/v1alpha1";
public kind = "Mock";
}
function createMockCatalogEntity() {
return new MockCatalogEntity({
metadata: {
uid: "a_catalogEntity_uid",
name: "a catalog entity",
labels: {
test: "label",
},
},
status: {
phase: "",
},
spec: {},
});
}
describe("entity running technical tests", () => {
let builder: ApplicationBuilder;
let windowDi: DiContainer;
let rendered: RenderResult;
let appEventListener: jest.MockedFunction<(event: AppEvent) => void>;
let onRun: jest.MockedFunction<(context: CatalogEntityActionContext) => void | Promise<void>>;
let catalogEntityRegistry: CatalogEntityRegistry;
beforeEach(async () => {
builder = getApplicationBuilder();
builder.afterWindowStart((windowDi) => {
onRun = jest.fn();
const catalogCategoryRegistery = windowDi.inject(catalogCategoryRegistryInjectable);
catalogCategoryRegistery.add(new MockCatalogCategory(onRun));
catalogEntityRegistry = windowDi.inject(catalogEntityRegistryInjectable);
const catalogEntityItem = createMockCatalogEntity();
catalogEntityRegistry.updateItems([catalogEntityItem]);
appEventListener = jest.fn();
windowDi.inject(appEventBusInjectable).addListener(appEventListener);
});
testUsingFakeTime();
rendered = await builder.render();
windowDi = builder.applicationWindow.only.di;
});
describe("when navigated to catalog", () => {
beforeEach(() => {
const navigateToCatalog = windowDi.inject(navigateToCatalogInjectable);
navigateToCatalog();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
describe("when details panel is opened", () => {
beforeEach(() => {
rendered.getByTestId("icon-for-menu-actions-for-catalog-for-a_catalogEntity_uid").click();
advanceFakeTime(500);
rendered.getByTestId("open-details-menu-item-for-a_catalogEntity_uid").click();
advanceFakeTime(500);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
describe("can use catalogEntityRegistry.addOnBeforeRun to add hooks for catalog entities", () => {
let onBeforeRunMock: AsyncFnMock<CatalogEntityOnBeforeRun>;
beforeEach(() => {
onBeforeRunMock = asyncFn();
catalogEntityRegistry.addOnBeforeRun(onBeforeRunMock);
rendered.getByTestId("detail-panel-hot-bar-icon").click();
});
it("calls on before run event", () => {
const target = onBeforeRunMock.mock.calls[0][0].target;
const actual = { id: target.getId(), name: target.getName() };
expect(actual).toEqual({
id: "a_catalogEntity_uid",
name: "a catalog entity",
});
});
it("does not call onRun yet", () => {
expect(onRun).not.toHaveBeenCalled();
});
it("when before run event resolves, calls onRun", async () => {
await onBeforeRunMock.resolve();
expect(onRun).toHaveBeenCalled();
});
});
it("onBeforeRun prevents event => onRun wont be triggered", async () => {
const onBeforeRunMock = jest.fn((event) => event.preventDefault());
catalogEntityRegistry.addOnBeforeRun(onBeforeRunMock);
rendered.getByTestId("detail-panel-hot-bar-icon").click();
await flushPromises();
expect(onRun).not.toHaveBeenCalled();
});
it("addOnBeforeRun throw an exception => onRun will be triggered", async () => {
catalogEntityRegistry.addOnBeforeRun(() => {
throw new Error("some error");
});
rendered.getByTestId("detail-panel-hot-bar-icon").click();
await flushPromises();
expect(onRun).toHaveBeenCalled();
});
it("addOnRunHook return a promise and does not prevent run event => onRun()", (done) => {
onRun.mockImplementation(() => done());
catalogEntityRegistry.addOnBeforeRun(async () => {});
rendered.getByTestId("detail-panel-hot-bar-icon").click();
});
it("addOnRunHook return a promise and prevents event wont be triggered", async () => {
catalogEntityRegistry.addOnBeforeRun(async (event) => event.preventDefault());
rendered.getByTestId("detail-panel-hot-bar-icon").click();
expect(onRun).not.toHaveBeenCalled();
});
it("addOnRunHook return a promise and reject => onRun will be triggered", async () => {
const onBeforeRunMock = asyncFn();
catalogEntityRegistry.addOnBeforeRun(onBeforeRunMock);
rendered.getByTestId("detail-panel-hot-bar-icon").click();
await onBeforeRunMock.reject();
expect(onRun).toHaveBeenCalled();
});
it("emits catalog open AppEvent", () => {
expect(appEventListener).toHaveBeenCalledWith( {
action: "open",
name: "catalog",
});
});
it("emits catalog change AppEvent when changing the category", () => {
rendered.getByText("Web Links").click();
expect(appEventListener).toHaveBeenCalledWith({
action: "change-category",
name: "catalog",
params: {
category: "Web Links",
},
});
});
});
});
});

View File

@ -9,18 +9,20 @@ import { KubernetesCluster, WebLink } from "../../common/catalog-entities";
import getClusterByIdInjectable from "../../common/cluster-store/get-by-id.injectable"; import getClusterByIdInjectable from "../../common/cluster-store/get-by-id.injectable";
import type { Cluster } from "../../common/cluster/cluster"; import type { Cluster } from "../../common/cluster/cluster";
import navigateToCatalogInjectable from "../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable"; import navigateToCatalogInjectable from "../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable";
import { advanceFakeTime, testUsingFakeTime } from "../../common/test-utils/use-fake-time";
import catalogEntityRegistryInjectable from "../../renderer/api/catalog/entity/registry.injectable"; import catalogEntityRegistryInjectable from "../../renderer/api/catalog/entity/registry.injectable";
import createClusterInjectable from "../../renderer/cluster/create-cluster.injectable"; import createClusterInjectable from "../../renderer/cluster/create-cluster.injectable";
import showEntityDetailsInjectable from "../../renderer/components/+catalog/entity-details/show.injectable";
import { type ApplicationBuilder, getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import { type ApplicationBuilder, getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
describe("opening catalog entity details panel", () => { describe("opening catalog entity details panel", () => {
let builder: ApplicationBuilder; let builder: ApplicationBuilder;
let rendered: RenderResult; let rendered: RenderResult;
let windowDi: DiContainer; let windowDi: DiContainer;
let cluster: Cluster;
let clusterEntity: KubernetesCluster; let clusterEntity: KubernetesCluster;
let localClusterEntity: KubernetesCluster; let localClusterEntity: KubernetesCluster;
let otherEntity: WebLink; let otherEntity: WebLink;
let cluster: Cluster;
beforeEach(async () => { beforeEach(async () => {
builder = getApplicationBuilder(); builder = getApplicationBuilder();
@ -28,7 +30,7 @@ describe("opening catalog entity details panel", () => {
builder.beforeWindowStart((windowDi) => { builder.beforeWindowStart((windowDi) => {
// TODO: remove once ClusterStore can be used without overriding it // TODO: remove once ClusterStore can be used without overriding it
windowDi.override(getClusterByIdInjectable, () => (clusterId) => { windowDi.override(getClusterByIdInjectable, () => (clusterId) => {
if (clusterId === cluster.id) { if (clusterId === cluster?.id) {
return cluster; return cluster;
} }
@ -36,6 +38,8 @@ describe("opening catalog entity details panel", () => {
}); });
}); });
testUsingFakeTime();
builder.afterWindowStart((windowDi) => { builder.afterWindowStart((windowDi) => {
const createCluster = windowDi.inject(createClusterInjectable); const createCluster = windowDi.inject(createClusterInjectable);
@ -129,6 +133,7 @@ describe("opening catalog entity details panel", () => {
describe("when opening the menu 'some-kubernetes-cluster'", () => { describe("when opening the menu 'some-kubernetes-cluster'", () => {
beforeEach(() => { beforeEach(() => {
rendered.getByTestId("icon-for-menu-actions-for-catalog-for-some-entity-id").click(); rendered.getByTestId("icon-for-menu-actions-for-catalog-for-some-entity-id").click();
advanceFakeTime(1000);
}); });
it("renders", () => { it("renders", () => {
@ -154,6 +159,7 @@ describe("opening catalog entity details panel", () => {
describe("when the panel opens", () => { describe("when the panel opens", () => {
beforeEach(async () => { beforeEach(async () => {
advanceFakeTime(1000);
await rendered.findAllByTestId("catalog-entity-details-drawer"); await rendered.findAllByTestId("catalog-entity-details-drawer");
}); });
@ -222,4 +228,21 @@ describe("opening catalog entity details panel", () => {
}); });
}); });
}); });
describe("when not navigated to the catalog and showEntityDetails is called from someplace", () => {
beforeEach(async () => {
const showEntityDetails = windowDi.inject(showEntityDetailsInjectable);
showEntityDetails("some-weblink-id");
advanceFakeTime(1000);
});
it("renders", async () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("opens the detail panel for the correct item", () => {
expect(rendered.queryByTestId("catalog-entity-details-content-for-some-weblink-id")).toBeInTheDocument();
});
});
}); });

View File

@ -0,0 +1,20 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { CatalogEntity } from "../../catalog-entity";
import catalogEntityRegistryInjectable from "./registry.injectable";
export type GetEntityById = (id: string) => CatalogEntity | undefined;
const getEntityByIdInjectable = getInjectable({
id: "get-entity-by-id",
instantiate: (di): GetEntityById => {
const catalogEntityRegistry = di.inject(catalogEntityRegistryInjectable);
return (id) => catalogEntityRegistry.getById(id);
},
});
export default getEntityByIdInjectable;

View File

@ -23,7 +23,7 @@ export type CatalogEntityOnBeforeRun = (event: CatalogRunEvent) => void | Promis
interface Dependencies { interface Dependencies {
navigate: Navigate; navigate: Navigate;
readonly categoryRegistry: CatalogCategoryRegistry; readonly categoryRegistry: CatalogCategoryRegistry;
logger: Logger; readonly logger: Logger;
} }
export class CatalogEntityRegistry { export class CatalogEntityRegistry {
@ -243,20 +243,24 @@ export class CatalogEntityRegistry {
* Perform the onBeforeRun check and, if successful, then proceed to call `entity`'s onRun method * 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 * @param entity The instance to invoke the hooks and then execute the onRun
*/ */
onRun(entity: CatalogEntity): void { async onRun(entity: CatalogEntity) {
this.onBeforeRun(entity) try {
.then(doOnRun => { const doOnRun = await this.onBeforeRun(entity);
if (doOnRun) {
return entity.onRun?.({ if (!doOnRun) {
navigate: this.dependencies.navigate, this.dependencies.logger.debug(`onBeforeRun for ${entity.getId()} returned false`);
setCommandPaletteContext: (entity) => {
this.activeEntity = entity; return;
}, }
});
} else { await entity.onRun?.({
this.dependencies.logger.debug(`onBeforeRun for ${entity.getId()} returned false`); navigate: this.dependencies.navigate,
} setCommandPaletteContext: (entity) => {
}) this.activeEntity = entity;
.catch(error => this.dependencies.logger.error(`[CATALOG-ENTITY-REGISTRY]: entity ${entity.getId()} onRun threw an error`, error)); },
});
} catch (error) {
this.dependencies.logger.error(`[CATALOG-ENTITY-REGISTRY]: entity ${entity.getId()} onRun threw an error`, error);
}
} }
} }

View File

@ -3,12 +3,16 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import type { DiContainer } from "@ogre-tools/injectable";
import type { CatalogCategoryMetadata, CatalogCategorySpec } from "../../../../common/catalog"; import type { CatalogCategoryMetadata, CatalogCategorySpec } from "../../../../common/catalog";
import { CatalogEntity, categoryVersion } from "../../../../common/catalog"; import { CatalogEntity, categoryVersion } from "../../../../common/catalog";
import catalogCategoryRegistryInjectable from "../../../../common/catalog/category-registry.injectable";
import { CatalogCategory } from "../../../api/catalog-entity"; import { CatalogCategory } from "../../../api/catalog-entity";
import catalogEntityRegistryInjectable from "../../../api/catalog/entity/registry.injectable";
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
import { noop } from "../../../utils"; import { noop } from "../../../utils";
import type { CatalogEntityStore } from "../catalog-entity-store/catalog-entity.store"; import type { CatalogEntityStore } from "../catalog-entity-store.injectable";
import { catalogEntityStore } from "../catalog-entity-store/catalog-entity.store"; import catalogEntityStoreInjectable from "../catalog-entity-store.injectable";
class TestEntityOne extends CatalogEntity { class TestEntityOne extends CatalogEntity {
public static readonly apiVersion: string = "entity.k8slens.dev/v1alpha1"; public static readonly apiVersion: string = "entity.k8slens.dev/v1alpha1";
@ -63,6 +67,12 @@ class TestCategoryTwo extends CatalogCategory {
} }
describe("CatalogEntityStore", () => { describe("CatalogEntityStore", () => {
let di: DiContainer;
beforeEach(() => {
di = getDiForUnitTesting({ doGeneralOverrides: true });
});
describe("getTotalCount", () => { describe("getTotalCount", () => {
let store: CatalogEntityStore; let store: CatalogEntityStore;
let testCategoryOne: TestCategoryOne; let testCategoryOne: TestCategoryOne;
@ -129,21 +139,22 @@ describe("CatalogEntityStore", () => {
testCategoryOne = new TestCategoryOne(); testCategoryOne = new TestCategoryOne();
testCategoryTwo = new TestCategoryTwo(); testCategoryTwo = new TestCategoryTwo();
store = catalogEntityStore({
catalogRegistry: { di.override(catalogCategoryRegistryInjectable, () => ({
items: [ items: [
testCategoryOne, testCategoryOne,
testCategoryTwo, testCategoryTwo,
], ],
}));
di.override(catalogEntityRegistryInjectable, () => ({
onRun: noop,
filteredItems: entityItems,
getItemsForCategory: <T extends CatalogEntity>(category: CatalogCategory): T[] => {
return entityItems.filter(item => category.spec.versions.some(version => item instanceof version.entityClass)) as T[];
}, },
entityRegistry: { } as any));
onRun: noop,
filteredItems: entityItems, store = di.inject(catalogEntityStoreInjectable);
getItemsForCategory: <T extends CatalogEntity>(category: CatalogCategory): T[] => {
return entityItems.filter(item => category.spec.versions.some(version => item instanceof version.entityClass)) as T[];
},
},
});
}); });
it("given no active category, returns count of all kinds", () => { it("given no active category, returns count of all kinds", () => {

View File

@ -0,0 +1,74 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import catalogEntityRegistryInjectable from "../../api/catalog/entity/registry.injectable";
import type { IComputedValue, IObservableValue } from "mobx";
import { computed, observable, reaction } from "mobx";
import type { CatalogEntity } from "../../api/catalog-entity";
import type { CatalogCategory } from "../../../common/catalog";
import type { Disposer } from "../../../common/utils";
import { disposer } from "../../../common/utils";
import type { ItemListStore } from "../item-object-list";
import catalogCategoryRegistryInjectable from "../../../common/catalog/category-registry.injectable";
import selectedCatalogEntityParamInjectable from "./entity-details/selected-uid.injectable";
export type CatalogEntityStore = ItemListStore<CatalogEntity, false> & {
readonly entities: IComputedValue<CatalogEntity[]>;
readonly activeCategory: IObservableValue<CatalogCategory | undefined>;
watch(): Disposer;
onRun(entity: CatalogEntity): void;
};
const catalogEntityStoreInjectable = getInjectable({
id: "catalog-entity-store",
instantiate: (di): CatalogEntityStore => {
const catalogEntityRegistry = di.inject(catalogEntityRegistryInjectable);
const catalogCategoryRegistry = di.inject(catalogCategoryRegistryInjectable);
const selectedCatalogEntityParam = di.inject(selectedCatalogEntityParamInjectable);
const activeCategory = observable.box<CatalogCategory | undefined>(undefined);
const entities = computed(() => {
const category = activeCategory.get();
return category
? catalogEntityRegistry.getItemsForCategory(category, { filtered: true })
: catalogEntityRegistry.filteredItems;
});
const loadAll = () => {
const category = activeCategory.get();
if (category) {
category.emit("load");
} else {
for (const category of catalogCategoryRegistry.items) {
category.emit("load");
}
}
};
return {
entities,
activeCategory,
watch: () => disposer(
reaction(() => entities.get(), loadAll),
reaction(() => activeCategory.get(), loadAll, { delay: 100 }),
),
onRun: entity => catalogEntityRegistry.onRun(entity),
failedLoading: false,
getTotalCount: () => entities.get().length,
isLoaded: true,
isSelected: (item) => item.getId() === selectedCatalogEntityParam.get(),
isSelectedAll: () => false,
pickOnlySelected: () => [],
toggleSelection: () => {},
toggleSelectionAll: () => {},
removeSelectedItems: async () => {},
};
},
});
export default catalogEntityStoreInjectable;

View File

@ -1,19 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { catalogEntityStore } from "./catalog-entity.store";
import catalogEntityRegistryInjectable from "../../../api/catalog/entity/registry.injectable";
import catalogCategoryRegistryInjectable from "../../../../common/catalog/category-registry.injectable";
const catalogEntityStoreInjectable = getInjectable({
id: "catalog-entity-store",
instantiate: (di) => catalogEntityStore({
entityRegistry: di.inject(catalogEntityRegistryInjectable),
catalogRegistry: di.inject(catalogCategoryRegistryInjectable),
}),
});
export default catalogEntityStoreInjectable;

View File

@ -1,86 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { IComputedValue, IObservableValue } from "mobx";
import { computed, observable, reaction } from "mobx";
import type { CatalogEntityRegistry } from "../../../api/catalog/entity/registry";
import type { CatalogEntity } from "../../../api/catalog-entity";
import type { CatalogCategory, CatalogCategoryRegistry } from "../../../../common/catalog";
import type { Disposer } from "../../../../common/utils";
import { disposer } from "../../../../common/utils";
import type { ItemListStore } from "../../item-object-list";
type EntityRegistry = Pick<CatalogEntityRegistry, "getItemsForCategory" | "filteredItems" | "onRun">;
type CatalogRegistry = Pick<CatalogCategoryRegistry, "items">;
interface Dependencies {
entityRegistry: EntityRegistry;
catalogRegistry: CatalogRegistry;
}
export type CatalogEntityStore = ItemListStore<CatalogEntity, false> & {
readonly entities: IComputedValue<CatalogEntity[]>;
readonly activeCategory: IObservableValue<CatalogCategory | undefined>;
readonly selectedItemId: IObservableValue<string | undefined>;
readonly selectedItem: IComputedValue<CatalogEntity | undefined>;
watch(): Disposer;
onRun(entity: CatalogEntity): void;
};
export function catalogEntityStore({
entityRegistry,
catalogRegistry,
}: Dependencies): CatalogEntityStore {
const activeCategory = observable.box<CatalogCategory | undefined>(undefined);
const selectedItemId = observable.box<string | undefined>(undefined);
const entities = computed(() => {
const category = activeCategory.get();
return category
? entityRegistry.getItemsForCategory(category, { filtered: true })
: entityRegistry.filteredItems;
});
const selectedItem = computed(() => {
const id = selectedItemId.get();
if (!id) {
return undefined;
}
return entities.get().find(entity => entity.getId() === id);
});
const loadAll = () => {
const category = activeCategory.get();
if (category) {
category.emit("load");
} else {
for (const category of catalogRegistry.items) {
category.emit("load");
}
}
};
return {
entities,
selectedItem,
activeCategory,
selectedItemId,
watch: () => disposer(
reaction(() => entities.get(), loadAll),
reaction(() => activeCategory.get(), loadAll, { delay: 100 }),
),
onRun: entity => entityRegistry.onRun(entity),
failedLoading: false,
getTotalCount: () => entities.get().length,
isLoaded: true,
isSelected: (item) => item.getId() === selectedItemId.get(),
isSelectedAll: () => false,
pickOnlySelected: () => [],
toggleSelection: () => {},
toggleSelectionAll: () => {},
removeSelectedItems: async () => {},
};
}

View File

@ -1,229 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Catalog } from "./catalog";
import type { CatalogEntityActionContext, CatalogEntityData } from "../../../common/catalog";
import { CatalogEntity } from "../../../common/catalog";
import type { CatalogEntityOnBeforeRun, CatalogEntityRegistry } from "../../api/catalog/entity/registry";
import type { CatalogEntityStore } from "./catalog-entity-store/catalog-entity.store";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import type { DiContainer } from "@ogre-tools/injectable";
import catalogEntityStoreInjectable from "./catalog-entity-store/catalog-entity-store.injectable";
import catalogEntityRegistryInjectable from "../../api/catalog/entity/registry.injectable";
import type { DiRender } from "../test-utils/renderFor";
import { renderFor } from "../test-utils/renderFor";
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import getConfigurationFileModelInjectable from "../../../common/get-configuration-file-model/get-configuration-file-model.injectable";
import type { AppEvent } from "../../../common/app-event-bus/event-bus";
import appEventBusInjectable from "../../../common/app-event-bus/app-event-bus.injectable";
import { computed } from "mobx";
import broadcastMessageInjectable from "../../../common/ipc/broadcast-message.injectable";
import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import { flushPromises } from "../../../common/test-utils/flush-promises";
import userStoreInjectable from "../../../common/user-store/user-store.injectable";
import releaseChannelInjectable from "../../../common/vars/release-channel.injectable";
import defaultUpdateChannelInjectable from "../../../features/application-update/common/selected-update-channel/default-update-channel.injectable";
import currentlyInClusterFrameInjectable from "../../routes/currently-in-cluster-frame.injectable";
class MockCatalogEntity extends CatalogEntity {
public apiVersion = "api";
public kind = "kind";
constructor(data: CatalogEntityData, public onRun: (context: CatalogEntityActionContext) => void | Promise<void>) {
super(data);
}
}
function createMockCatalogEntity(onRun: (context: CatalogEntityActionContext) => void | Promise<void>) {
return new MockCatalogEntity({
metadata: {
uid: "a_catalogEntity_uid",
name: "a catalog entity",
labels: {
test: "label",
},
},
status: {
phase: "",
},
spec: {},
}, onRun);
}
describe("<Catalog />", () => {
let di: DiContainer;
let catalogEntityStore: CatalogEntityStore;
let catalogEntityRegistry: CatalogEntityRegistry;
let appEventListener: jest.MockedFunction<(event: AppEvent) => void>;
let onRun: jest.MockedFunction<(context: CatalogEntityActionContext) => void | Promise<void>>;
let catalogEntityItem: MockCatalogEntity;
let render: DiRender;
beforeEach(async () => {
di = getDiForUnitTesting({ doGeneralOverrides: true });
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
di.override(currentlyInClusterFrameInjectable, () => false);
di.override(broadcastMessageInjectable, () => async () => {});
di.permitSideEffects(getConfigurationFileModelInjectable);
render = renderFor(di);
onRun = jest.fn();
catalogEntityItem = createMockCatalogEntity(onRun);
catalogEntityRegistry = di.inject(catalogEntityRegistryInjectable);
appEventListener = jest.fn();
di.inject(appEventBusInjectable).addListener(appEventListener);
catalogEntityStore = di.inject(catalogEntityStoreInjectable);
Object.assign(catalogEntityStore, {
selectedItem: computed(() => catalogEntityItem),
});
di.override(releaseChannelInjectable, () => ({
get: () => "latest" as const,
init: async () => {},
}));
await di.inject(defaultUpdateChannelInjectable).init();
di.inject(userStoreInjectable).load();
});
describe("can use catalogEntityRegistry.addOnBeforeRun to add hooks for catalog entities", () => {
let onBeforeRunMock: AsyncFnMock<CatalogEntityOnBeforeRun>;
beforeEach(() => {
onBeforeRunMock = asyncFn();
catalogEntityRegistry.addOnBeforeRun(onBeforeRunMock);
render(<Catalog />);
userEvent.click(screen.getByTestId("detail-panel-hot-bar-icon"));
});
it("calls on before run event", () => {
const target = onBeforeRunMock.mock.calls[0][0].target;
const actual = { id: target.getId(), name: target.getName() };
expect(actual).toEqual({
id: "a_catalogEntity_uid",
name: "a catalog entity",
});
});
it("does not call onRun yet", () => {
expect(onRun).not.toHaveBeenCalled();
});
it("when before run event resolves, calls onRun", async () => {
await onBeforeRunMock.resolve();
expect(onRun).toHaveBeenCalled();
});
});
it("onBeforeRun prevents event => onRun wont be triggered", async () => {
const onBeforeRunMock = jest.fn((event) => event.preventDefault());
catalogEntityRegistry.addOnBeforeRun(onBeforeRunMock);
render(<Catalog />);
userEvent.click(screen.getByTestId("detail-panel-hot-bar-icon"));
await flushPromises();
expect(onRun).not.toHaveBeenCalled();
});
it("addOnBeforeRun throw an exception => onRun will be triggered", async () => {
const onBeforeRunMock = jest.fn(() => {
throw new Error("some error");
});
catalogEntityRegistry.addOnBeforeRun(onBeforeRunMock);
render(<Catalog />);
userEvent.click(screen.getByTestId("detail-panel-hot-bar-icon"));
await flushPromises();
expect(onRun).toHaveBeenCalled();
});
it("addOnRunHook return a promise and does not prevent run event => onRun()", (done) => {
onRun.mockImplementation(() => done());
catalogEntityRegistry.addOnBeforeRun(
async () => {
// no op
},
);
render(<Catalog />);
userEvent.click(screen.getByTestId("detail-panel-hot-bar-icon"));
});
it("addOnRunHook return a promise and prevents event wont be triggered", async () => {
const onBeforeRunMock = asyncFn();
catalogEntityRegistry.addOnBeforeRun(onBeforeRunMock);
render(<Catalog />);
userEvent.click(screen.getByTestId("detail-panel-hot-bar-icon"));
onBeforeRunMock.mock.calls[0][0].preventDefault();
await onBeforeRunMock.resolve();
expect(onRun).not.toHaveBeenCalled();
});
it("addOnRunHook return a promise and reject => onRun will be triggered", async () => {
const onBeforeRunMock = asyncFn();
catalogEntityRegistry.addOnBeforeRun(onBeforeRunMock);
render(<Catalog />);
userEvent.click(screen.getByTestId("detail-panel-hot-bar-icon"));
await onBeforeRunMock.reject();
expect(onRun).toHaveBeenCalled();
});
it("emits catalog open AppEvent", () => {
render(<Catalog />);
expect(appEventListener).toHaveBeenCalledWith( {
action: "open",
name: "catalog",
});
});
it("emits catalog change AppEvent when changing the category", () => {
render(<Catalog />);
userEvent.click(screen.getByText("Web Links"));
expect(appEventListener).toHaveBeenCalledWith({
action: "change-category",
name: "catalog",
params: {
category: "Web Links",
},
});
});
});

View File

@ -10,7 +10,7 @@ import { disposeOnUnmount, observer } from "mobx-react";
import { ItemListLayout } from "../item-object-list"; import { ItemListLayout } from "../item-object-list";
import type { IComputedValue } from "mobx"; import type { IComputedValue } from "mobx";
import { action, computed, makeObservable, observable, reaction, runInAction, when } from "mobx"; import { action, computed, makeObservable, observable, reaction, runInAction, when } from "mobx";
import type { CatalogEntityStore } from "./catalog-entity-store/catalog-entity.store"; import type { CatalogEntityStore } from "./catalog-entity-store.injectable";
import { MenuItem, MenuActions } from "../menu"; import { MenuItem, MenuActions } from "../menu";
import type { CatalogEntityContextMenu } from "../../api/catalog-entity"; import type { CatalogEntityContextMenu } from "../../api/catalog-entity";
import type { CatalogCategory, CatalogCategoryRegistry, CatalogEntity } from "../../../common/catalog"; import type { CatalogCategory, CatalogCategoryRegistry, CatalogEntity } from "../../../common/catalog";
@ -19,7 +19,6 @@ import type { ShowNotification } from "../notifications";
import { MainLayout } from "../layout/main-layout"; import { MainLayout } from "../layout/main-layout";
import type { StorageLayer } from "../../utils"; import type { StorageLayer } from "../../utils";
import { prevDefault } from "../../utils"; import { prevDefault } from "../../utils";
import { CatalogEntityDetails } from "./entity-details/view";
import { CatalogMenu } from "./catalog-menu"; import { CatalogMenu } from "./catalog-menu";
import { RenderDelay } from "../render-delay/render-delay"; import { RenderDelay } from "../render-delay/render-delay";
import { Icon } from "../icon"; import { Icon } from "../icon";
@ -27,7 +26,7 @@ import { HotbarToggleMenuItem } from "./hotbar-toggle-menu-item";
import { Avatar } from "../avatar"; import { Avatar } from "../avatar";
import { withInjectables } from "@ogre-tools/injectable-react"; import { withInjectables } from "@ogre-tools/injectable-react";
import catalogPreviousActiveTabStorageInjectable from "./catalog-previous-active-tab-storage/catalog-previous-active-tab-storage.injectable"; import catalogPreviousActiveTabStorageInjectable from "./catalog-previous-active-tab-storage/catalog-previous-active-tab-storage.injectable";
import catalogEntityStoreInjectable from "./catalog-entity-store/catalog-entity-store.injectable"; import catalogEntityStoreInjectable from "./catalog-entity-store.injectable";
import type { GetCategoryColumnsParams, CategoryColumns } from "./columns/get.injectable"; import type { GetCategoryColumnsParams, CategoryColumns } from "./columns/get.injectable";
import getCategoryColumnsInjectable from "./columns/get.injectable"; import getCategoryColumnsInjectable from "./columns/get.injectable";
import type { RegisteredCustomCategoryViewDecl } from "./custom-views.injectable"; import type { RegisteredCustomCategoryViewDecl } from "./custom-views.injectable";
@ -51,6 +50,10 @@ import emitAppEventInjectable from "../../../common/app-event-bus/emit-event.inj
import type { Logger } from "../../../common/logger"; import type { Logger } from "../../../common/logger";
import loggerInjectable from "../../../common/logger.injectable"; import loggerInjectable from "../../../common/logger.injectable";
import showErrorNotificationInjectable from "../notifications/show-error-notification.injectable"; import showErrorNotificationInjectable from "../notifications/show-error-notification.injectable";
import type { ShowEntityDetails } from "./entity-details/show.injectable";
import showEntityDetailsInjectable from "./entity-details/show.injectable";
import type { OnCatalogEntityListClick } from "./entity-details/on-catalog-click.injectable";
import onCatalogEntityListClickInjectable from "./entity-details/on-catalog-click.injectable";
interface Dependencies { interface Dependencies {
catalogPreviousActiveTabStorage: StorageLayer<string | null>; catalogPreviousActiveTabStorage: StorageLayer<string | null>;
@ -58,6 +61,8 @@ interface Dependencies {
getCategoryColumns: (params: GetCategoryColumnsParams) => CategoryColumns; getCategoryColumns: (params: GetCategoryColumnsParams) => CategoryColumns;
customCategoryViews: IComputedValue<Map<string, Map<string, RegisteredCustomCategoryViewDecl>>>; customCategoryViews: IComputedValue<Map<string, Map<string, RegisteredCustomCategoryViewDecl>>>;
emitEvent: EmitAppEvent; emitEvent: EmitAppEvent;
showEntityDetails: ShowEntityDetails;
onCatalogEntityListClick: OnCatalogEntityListClick;
routeParameters: { routeParameters: {
group: IComputedValue<string>; group: IComputedValue<string>;
kind: IComputedValue<string>; kind: IComputedValue<string>;
@ -161,26 +166,16 @@ class NonInjectedCatalog extends React.Component<Dependencies> {
this.props.hotbarStore.removeFromHotbar(entity.getId()); this.props.hotbarStore.removeFromHotbar(entity.getId());
} }
onDetails = (entity: CatalogEntity) => {
if (this.props.catalogEntityStore.selectedItemId.get()) {
this.props.catalogEntityStore.selectedItemId.set(undefined);
} else {
this.props.catalogEntityStore.onRun(entity);
}
};
get categories() {
return this.props.catalogCategoryRegistry.items;
}
onTabChange = action((tabId: string | null) => { onTabChange = action((tabId: string | null) => {
const activeCategory = this.categories.find(category => category.getId() === tabId); const activeCategory = tabId
? this.props.catalogCategoryRegistry.getById(tabId)
: undefined;
this.props.emitEvent({ this.props.emitEvent({
name: "catalog", name: "catalog",
action: "change-category", action: "change-category",
params: { params: {
category: activeCategory ? activeCategory.getName() : "Browse", category: activeCategory?.getName() ?? "Browse",
}, },
}); });
@ -211,7 +206,7 @@ class NonInjectedCatalog extends React.Component<Dependencies> {
<MenuItem <MenuItem
key="open-details" key="open-details"
data-testid={`open-details-menu-item-for-${entity.getId()}`} data-testid={`open-details-menu-item-for-${entity.getId()}`}
onClick={() => this.props.catalogEntityStore.selectedItemId.set(entity.getId())} onClick={() => this.props.showEntityDetails(entity.getId())}
> >
View Details View Details
</MenuItem> </MenuItem>
@ -309,7 +304,7 @@ class NonInjectedCatalog extends React.Component<Dependencies> {
disabled: !entity.isEnabled(), disabled: !entity.isEnabled(),
})} })}
{...getCategoryColumns({ activeCategory })} {...getCategoryColumns({ activeCategory })}
onDetails={this.onDetails} onDetails={this.props.onCatalogEntityListClick}
renderItemMenu={this.renderItemMenu} renderItemMenu={this.renderItemMenu}
data-testid={`catalog-list-for-${activeCategory?.metadata.name ?? "browse-all"}`} data-testid={`catalog-list-for-${activeCategory?.metadata.name ?? "browse-all"}`}
/> />
@ -318,7 +313,6 @@ class NonInjectedCatalog extends React.Component<Dependencies> {
render() { render() {
const activeCategory = this.props.catalogEntityStore.activeCategory.get(); const activeCategory = this.props.catalogEntityStore.activeCategory.get();
const selectedItem = this.props.catalogEntityStore.selectedItem.get();
return ( return (
<MainLayout <MainLayout
@ -333,21 +327,13 @@ class NonInjectedCatalog extends React.Component<Dependencies> {
{this.renderViews(activeCategory)} {this.renderViews(activeCategory)}
</div> </div>
{ {
selectedItem activeCategory
? ( ? (
<CatalogEntityDetails <RenderDelay>
entity={selectedItem} <CatalogAddButton category={activeCategory} />
hideDetails={() => this.props.catalogEntityStore.selectedItemId.set(undefined)} </RenderDelay>
onRun={() => this.props.catalogEntityStore.onRun(selectedItem)}
/>
) )
: activeCategory : null
? (
<RenderDelay>
<CatalogAddButton category={activeCategory} />
</RenderDelay>
)
: null
} }
</MainLayout> </MainLayout>
); );
@ -371,5 +357,7 @@ export const Catalog = withInjectables<Dependencies>(NonInjectedCatalog, {
normalizeMenuItem: di.inject(normalizeCatalogEntityContextMenuInjectable), normalizeMenuItem: di.inject(normalizeCatalogEntityContextMenuInjectable),
logger: di.inject(loggerInjectable), logger: di.inject(loggerInjectable),
showErrorNotification: di.inject(showErrorNotificationInjectable), showErrorNotification: di.inject(showErrorNotificationInjectable),
showEntityDetails: di.inject(showEntityDetailsInjectable),
onCatalogEntityListClick: di.inject(onCatalogEntityListClickInjectable),
}), }),
}); });

View File

@ -0,0 +1,65 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { withInjectables } from "@ogre-tools/injectable-react";
import type { IComputedValue } from "mobx";
import { computed } from "mobx";
import { observer } from "mobx-react";
import React from "react";
import type { CatalogEntity } from "../../../api/catalog-entity";
import type { CatalogEntityRegistry } from "../../../api/catalog/entity/registry";
import catalogEntityRegistryInjectable from "../../../api/catalog/entity/registry.injectable";
import { rootFrameChildComponentInjectionToken } from "../../../frames/root-frame/root-frame-child-component-injection-token";
import type { HideEntityDetails } from "./hide.injectable";
import hideEntityDetailsInjectable from "./hide.injectable";
import selectedCatalogEntityInjectable from "./selected-entity.injectable";
import { CatalogEntityDetails } from "./view";
interface Dependencies {
selectedCatalogEntity: IComputedValue<CatalogEntity | undefined>;
hideEntityDetails: HideEntityDetails;
catalogEntityRegistry: CatalogEntityRegistry;
}
const NonInjectedCatalogEntityDetailsComponent = observer(({
selectedCatalogEntity,
hideEntityDetails,
catalogEntityRegistry,
}: Dependencies) => {
const entity = selectedCatalogEntity.get();
if (!entity) {
return null;
}
return (
<CatalogEntityDetails
entity={entity}
hideDetails={hideEntityDetails}
onRun={() => catalogEntityRegistry.onRun(entity)}
/>
);
});
const CatalogEntityDetailsComponent = withInjectables<Dependencies>(NonInjectedCatalogEntityDetailsComponent, {
getProps: (di, props) => ({
...props,
selectedCatalogEntity: di.inject(selectedCatalogEntityInjectable),
catalogEntityRegistry: di.inject(catalogEntityRegistryInjectable),
hideEntityDetails: di.inject(hideEntityDetailsInjectable),
}),
});
const catalogEntityDetailsComponentInjectable = getInjectable({
id: "catalog-entity-details-component",
instantiate: () => ({
id: "catalog-entity-details-component",
Component: CatalogEntityDetailsComponent,
shouldRender: computed(() => true),
}),
injectionToken: rootFrameChildComponentInjectionToken,
});
export default catalogEntityDetailsComponentInjectable;

View File

@ -0,0 +1,20 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import selectedCatalogEntityParamInjectable from "./selected-uid.injectable";
export type HideEntityDetails = () => void;
const hideEntityDetailsInjectable = getInjectable({
id: "hide-entity-details",
instantiate: (di) => {
const selectedCatalogEntityParam = di.inject(selectedCatalogEntityParamInjectable);
return () => selectedCatalogEntityParam.clear();
},
});
export default hideEntityDetailsInjectable;

View File

@ -0,0 +1,31 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { action } from "mobx";
import type { CatalogEntity } from "../../../api/catalog-entity";
import catalogEntityRegistryInjectable from "../../../api/catalog/entity/registry.injectable";
import selectedCatalogEntityParamInjectable from "./selected-uid.injectable";
export type OnCatalogEntityListClick = (entity: CatalogEntity) => void;
const onCatalogEntityListClickInjectable = getInjectable({
id: "on-catalog-entity-list-click",
instantiate: (di): OnCatalogEntityListClick => {
const selectedCatalogEntityParam = di.inject(selectedCatalogEntityParamInjectable);
const catalogEntityRegistry = di.inject(catalogEntityRegistryInjectable);
return action(entity => {
if (selectedCatalogEntityParam.get() === entity.getId()) {
selectedCatalogEntityParam.clear();
} else if (selectedCatalogEntityParam.get() === undefined) {
catalogEntityRegistry.onRun(entity);
} else {
selectedCatalogEntityParam.set(entity.getId());
}
});
},
});
export default onCatalogEntityListClickInjectable;

View File

@ -0,0 +1,28 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { computed } from "mobx";
import getEntityByIdInjectable from "../../../api/catalog/entity/get-by-id.injectable";
import selectedCatalogEntityParamInjectable from "./selected-uid.injectable";
const selectedCatalogEntityInjectable = getInjectable({
id: "selected-catalog-entity",
instantiate: (di) => {
const getEntityById = di.inject(getEntityByIdInjectable);
const selectedCatalogEntityParam = di.inject(selectedCatalogEntityParamInjectable);
return computed(() => {
const id = selectedCatalogEntityParam.get();
if (!id) {
return undefined;
}
return getEntityById(id);
});
},
});
export default selectedCatalogEntityInjectable;

View File

@ -0,0 +1,19 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import createPageParamInjectable from "../../../navigation/create-page-param.injectable";
const selectedCatalogEntityParamInjectable = getInjectable({
id: "selected-catalog-entity-param",
instantiate: (di) => {
const createPageParam = di.inject(createPageParamInjectable);
return createPageParam({
name: "catalog-entity-details",
});
},
});
export default selectedCatalogEntityParamInjectable;

View File

@ -0,0 +1,19 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import selectedCatalogEntityParamInjectable from "./selected-uid.injectable";
export type ShowEntityDetails = (id: string) => void;
const showEntityDetailsInjectable = getInjectable({
id: "show-entity-details",
instantiate: (di): ShowEntityDetails => {
const selectedCatalogEntityParam = di.inject(selectedCatalogEntityParamInjectable);
return (id) => selectedCatalogEntityParam.set(id);
},
});
export default showEntityDetailsInjectable;