diff --git a/__mocks__/react-beautiful-dnd.tsx b/__mocks__/react-beautiful-dnd.tsx index 6404bfd450..79eec81f6e 100644 --- a/__mocks__/react-beautiful-dnd.tsx +++ b/__mocks__/react-beautiful-dnd.tsx @@ -11,5 +11,5 @@ import type { } from "react-beautiful-dnd"; export const DragDropContext = ({ children }: DragDropContextProps) => <>{ children }; -export const Draggable = ({ children }: DraggableProps) => <>{ children }; -export const Droppable = ({ children }: DroppableProps) => <>{ children }; +export const Draggable = ({ children }: DraggableProps) => <>{ children({} as any, {} as any, {} as any) }; +export const Droppable = ({ children }: DroppableProps) => <>{ children({} as any, {} as any) }; diff --git a/src/common/catalog-entities/web-link.ts b/src/common/catalog-entities/web-link.ts index dade63af16..b79f798566 100644 --- a/src/common/catalog-entities/web-link.ts +++ b/src/common/catalog-entities/web-link.ts @@ -3,11 +3,11 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { getLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; +import { Environments, getEnvironmentSpecificLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; import type { CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog"; import { CatalogCategory, CatalogEntity, categoryVersion } from "../catalog/catalog-entity"; import productNameInjectable from "../vars/product-name.injectable"; -import { WeblinkStore } from "../weblink-store"; +import weblinkStoreInjectable from "../weblink-store.injectable"; export type WebLinkStatusPhase = "available" | "unavailable"; @@ -31,14 +31,15 @@ export class WebLink extends CatalogEntity WeblinkStore.getInstance().removeById(this.getId()), + onClick: async () => di.inject(weblinkStoreInjectable).removeById(this.getId()), confirm: { message: `Remove Web Link "${this.getName()}" from ${productName}?`, }, diff --git a/src/extensions/extension-loader/extension-loader.ts b/src/extensions/extension-loader/extension-loader.ts index 23840b67c4..ddfaac8b91 100644 --- a/src/extensions/extension-loader/extension-loader.ts +++ b/src/extensions/extension-loader/extension-loader.ts @@ -12,8 +12,6 @@ import type { Disposer } from "../../common/utils"; import { isDefined, toJS } from "../../common/utils"; import type { InstalledExtension } from "../extension-discovery/extension-discovery"; import type { LensExtension, LensExtensionConstructor, LensExtensionId } from "../lens-extension"; -import type { LensRendererExtension } from "../lens-renderer-extension"; -import * as registries from "../registries"; import type { LensExtensionState } from "../extensions-store/extensions-store"; import { extensionLoaderFromMainChannel, extensionLoaderFromRendererChannel } from "../../common/ipc/extension-handling"; import { requestExtensionLoaderInitialState } from "../../renderer/ipc"; @@ -252,28 +250,13 @@ export class ExtensionLoader { } loadOnMain() { - this.autoInitExtensions(() => Promise.resolve([])); + this.autoInitExtensions(async () => []); } loadOnClusterManagerRenderer = () => { this.dependencies.logger.debug(`${logModule}: load on main renderer (cluster manager)`); - return this.autoInitExtensions(async (ext) => { - const extension = ext as LensRendererExtension; - const removeItems = [ - registries.CatalogEntityDetailRegistry.getInstance().add(extension.catalogEntityDetailItems), - ]; - - this.onRemoveExtensionId.addListener((removedExtensionId) => { - if (removedExtensionId === extension.id) { - removeItems.forEach(remove => { - remove(); - }); - } - }); - - return removeItems; - }); + return this.autoInitExtensions(async () => []); }; loadOnClusterRenderer = () => { diff --git a/src/extensions/registries/catalog-entity-detail-registry.ts b/src/extensions/registries/catalog-entity-detail-registry.ts deleted file mode 100644 index 9c2aeb348c..0000000000 --- a/src/extensions/registries/catalog-entity-detail-registry.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type React from "react"; -import type { Disposer } from "../../common/utils"; -import type { CatalogEntity } from "../common-api/catalog"; -import { BaseRegistry } from "./base-registry"; - -export interface CatalogEntityDetailsProps { - entity: T; -} - -export interface CatalogEntityDetailComponents { - Details: React.ComponentType>; -} - -export interface CatalogEntityDetailRegistration { - kind: string; - apiVersions: string[]; - components: CatalogEntityDetailComponents; - priority?: number; -} - -export class CatalogEntityDetailRegistry extends BaseRegistry> { - add(items: CatalogEntityDetailRegistration | CatalogEntityDetailRegistration[]): Disposer { - return super.add(items as never); - } - - getItemsForKind(kind: string, apiVersion: string) { - const items = this.getItems().filter((item) => { - return item.kind === kind && item.apiVersions.includes(apiVersion); - }); - - return items.sort((a, b) => (b.priority ?? 50) - (a.priority ?? 50)); - } -} diff --git a/src/extensions/registries/index.ts b/src/extensions/registries/index.ts index 2841b4c031..02251f2fd7 100644 --- a/src/extensions/registries/index.ts +++ b/src/extensions/registries/index.ts @@ -7,5 +7,4 @@ export * from "./page-registry"; export * from "./page-menu-registry"; -export * from "./catalog-entity-detail-registry"; export * from "./protocol-handler"; diff --git a/src/features/catalog/__snapshots__/opening-entity-details.test.tsx.snap b/src/features/catalog/__snapshots__/opening-entity-details.test.tsx.snap new file mode 100644 index 0000000000..56c4a912e4 --- /dev/null +++ b/src/features/catalog/__snapshots__/opening-entity-details.test.tsx.snap @@ -0,0 +1,6511 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`opening catalog entity details panel renders 1`] = ` + +
+
+
+
+
+ + + home + + +
+
+
+ + + arrow_back + + +
+
+
+ + + arrow_forward + + +
+
+
+
+
+
+
+
+ +
+
+

+ Welcome to some-product-name! +

+

+ 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. +
+
+ If you have any questions or feedback, please join our + + Lens Community slack channel + + . +

+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + arrow_left + + +
+
+ 0 +
+
+ + + arrow_right + + +
+
+
+
+
+
+
+
+
+ +`; + +exports[`opening catalog entity details panel when navigated to the catalog renders 1`] = ` + +
+
+
+
+
+ + + home + + +
+
+
+ + + arrow_back + + +
+
+
+ + + arrow_forward + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + arrow_left + + +
+
+ 0 +
+
+ + + arrow_right + + +
+
+
+
+
+
+
+
+
+ +`; + +exports[`opening catalog entity details panel when navigated to the catalog when opening the menu 'some-kubernetes-cluster' renders 1`] = ` + +
+
+
+
+
+ + + home + + +
+
+
+ + + arrow_back + + +
+
+
+ + + arrow_forward + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + arrow_left + + +
+
+ 0 +
+
+ + + arrow_right + + +
+
+
+
+
+
+
+
+
+ + +`; + +exports[`opening catalog entity details panel when navigated to the catalog when opening the menu 'some-kubernetes-cluster' when clicking the 'View Details' menu item renders 1`] = ` + +
+
+
+
+
+ + + home + + +
+
+
+ + + arrow_back + + +
+
+
+ + + arrow_forward + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + arrow_left + + +
+
+ 0 +
+
+ + + arrow_right + + +
+
+
+
+
+
+
+
+
+ + +`; + +exports[`opening catalog entity details panel when navigated to the catalog when opening the menu 'some-kubernetes-cluster' when clicking the 'View Details' menu item when the panel opens renders 1`] = ` + +
+
+
+
+
+ + + home + + +
+
+
+ + + arrow_back + + +
+
+
+ + + arrow_forward + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + arrow_left + + +
+
+ 0 +
+
+ + + arrow_right + + +
+
+
+
+
+
+
+
+
+ +
+
+
+
+ KubernetesCluster: some-kubernetes-cluster + + + content_copy + + +
+ Copy +
+
+ + + + close + + +
+ Close +
+
+
+
+
+
+ skc +
+
+ Click to open +
+
+ +
+
+
+ Kubernetes Information +
+ +
+
+
+
+
+ +`; + +exports[`opening catalog entity details panel when navigated to the catalog when opening the menu 'some-weblink' renders 1`] = ` + +
+
+
+
+
+ + + home + + +
+
+
+ + + arrow_back + + +
+
+
+ + + arrow_forward + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + arrow_left + + +
+
+ 0 +
+
+ + + arrow_right + + +
+
+
+
+
+
+
+
+
+ + +`; + +exports[`opening catalog entity details panel when navigated to the catalog when opening the menu 'some-weblink' when clicking the 'View Details' menu item renders 1`] = ` + +
+
+
+
+
+ + + home + + +
+
+
+ + + arrow_back + + +
+
+
+ + + arrow_forward + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + arrow_left + + +
+
+ 0 +
+
+ + + arrow_right + + +
+
+
+
+
+
+
+
+
+ + +`; + +exports[`opening catalog entity details panel when navigated to the catalog when opening the menu 'some-weblink' when clicking the 'View Details' menu item when the panel opens renders 1`] = ` + +
+
+
+
+
+ + + home + + +
+
+
+ + + arrow_back + + +
+
+
+ + + arrow_forward + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + arrow_left + + +
+
+ 0 +
+
+ + + arrow_right + + +
+
+
+
+
+
+
+
+
+ +
+
+
+
+ WebLink: some-weblink + + + content_copy + + +
+ Copy +
+
+ + + close + + +
+ Close +
+
+
+
+
+
+ sw +
+
+ Click to open +
+
+ +
+
+
+ More Information +
+
+ + URL + + + https://my-websome.com + +
+
+
+
+
+
+ +`; diff --git a/src/features/catalog/opening-entity-details.test.tsx b/src/features/catalog/opening-entity-details.test.tsx new file mode 100644 index 0000000000..2d548697ed --- /dev/null +++ b/src/features/catalog/opening-entity-details.test.tsx @@ -0,0 +1,223 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { DiContainer } from "@ogre-tools/injectable"; +import type { RenderResult } from "@testing-library/react"; +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 catalogEntityRegistryInjectable from "../../renderer/api/catalog/entity/registry.injectable"; +import { type ApplicationBuilder, getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; +import createClusterInjectable from "../../renderer/create-cluster/create-cluster.injectable"; + +describe("opening catalog entity details panel", () => { + let builder: ApplicationBuilder; + let rendered: RenderResult; + let windowDi: DiContainer; + let clusterEntity: KubernetesCluster; + let localClusterEntity: KubernetesCluster; + let otherEntity: WebLink; + let cluster: Cluster; + + beforeEach(async () => { + builder = getApplicationBuilder(); + + builder.afterWindowStart((windowDi) => { + const createCluster = windowDi.inject(createClusterInjectable); + + clusterEntity = new KubernetesCluster({ + metadata: { + labels: {}, + name: "some-kubernetes-cluster", + uid: "some-entity-id", + }, + spec: { + kubeconfigContext: "some-context", + kubeconfigPath: "/some/path/to/kubeconfig", + }, + status: { + phase: "connecting", + }, + }); + localClusterEntity = new KubernetesCluster({ + metadata: { + labels: {}, + name: "some-local-kubernetes-cluster", + uid: "some-entity-id-2", + source: "local", + }, + spec: { + kubeconfigContext: "some-context", + kubeconfigPath: "/some/path/to/local/kubeconfig", + }, + status: { + phase: "connecting", + }, + }); + otherEntity = new WebLink({ + metadata: { + labels: {}, + name: "some-weblink", + uid: "some-weblink-id", + }, + spec: { + url: "https://my-websome.com", + }, + status: { + phase: "available", + }, + }); + cluster = createCluster({ + contextName: clusterEntity.spec.kubeconfigContext, + id: clusterEntity.getId(), + kubeConfigPath: clusterEntity.spec.kubeconfigPath, + }, { + clusterServerUrl: "https://localhost:9999", + }); + + // TODO: remove once ClusterStore can be used without overriding it + windowDi.override(getClusterByIdInjectable, () => (clusterId) => { + if (clusterId === cluster.id) { + return cluster; + } + + return undefined; + }); + + // TODO: replace with proper entity source once syncing entities between main and windows is injectable + const catalogEntityRegistry = windowDi.inject(catalogEntityRegistryInjectable); + + catalogEntityRegistry.updateItems([clusterEntity, otherEntity, localClusterEntity]); + }); + + rendered = await builder.render(); + windowDi = builder.applicationWindow.only.di; + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + it("shouldn't show the details yet", () => { + expect(rendered.queryByTestId("catalog-entity-details-drawer")).not.toBeInTheDocument(); + }); + + describe("when navigated to the catalog", () => { + beforeEach(async () => { + const navigateToCatalog = windowDi.inject(navigateToCatalogInjectable); + + navigateToCatalog(); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + it("should show the 'Browse All' view", () => { + expect(rendered.queryByTestId("catalog-list-for-browse-all")).toBeInTheDocument(); + }); + + it("shouldn't show the details yet", () => { + expect(rendered.queryByTestId("catalog-entity-details-drawer")).not.toBeInTheDocument(); + }); + + describe("when opening the menu 'some-kubernetes-cluster'", () => { + beforeEach(() => { + rendered.getByTestId("icon-for-menu-actions-for-catalog-for-some-entity-id").click(); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + it("opens the menu", () => { + expect(rendered.queryByTestId("menu-actions-for-catalog-for-some-entity-id")).toBeInTheDocument(); + }); + + it("shouldn't show the details yet", () => { + expect(rendered.queryByTestId("catalog-entity-details-drawer")).not.toBeInTheDocument(); + }); + + describe("when clicking the 'View Details' menu item", () => { + beforeEach(() => { + rendered.getByTestId("open-details-menu-item-for-some-entity-id").click(); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + describe("when the panel opens", () => { + beforeEach(async () => { + await rendered.findAllByTestId("catalog-entity-details-drawer"); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + it("opens the detail panel for the correct item", () => { + expect(rendered.queryByTestId("catalog-entity-details-content-for-some-entity-id")).toBeInTheDocument(); + }); + + it("shows the registered items", () => { + expect(rendered.queryByTestId("kubernetes-distro-for-some-entity-id")).toBeInTheDocument(); + }); + }); + }); + }); + + describe("when opening the menu 'some-weblink'", () => { + beforeEach(() => { + rendered.getByTestId("icon-for-menu-actions-for-catalog-for-some-weblink-id").click(); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + it("opens the menu", () => { + expect(rendered.queryByTestId("menu-actions-for-catalog-for-some-weblink-id")).toBeInTheDocument(); + }); + + it("shouldn't show the details yet", () => { + expect(rendered.queryByTestId("catalog-entity-details-drawer")).not.toBeInTheDocument(); + }); + + describe("when clicking the 'View Details' menu item", () => { + beforeEach(() => { + rendered.getByTestId("open-details-menu-item-for-some-weblink-id").click(); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + describe("when the panel opens", () => { + beforeEach(async () => { + await rendered.findAllByTestId("catalog-entity-details-drawer"); + }); + + it("renders", () => { + 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(); + }); + + it("shows the registered items", () => { + expect(rendered.queryByTestId("weblink-url-for-some-weblink-id")).toBeInTheDocument(); + }); + + it("should not show registered items for different kinds", () => { + expect(rendered.queryByTestId("kubernetes-distro-for-some-weblink-id")).not.toBeInTheDocument(); + }); + }); + }); + }); + }); +}); diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx index 6cfb6dfb60..ea3899b566 100644 --- a/src/renderer/bootstrap.tsx +++ b/src/renderer/bootstrap.tsx @@ -94,12 +94,6 @@ export async function bootstrap(di: DiContainer) { await attachChromeDebugger(); rootElem.classList.toggle("is-mac", isMac); - logger.info(`${logPrefix} initializing Registries`); - initializers.initRegistries(); - - logger.info(`${logPrefix} initializing CatalogEntityDetailRegistry`); - initializers.initCatalogEntityDetailRegistry(); - logger.info(`${logPrefix} initializing CatalogCategoryRegistryEntries`); initializers.initCatalogCategoryRegistryEntries({ navigateToAddCluster: di.inject(navigateToAddClusterInjectable), diff --git a/src/renderer/components/+catalog/catalog-entity-details.tsx b/src/renderer/components/+catalog/catalog-entity-details.tsx deleted file mode 100644 index fb70b06062..0000000000 --- a/src/renderer/components/+catalog/catalog-entity-details.tsx +++ /dev/null @@ -1,124 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import styles from "./catalog-entity-details.module.scss"; -import React, { Component } from "react"; -import { observer } from "mobx-react"; -import { Drawer, DrawerItem } from "../drawer"; -import type { CatalogCategory, CatalogEntity } from "../../../common/catalog"; -import { Icon } from "../icon"; -import { CatalogEntityDrawerMenu } from "./catalog-entity-drawer-menu"; -import { CatalogEntityDetailRegistry } from "../../../extensions/registries"; -import { isDevelopment } from "../../../common/vars"; -import { cssNames } from "../../utils"; -import { Avatar } from "../avatar"; -import type { GetLabelBadges } from "./get-label-badges.injectable"; -import { withInjectables } from "@ogre-tools/injectable-react"; -import getLabelBadgesInjectable from "./get-label-badges.injectable"; - -export interface CatalogEntityDetailsProps { - entity: Entity; - hideDetails(): void; - onRun: () => void; -} - -interface Dependencies { - getLabelBadges: GetLabelBadges; -} - -@observer -class NonInjectedCatalogEntityDetails extends Component & Dependencies> { - categoryIcon(category: CatalogCategory) { - if (Icon.isSvg(category.metadata.icon)) { - return ; - } else { - return ; - } - } - - renderContent(entity: Entity) { - const { onRun, hideDetails, getLabelBadges } = this.props; - const detailItems = CatalogEntityDetailRegistry.getInstance().getItemsForKind(entity.kind, entity.apiVersion); - const details = detailItems.map(({ components }, index) => ); - const showDefaultDetails = detailItems.find((item) => item.priority ?? 50 > 999) === undefined; - - return ( - <> - {showDefaultDetails && ( -
-
- - {entity.spec.icon?.material && } - - {entity.isEnabled() && ( -
- Click to open -
- )} -
-
- - {entity.getName()} - - - {entity.kind} - - - {entity.getSource()} - - - {entity.status.phase} - - - {getLabelBadges(entity, hideDetails)} - - {isDevelopment && ( - - {entity.getId()} - - )} -
-
- )} -
- {details} -
- - ); - } - - render() { - const { entity, hideDetails } = this.props; - - return ( - } - onClose={hideDetails} - > - {this.renderContent(entity)} - - ); - } -} - -export const CatalogEntityDetails = withInjectables>(NonInjectedCatalogEntityDetails, { - getProps: (di, props) => ({ - ...props, - getLabelBadges: di.inject(getLabelBadgesInjectable), - }), -}) as (props: CatalogEntityDetailsProps) => React.ReactElement; diff --git a/src/renderer/components/+catalog/catalog.test.tsx b/src/renderer/components/+catalog/catalog.test.tsx index 6eb6c1ad57..5c6e23ce4d 100644 --- a/src/renderer/components/+catalog/catalog.test.tsx +++ b/src/renderer/components/+catalog/catalog.test.tsx @@ -10,7 +10,6 @@ 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 { CatalogEntityDetailRegistry } from "../../../extensions/registries"; import type { CatalogEntityStore } from "./catalog-entity-store/catalog-entity.store"; import { getDiForUnitTesting } from "../../getDiForUnitTesting"; import type { DiContainer } from "@ogre-tools/injectable"; @@ -71,8 +70,6 @@ describe("", () => { di.permitSideEffects(getConfigurationFileModelInjectable); - CatalogEntityDetailRegistry.createInstance(); - render = renderFor(di); onRun = jest.fn(); catalogEntityItem = createMockCatalogEntity(onRun); @@ -87,10 +84,6 @@ describe("", () => { }); }); - afterEach(() => { - CatalogEntityDetailRegistry.resetInstance(); - }); - describe("can use catalogEntityRegistry.addOnBeforeRun to add hooks for catalog entities", () => { let onBeforeRunMock: AsyncFnMock; diff --git a/src/renderer/components/+catalog/catalog.tsx b/src/renderer/components/+catalog/catalog.tsx index 2dc7a589fb..acd5346b17 100644 --- a/src/renderer/components/+catalog/catalog.tsx +++ b/src/renderer/components/+catalog/catalog.tsx @@ -15,11 +15,11 @@ import { MenuItem, MenuActions } from "../menu"; import type { CatalogEntityContextMenu } from "../../api/catalog-entity"; import type { CatalogCategory, CatalogCategoryRegistry, CatalogEntity } from "../../../common/catalog"; import { CatalogAddButton } from "./catalog-add-button"; -import { Notifications } from "../notifications"; +import type { ShowNotification } from "../notifications"; import { MainLayout } from "../layout/main-layout"; import type { StorageLayer } from "../../utils"; import { prevDefault } from "../../utils"; -import { CatalogEntityDetails } from "./catalog-entity-details"; +import { CatalogEntityDetails } from "./entity-details/view"; import { CatalogMenu } from "./catalog-menu"; import { RenderDelay } from "../render-delay/render-delay"; import { Icon } from "../icon"; @@ -48,6 +48,9 @@ import type { NormalizeCatalogEntityContextMenu } from "../../catalog/normalize- import normalizeCatalogEntityContextMenuInjectable from "../../catalog/normalize-menu-item.injectable"; import type { EmitAppEvent } from "../../../common/app-event-bus/emit-event.injectable"; import emitAppEventInjectable from "../../../common/app-event-bus/emit-event.injectable"; +import type { Logger } from "../../../common/logger"; +import loggerInjectable from "../../../common/logger.injectable"; +import showErrorNotificationInjectable from "../notifications/show-error-notification.injectable"; interface Dependencies { catalogPreviousActiveTabStorage: StorageLayer; @@ -65,12 +68,14 @@ interface Dependencies { visitEntityContextMenu: VisitEntityContextMenu; navigate: Navigate; normalizeMenuItem: NormalizeCatalogEntityContextMenu; + showErrorNotification: ShowNotification; + logger: Logger; } @observer class NonInjectedCatalog extends React.Component { private readonly menuItems = observable.array(); - @observable activeTab?: string; + @observable activeTab: string | undefined = undefined; constructor(props: Dependencies) { super(props); @@ -79,7 +84,7 @@ class NonInjectedCatalog extends React.Component { @computed get routeActiveTab(): string { - const { group, kind } = this.props.routeParameters; + const { routeParameters: { group, kind }, catalogPreviousActiveTabStorage } = this.props; const dereferencedGroup = group.get(); const dereferencedKind = kind.get(); @@ -88,13 +93,7 @@ class NonInjectedCatalog extends React.Component { return `${dereferencedGroup}/${dereferencedKind}`; } - const previousTab = this.props.catalogPreviousActiveTabStorage.get(); - - if (previousTab) { - return previousTab; - } - - return browseCatalogTab; + return catalogPreviousActiveTabStorage.get() || browseCatalogTab; } async componentDidMount() { @@ -102,6 +101,8 @@ class NonInjectedCatalog extends React.Component { catalogEntityStore, catalogPreviousActiveTabStorage, catalogCategoryRegistry, + logger, + showErrorNotification, } = this.props; disposeOnUnmount(this, [ @@ -110,7 +111,11 @@ class NonInjectedCatalog extends React.Component { catalogPreviousActiveTabStorage.set(this.routeActiveTab); try { - await when(() => (routeTab === browseCatalogTab || !!catalogCategoryRegistry.filteredItems.find(i => i.getId() === routeTab)), { timeout: 5_000 }); // we need to wait because extensions might take a while to load + if (routeTab !== browseCatalogTab) { + // we need to wait because extensions might take a while to load + await when(() => Boolean(catalogCategoryRegistry.filteredItems.find(i => i.getId() === routeTab)), { timeout: 5_000 }); + } + const item = catalogCategoryRegistry.filteredItems.find(i => i.getId() === routeTab); runInAction(() => { @@ -118,8 +123,8 @@ class NonInjectedCatalog extends React.Component { catalogEntityStore.activeCategory.set(item); }); } catch (error) { - console.error(error); - Notifications.error(( + logger.warn("Failed to find route tab", error); + showErrorNotification((

{"Unknown category: "} {routeTab} @@ -198,9 +203,14 @@ class NonInjectedCatalog extends React.Component { }; return ( - + this.props.catalogEntityStore.selectedItemId.set(entity.getId())} > View Details @@ -253,7 +263,7 @@ class NonInjectedCatalog extends React.Component { renderViews = (activeCategory: CatalogCategory | undefined) => { if (!activeCategory) { - return this.renderList(activeCategory); + return this.renderList(undefined); } const customViews = this.props.customCategoryViews.get() @@ -301,15 +311,12 @@ class NonInjectedCatalog extends React.Component { {...getCategoryColumns({ activeCategory })} onDetails={this.onDetails} renderItemMenu={this.renderItemMenu} + data-testid={`catalog-list-for-${activeCategory?.metadata.name ?? "browse-all"}`} /> ); } render() { - if (!this.props.catalogEntityStore) { - return null; - } - const activeCategory = this.props.catalogEntityStore.activeCategory.get(); const selectedItem = this.props.catalogEntityStore.selectedItem.get(); @@ -362,5 +369,7 @@ export const Catalog = withInjectables(NonInjectedCatalog, { visitEntityContextMenu: di.inject(visitEntityContextMenuInjectable), navigate: di.inject(navigateInjectable), normalizeMenuItem: di.inject(normalizeCatalogEntityContextMenuInjectable), + logger: di.inject(loggerInjectable), + showErrorNotification: di.inject(showErrorNotificationInjectable), }), }); diff --git a/src/renderer/components/+catalog/entity-details/detail-items.injectable.ts b/src/renderer/components/+catalog/entity-details/detail-items.injectable.ts new file mode 100644 index 0000000000..7ef414d988 --- /dev/null +++ b/src/renderer/components/+catalog/entity-details/detail-items.injectable.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-for-mobx"; +import { computed } from "mobx"; +import { byOrderNumber } from "../../../../common/utils/composable-responsibilities/orderable/orderable"; +import type { CatalogEntity } from "../../../api/catalog-entity"; +import { catalogEntityDetailItemInjectionToken } from "./token"; + +const catalogEntityDetailItemsInjectable = getInjectable({ + id: "catalog-entity-detail-items", + instantiate: (di, entity) => { + const computedInjectMany = di.inject(computedInjectManyInjectable); + const detailItems = computedInjectMany(catalogEntityDetailItemInjectionToken); + + return computed(() => ( + detailItems.get() + .filter(item => ( + item.apiVersions.has(entity.apiVersion) + && item.kind === entity.kind + )) + .sort(byOrderNumber) + .map(item => item.components.Details) + )); + }, + lifecycle: lifecycleEnum.keyedSingleton({ + getInstanceKey: (di, entity: CatalogEntity) => `${entity.apiVersion}/${entity.kind}`, + }), +}); + +export default catalogEntityDetailItemsInjectable; diff --git a/src/renderer/components/+catalog/entity-details/internal/kubernetes-cluster-details.injectable.tsx b/src/renderer/components/+catalog/entity-details/internal/kubernetes-cluster-details.injectable.tsx new file mode 100644 index 0000000000..89318bac15 --- /dev/null +++ b/src/renderer/components/+catalog/entity-details/internal/kubernetes-cluster-details.injectable.tsx @@ -0,0 +1,39 @@ +/** + * 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 React from "react"; +import { KubernetesCluster } from "../../../../../common/catalog-entities"; +import { DrawerTitle, DrawerItem } from "../../../drawer"; +import { catalogEntityDetailItemInjectionToken } from "../token"; + +const kubernetesClusterDetailsItemInjectable = getInjectable({ + id: "kubernetes-cluster-details-item", + instantiate: () => ({ + apiVersions: new Set([KubernetesCluster.apiVersion]), + kind: KubernetesCluster.kind, + orderNumber: 40, + components: { + Details: ({ entity }) => ( + <> + Kubernetes Information +

+ + {entity.metadata.distro || "unknown"} + + + {entity.metadata.kubeVersion || "unknown"} + +
+ + ), + }, + }), + injectionToken: catalogEntityDetailItemInjectionToken, +}); + +export default kubernetesClusterDetailsItemInjectable; diff --git a/src/renderer/components/+catalog/entity-details/internal/weblink-details.injectable.tsx b/src/renderer/components/+catalog/entity-details/internal/weblink-details.injectable.tsx new file mode 100644 index 0000000000..9ba12de575 --- /dev/null +++ b/src/renderer/components/+catalog/entity-details/internal/weblink-details.injectable.tsx @@ -0,0 +1,34 @@ +/** + * 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 React from "react"; +import { WebLink } from "../../../../../common/catalog-entities"; +import { DrawerTitle, DrawerItem } from "../../../drawer"; +import { catalogEntityDetailItemInjectionToken } from "../token"; + +const weblinkDetailsItemInjectable = getInjectable({ + id: "weblink-details-item", + instantiate: () => ({ + apiVersions: new Set([WebLink.apiVersion]), + kind: WebLink.kind, + components: { + Details: ({ entity }) => ( + <> + More Information + + {entity.spec.url} + + + ), + }, + orderNumber: 40, + }), + injectionToken: catalogEntityDetailItemInjectionToken, +}); + +export default weblinkDetailsItemInjectable; diff --git a/src/renderer/components/+catalog/entity-details/registrator.injectable.ts b/src/renderer/components/+catalog/entity-details/registrator.injectable.ts new file mode 100644 index 0000000000..02a6a7ed23 --- /dev/null +++ b/src/renderer/components/+catalog/entity-details/registrator.injectable.ts @@ -0,0 +1,39 @@ +/** + * 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 { extensionRegistratorInjectionToken } from "../../../../extensions/extension-loader/extension-registrator-injection-token"; +import type { LensRendererExtension } from "../../../../extensions/lens-renderer-extension"; +import type { CatalogEntity } from "../../../api/catalog-entity"; +import { getRandId } from "../../../utils"; +import type { CatalogEntityDetailRegistration } from "./token"; +import { catalogEntityDetailItemInjectionToken } from "./token"; + +const catalogEntityDetailItemsRegistratorInjectable = getInjectable({ + id: "catalog-entity-detail-items-registrator", + instantiate: () => (ext) => { + const extension = ext as LensRendererExtension; + + return extension.catalogEntityDetailItems.map(getRegistratorFor(extension)); + }, + injectionToken: extensionRegistratorInjectionToken, +}); + +export default catalogEntityDetailItemsRegistratorInjectable; + +const getRegistratorFor = (extension: LensRendererExtension) => ({ + apiVersions, + components, + kind, + priority, +}: CatalogEntityDetailRegistration) => getInjectable({ + id: `catalog-entity-detail-item-for-${extension.sanitizedExtensionId}-${getRandId({ sep: "-" })}`, + instantiate: () => ({ + apiVersions: new Set(apiVersions), + components, + kind, + orderNumber: priority ?? 50, + }), + injectionToken: catalogEntityDetailItemInjectionToken, +}); diff --git a/src/renderer/components/+catalog/entity-details/token.ts b/src/renderer/components/+catalog/entity-details/token.ts new file mode 100644 index 0000000000..55d05cef2d --- /dev/null +++ b/src/renderer/components/+catalog/entity-details/token.ts @@ -0,0 +1,35 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getInjectionToken } from "@ogre-tools/injectable"; +import type { CatalogEntity } from "../../../api/catalog-entity"; + +export interface CatalogEntityDetailsProps { + entity: T; +} + +export type CatalogEntityDetailsComponent = React.ComponentType>; + +export interface CatalogEntityDetailComponents { + Details: CatalogEntityDetailsComponent; +} + +export interface CatalogEntityDetailRegistration { + kind: string; + apiVersions: string[]; + components: CatalogEntityDetailComponents; + priority?: number; +} + +export interface CatalogEntityDetailItem { + kind: string; + apiVersions: Set; + components: CatalogEntityDetailComponents; + orderNumber: number; +} + +export const catalogEntityDetailItemInjectionToken = getInjectionToken({ + id: "catalog-entity-detail-item-token", +}); diff --git a/src/renderer/components/+catalog/catalog-entity-details.module.scss b/src/renderer/components/+catalog/entity-details/view.module.scss similarity index 100% rename from src/renderer/components/+catalog/catalog-entity-details.module.scss rename to src/renderer/components/+catalog/entity-details/view.module.scss diff --git a/src/renderer/components/+catalog/entity-details/view.tsx b/src/renderer/components/+catalog/entity-details/view.tsx new file mode 100644 index 0000000000..6b9b92247d --- /dev/null +++ b/src/renderer/components/+catalog/entity-details/view.tsx @@ -0,0 +1,127 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import styles from "./view.module.scss"; +import React, { Component } from "react"; +import { observer } from "mobx-react"; +import { Drawer, DrawerItem } from "../../drawer"; +import type { CatalogCategory, CatalogEntity } from "../../../../common/catalog"; +import { Icon } from "../../icon"; +import { CatalogEntityDrawerMenu } from "../catalog-entity-drawer-menu"; +import { cssNames } from "../../../utils"; +import { Avatar } from "../../avatar"; +import type { GetLabelBadges } from "../get-label-badges.injectable"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import getLabelBadgesInjectable from "../get-label-badges.injectable"; +import isDevelopmentInjectable from "../../../../common/vars/is-development.injectable"; +import type { IComputedValue } from "mobx"; +import type { CatalogEntityDetailsComponent } from "./token"; +import catalogEntityDetailItemsInjectable from "./detail-items.injectable"; + +export interface CatalogEntityDetailsProps { + entity: Entity; + hideDetails(): void; + onRun: () => void; +} + +interface Dependencies { + getLabelBadges: GetLabelBadges; + isDevelopment: boolean; + detailItems: IComputedValue[]>; +} + +@observer +class NonInjectedCatalogEntityDetails extends Component & Dependencies> { + categoryIcon(category: CatalogCategory) { + if (Icon.isSvg(category.metadata.icon)) { + return ; + } else { + return ; + } + } + + renderContent(entity: Entity) { + const { onRun, hideDetails, getLabelBadges, isDevelopment, detailItems } = this.props; + const details = detailItems.get().map((Details, index) =>
); + + return ( + <> +
+
+ + {entity.spec.icon?.material && } + + {entity.isEnabled() && ( +
+ Click to open +
+ )} +
+
+ + {entity.getName()} + + + {entity.kind} + + + {entity.getSource()} + + + {entity.status.phase} + + + {getLabelBadges(entity, hideDetails)} + + {isDevelopment && ( + + {entity.getId()} + + )} +
+
+
+ {details} +
+ + ); + } + + render() { + const { entity, hideDetails } = this.props; + + return ( + } + onClose={hideDetails} + data-testid="catalog-entity-details-drawer" + > + {this.renderContent(entity)} + + ); + } +} + +export const CatalogEntityDetails = withInjectables>(NonInjectedCatalogEntityDetails, { + getProps: (di, props) => ({ + ...props, + getLabelBadges: di.inject(getLabelBadgesInjectable), + isDevelopment: di.inject(isDevelopmentInjectable), + detailItems: di.inject(catalogEntityDetailItemsInjectable, props.entity), + }), +}) as (props: CatalogEntityDetailsProps) => React.ReactElement; diff --git a/src/renderer/components/icon/icon.tsx b/src/renderer/components/icon/icon.tsx index 425b3235b0..b51299ba15 100644 --- a/src/renderer/components/icon/icon.tsx +++ b/src/renderer/components/icon/icon.tsx @@ -153,6 +153,7 @@ export interface BaseIconProps { focusable?: boolean; sticker?: boolean; disabled?: boolean; + "data-testid"?: string; } export interface IconProps extends React.HTMLAttributes, BaseIconProps {} diff --git a/src/renderer/components/item-object-list/list-layout.tsx b/src/renderer/components/item-object-list/list-layout.tsx index 63fbc651f8..2d3a9ae17b 100644 --- a/src/renderer/components/item-object-list/list-layout.tsx +++ b/src/renderer/components/item-object-list/list-layout.tsx @@ -131,6 +131,7 @@ export type ItemListLayoutProps; + "data-testid"?: string; } & ( PreLoadStores extends true ? { @@ -271,11 +272,12 @@ class NonInjectedItemListLayout (
this.items} diff --git a/src/renderer/components/menu/menu-actions.tsx b/src/renderer/components/menu/menu-actions.tsx index 239ca46dd2..0e77446caf 100644 --- a/src/renderer/components/menu/menu-actions.tsx +++ b/src/renderer/components/menu/menu-actions.tsx @@ -91,15 +91,22 @@ class NonInjectedMenuActions extends React.Component(triggerIcon)) { - className = cssNames(triggerIcon.props.className, { active: this.isOpen }); + const className = cssNames(triggerIcon.props.className, { active: this.isOpen }); return React.cloneElement(triggerIcon, { id: this.props.id, className }); } + const iconProps: IconProps & TooltipDecoratorProps = { id: this.props.id, interactive: true, @@ -108,6 +115,10 @@ class NonInjectedMenuActions extends React.Component { } render() { - const { position, id, animated } = this.props; - let { className, usePortal } = this.props; - - className = cssNames("Menu", className, this.state.position || position, { + const { position, id, animated, "data-testid": dataTestId, usePortal, className } = this.props; + const classNames = cssNames("Menu", className, this.state.position || position, { portal: usePortal, }); + // const menuChildren = let children = this.props.children as ReactElement; @@ -351,12 +351,13 @@ class NonInjectedMenu extends React.Component {
    {menuItems}
@@ -376,11 +377,13 @@ class NonInjectedMenu extends React.Component { ); - if (usePortal === true) usePortal = document.body; + if (!usePortal) { + return menu; + } - return usePortal instanceof HTMLElement - ? createPortal(menu, usePortal) - : menu; + const portal = usePortal === true ? document.body : usePortal; + + return createPortal(menu, portal); } } diff --git a/src/renderer/getDiForUnitTesting.tsx b/src/renderer/getDiForUnitTesting.tsx index 07c7afb007..6dc7fa010e 100644 --- a/src/renderer/getDiForUnitTesting.tsx +++ b/src/renderer/getDiForUnitTesting.tsx @@ -145,13 +145,19 @@ export const getDiForUnitTesting = ( callForPublicHelmRepositoriesInjectable, ]); - // eslint-disable-next-line unused-imports/no-unused-vars-ts - di.override(extensionsStoreInjectable, () => ({ isEnabled: ({ id, isBundled }) => false }) as ExtensionsStore); + di.override(extensionsStoreInjectable, () => ({ + isEnabled: () => false, + }) as Partial as ExtensionsStore); di.override(hotbarStoreInjectable, () => ({ - getActive: () => ({ name: "some-hotbar", items: [] }), + getActive: () => ({ + name: "some-hotbar", + items: [null, null, null, null, null, null, null, null, null, null, null, null], + id: "some-hotbar", + }), getDisplayIndex: () => "0", - }) as unknown as HotbarStore); + isAddedToActive: () => false, + }) as Partial as HotbarStore); di.override(fileSystemProvisionerStoreInjectable, () => ({}) as FileSystemProvisionerStore); diff --git a/src/renderer/initializers/catalog-entity-detail-registry.tsx b/src/renderer/initializers/catalog-entity-detail-registry.tsx deleted file mode 100644 index 7fcbd1b776..0000000000 --- a/src/renderer/initializers/catalog-entity-detail-registry.tsx +++ /dev/null @@ -1,52 +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 { KubernetesCluster, WebLink } from "../../common/catalog-entities"; -import type { CatalogEntityDetailsProps } from "../../extensions/registries"; -import { CatalogEntityDetailRegistry } from "../../extensions/registries"; -import { DrawerItem, DrawerTitle } from "../components/drawer"; - -export function initCatalogEntityDetailRegistry() { - CatalogEntityDetailRegistry.getInstance() - .add([ - { - apiVersions: [KubernetesCluster.apiVersion], - kind: KubernetesCluster.kind, - components: { - Details: ({ entity }: CatalogEntityDetailsProps) => ( - <> - Kubernetes Information -
- - {entity.metadata.distro || "unknown"} - - - {entity.metadata.kubeVersion || "unknown"} - -
- - ), - }, - }, - ]); - CatalogEntityDetailRegistry.getInstance() - .add([ - { - apiVersions: [WebLink.apiVersion], - kind: WebLink.kind, - components: { - Details: ({ entity }: CatalogEntityDetailsProps) => ( - <> - More Information - - {entity.spec.url} - - - ), - }, - }, - ]); -} diff --git a/src/renderer/initializers/index.ts b/src/renderer/initializers/index.ts index 6fb131e507..676b4e8a65 100644 --- a/src/renderer/initializers/index.ts +++ b/src/renderer/initializers/index.ts @@ -3,7 +3,6 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -export * from "./catalog-entity-detail-registry"; export * from "./catalog"; export * from "./ipc"; export * from "./registries"; diff --git a/src/renderer/initializers/registries.ts b/src/renderer/initializers/registries.ts deleted file mode 100644 index 6d9b7a075e..0000000000 --- a/src/renderer/initializers/registries.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import * as registries from "../../extensions/registries"; - -export function initRegistries() { - registries.CatalogEntityDetailRegistry.createInstance(); -}