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 authored and GitHub committed 2023-03-07 10:10:14 -05:00
1 parent f88555a1d7
commit 0084af56d6
20 files changed
+2680 -405

No files matched your search

File diff suppressed because it is too large. Load diff
@@ -1925,10 +1925,10 @@ exports[`opening catalog entity details panel when navigated to the catalog when
/>
</div>
<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"
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
class="MenuItem"
@@ -2778,10 +2778,10 @@ exports[`opening catalog entity details panel when navigated to the catalog when
/>
</div>
<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"
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
class="MenuItem"
@@ -6609,3 +6609,459 @@ exports[`opening catalog entity details panel when navigated to the catalog when
</div>
</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>
`;
@@ -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",
},
});
});
});
});
});
@@ -9,18 +9,20 @@ import { KubernetesCluster, WebLink } from "../../common/catalog-entities";
import getClusterByIdInjectable from "../../common/cluster-store/get-by-id.injectable";
import type { Cluster } from "../../common/cluster/cluster";
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 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";
describe("opening catalog entity details panel", () => {
let builder: ApplicationBuilder;
let rendered: RenderResult;
let windowDi: DiContainer;
let cluster: Cluster;
let clusterEntity: KubernetesCluster;
let localClusterEntity: KubernetesCluster;
let otherEntity: WebLink;
let cluster: Cluster;
beforeEach(async () => {
builder = getApplicationBuilder();
@@ -28,7 +30,7 @@ describe("opening catalog entity details panel", () => {
builder.beforeWindowStart((windowDi) => {
// TODO: remove once ClusterStore can be used without overriding it
windowDi.override(getClusterByIdInjectable, () => (clusterId) => {
if (clusterId === cluster.id) {
if (clusterId === cluster?.id) {
return cluster;
}
@@ -36,6 +38,8 @@ describe("opening catalog entity details panel", () => {
});
});
testUsingFakeTime();
builder.afterWindowStart((windowDi) => {
const createCluster = windowDi.inject(createClusterInjectable);
@@ -129,6 +133,7 @@ describe("opening catalog entity details panel", () => {
describe("when opening the menu 'some-kubernetes-cluster'", () => {
beforeEach(() => {
rendered.getByTestId("icon-for-menu-actions-for-catalog-for-some-entity-id").click();
advanceFakeTime(1000);
});
it("renders", () => {
@@ -154,6 +159,7 @@ describe("opening catalog entity details panel", () => {
describe("when the panel opens", () => {
beforeEach(async () => {
advanceFakeTime(1000);
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();
});
});
});