mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Replace EntityDetailRegistry with an injectable solution
- Add some behavioural tests Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
5c69b273b1
commit
73eaf5a22c
@ -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) }</>;
|
||||
|
||||
@ -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<CatalogEntityMetadata, WebLinkStatus,
|
||||
}
|
||||
|
||||
onContextMenuOpen(context: CatalogEntityContextMenuContext) {
|
||||
const di = getLegacyGlobalDiForExtensionApi();
|
||||
// NOTE: this is safe because `onContextMenuOpen` is only supposed to be called in the renderer
|
||||
const di = getEnvironmentSpecificLegacyGlobalDiForExtensionApi(Environments.renderer);
|
||||
const productName = di.inject(productNameInjectable);
|
||||
|
||||
if (this.metadata.source === "local") {
|
||||
context.menuItems.push({
|
||||
title: "Delete",
|
||||
icon: "delete",
|
||||
onClick: async () => WeblinkStore.getInstance().removeById(this.getId()),
|
||||
onClick: async () => di.inject(weblinkStoreInjectable).removeById(this.getId()),
|
||||
confirm: {
|
||||
message: `Remove Web Link "${this.getName()}" from ${productName}?`,
|
||||
},
|
||||
|
||||
@ -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 = () => {
|
||||
|
||||
@ -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<T extends CatalogEntity> {
|
||||
entity: T;
|
||||
}
|
||||
|
||||
export interface CatalogEntityDetailComponents<T extends CatalogEntity> {
|
||||
Details: React.ComponentType<CatalogEntityDetailsProps<T>>;
|
||||
}
|
||||
|
||||
export interface CatalogEntityDetailRegistration<T extends CatalogEntity> {
|
||||
kind: string;
|
||||
apiVersions: string[];
|
||||
components: CatalogEntityDetailComponents<T>;
|
||||
priority?: number;
|
||||
}
|
||||
|
||||
export class CatalogEntityDetailRegistry extends BaseRegistry<CatalogEntityDetailRegistration<CatalogEntity>> {
|
||||
add<T extends CatalogEntity>(items: CatalogEntityDetailRegistration<T> | CatalogEntityDetailRegistration<T>[]): 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));
|
||||
}
|
||||
}
|
||||
@ -7,5 +7,4 @@
|
||||
|
||||
export * from "./page-registry";
|
||||
export * from "./page-menu-registry";
|
||||
export * from "./catalog-entity-detail-registry";
|
||||
export * from "./protocol-handler";
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
223
src/features/catalog/opening-entity-details.test.tsx
Normal file
223
src/features/catalog/opening-entity-details.test.tsx
Normal file
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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),
|
||||
|
||||
@ -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 extends CatalogEntity> {
|
||||
entity: Entity;
|
||||
hideDetails(): void;
|
||||
onRun: () => void;
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
getLabelBadges: GetLabelBadges;
|
||||
}
|
||||
|
||||
@observer
|
||||
class NonInjectedCatalogEntityDetails<Entity extends CatalogEntity> extends Component<CatalogEntityDetailsProps<Entity> & Dependencies> {
|
||||
categoryIcon(category: CatalogCategory) {
|
||||
if (Icon.isSvg(category.metadata.icon)) {
|
||||
return <Icon svg={category.metadata.icon} smallest />;
|
||||
} else {
|
||||
return <Icon material={category.metadata.icon} smallest />;
|
||||
}
|
||||
}
|
||||
|
||||
renderContent(entity: Entity) {
|
||||
const { onRun, hideDetails, getLabelBadges } = this.props;
|
||||
const detailItems = CatalogEntityDetailRegistry.getInstance().getItemsForKind(entity.kind, entity.apiVersion);
|
||||
const details = detailItems.map(({ components }, index) => <components.Details entity={entity} key={index} />);
|
||||
const showDefaultDetails = detailItems.find((item) => item.priority ?? 50 > 999) === undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
{showDefaultDetails && (
|
||||
<div className="flex">
|
||||
<div className={styles.entityIcon}>
|
||||
<Avatar
|
||||
title={entity.getName()}
|
||||
colorHash={`${entity.getName()}-${entity.getSource()}`}
|
||||
size={128}
|
||||
src={entity.spec.icon?.src}
|
||||
data-testid="detail-panel-hot-bar-icon"
|
||||
background={entity.spec.icon?.background}
|
||||
onClick={onRun}
|
||||
className={styles.avatar}
|
||||
>
|
||||
{entity.spec.icon?.material && <Icon material={entity.spec.icon?.material}/>}
|
||||
</Avatar>
|
||||
{entity.isEnabled() && (
|
||||
<div className={styles.hint}>
|
||||
Click to open
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={cssNames("box grow", styles.metadata)}>
|
||||
<DrawerItem name="Name">
|
||||
{entity.getName()}
|
||||
</DrawerItem>
|
||||
<DrawerItem name="Kind">
|
||||
{entity.kind}
|
||||
</DrawerItem>
|
||||
<DrawerItem name="Source">
|
||||
{entity.getSource()}
|
||||
</DrawerItem>
|
||||
<DrawerItem name="Status">
|
||||
{entity.status.phase}
|
||||
</DrawerItem>
|
||||
<DrawerItem name="Labels">
|
||||
{getLabelBadges(entity, hideDetails)}
|
||||
</DrawerItem>
|
||||
{isDevelopment && (
|
||||
<DrawerItem name="Id">
|
||||
{entity.getId()}
|
||||
</DrawerItem>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="box grow">
|
||||
{details}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { entity, hideDetails } = this.props;
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
className={styles.entityDetails}
|
||||
usePortal={true}
|
||||
open={true}
|
||||
title={`${entity.kind}: ${entity.getName()}`}
|
||||
toolbar={<CatalogEntityDrawerMenu entity={entity} key={entity.getId()} />}
|
||||
onClose={hideDetails}
|
||||
>
|
||||
{this.renderContent(entity)}
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const CatalogEntityDetails = withInjectables<Dependencies, CatalogEntityDetailsProps<CatalogEntity>>(NonInjectedCatalogEntityDetails, {
|
||||
getProps: (di, props) => ({
|
||||
...props,
|
||||
getLabelBadges: di.inject(getLabelBadgesInjectable),
|
||||
}),
|
||||
}) as <Entity extends CatalogEntity>(props: CatalogEntityDetailsProps<Entity>) => React.ReactElement;
|
||||
@ -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("<Catalog />", () => {
|
||||
|
||||
di.permitSideEffects(getConfigurationFileModelInjectable);
|
||||
|
||||
CatalogEntityDetailRegistry.createInstance();
|
||||
|
||||
render = renderFor(di);
|
||||
onRun = jest.fn();
|
||||
catalogEntityItem = createMockCatalogEntity(onRun);
|
||||
@ -87,10 +84,6 @@ describe("<Catalog />", () => {
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
CatalogEntityDetailRegistry.resetInstance();
|
||||
});
|
||||
|
||||
describe("can use catalogEntityRegistry.addOnBeforeRun to add hooks for catalog entities", () => {
|
||||
let onBeforeRunMock: AsyncFnMock<CatalogEntityOnBeforeRun>;
|
||||
|
||||
|
||||
@ -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<string | null>;
|
||||
@ -65,12 +68,14 @@ interface Dependencies {
|
||||
visitEntityContextMenu: VisitEntityContextMenu;
|
||||
navigate: Navigate;
|
||||
normalizeMenuItem: NormalizeCatalogEntityContextMenu;
|
||||
showErrorNotification: ShowNotification;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
@observer
|
||||
class NonInjectedCatalog extends React.Component<Dependencies> {
|
||||
private readonly menuItems = observable.array<CatalogEntityContextMenu>();
|
||||
@observable activeTab?: string;
|
||||
@observable activeTab: string | undefined = undefined;
|
||||
|
||||
constructor(props: Dependencies) {
|
||||
super(props);
|
||||
@ -79,7 +84,7 @@ class NonInjectedCatalog extends React.Component<Dependencies> {
|
||||
|
||||
@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<Dependencies> {
|
||||
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<Dependencies> {
|
||||
catalogEntityStore,
|
||||
catalogPreviousActiveTabStorage,
|
||||
catalogCategoryRegistry,
|
||||
logger,
|
||||
showErrorNotification,
|
||||
} = this.props;
|
||||
|
||||
disposeOnUnmount(this, [
|
||||
@ -110,7 +111,11 @@ class NonInjectedCatalog extends React.Component<Dependencies> {
|
||||
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<Dependencies> {
|
||||
catalogEntityStore.activeCategory.set(item);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
Notifications.error((
|
||||
logger.warn("Failed to find route tab", error);
|
||||
showErrorNotification((
|
||||
<p>
|
||||
{"Unknown category: "}
|
||||
{routeTab}
|
||||
@ -198,9 +203,14 @@ class NonInjectedCatalog extends React.Component<Dependencies> {
|
||||
};
|
||||
|
||||
return (
|
||||
<MenuActions id={`menu-actions-for-catalog-for-${entity.getId()}`} onOpen={onOpen}>
|
||||
<MenuActions
|
||||
id={`menu-actions-for-catalog-for-${entity.getId()}`}
|
||||
data-testid={`menu-actions-for-catalog-for-${entity.getId()}`}
|
||||
onOpen={onOpen}
|
||||
>
|
||||
<MenuItem
|
||||
key="open-details"
|
||||
data-testid={`open-details-menu-item-for-${entity.getId()}`}
|
||||
onClick={() => this.props.catalogEntityStore.selectedItemId.set(entity.getId())}
|
||||
>
|
||||
View Details
|
||||
@ -253,7 +263,7 @@ class NonInjectedCatalog extends React.Component<Dependencies> {
|
||||
|
||||
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<Dependencies> {
|
||||
{...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<Dependencies>(NonInjectedCatalog, {
|
||||
visitEntityContextMenu: di.inject(visitEntityContextMenuInjectable),
|
||||
navigate: di.inject(navigateInjectable),
|
||||
normalizeMenuItem: di.inject(normalizeCatalogEntityContextMenuInjectable),
|
||||
logger: di.inject(loggerInjectable),
|
||||
showErrorNotification: di.inject(showErrorNotificationInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
@ -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;
|
||||
@ -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 }) => (
|
||||
<>
|
||||
<DrawerTitle>Kubernetes Information</DrawerTitle>
|
||||
<div className="box grow EntityMetadata">
|
||||
<DrawerItem
|
||||
name="Distribution"
|
||||
data-testid={`kubernetes-distro-for-${entity.getId()}`}
|
||||
>
|
||||
{entity.metadata.distro || "unknown"}
|
||||
</DrawerItem>
|
||||
<DrawerItem name="Kubelet Version">
|
||||
{entity.metadata.kubeVersion || "unknown"}
|
||||
</DrawerItem>
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
},
|
||||
}),
|
||||
injectionToken: catalogEntityDetailItemInjectionToken,
|
||||
});
|
||||
|
||||
export default kubernetesClusterDetailsItemInjectable;
|
||||
@ -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 }) => (
|
||||
<>
|
||||
<DrawerTitle>More Information</DrawerTitle>
|
||||
<DrawerItem
|
||||
name="URL"
|
||||
data-testid={`weblink-url-for-${entity.getId()}`}
|
||||
>
|
||||
{entity.spec.url}
|
||||
</DrawerItem>
|
||||
</>
|
||||
),
|
||||
},
|
||||
orderNumber: 40,
|
||||
}),
|
||||
injectionToken: catalogEntityDetailItemInjectionToken,
|
||||
});
|
||||
|
||||
export default weblinkDetailsItemInjectable;
|
||||
@ -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<CatalogEntity>) => getInjectable({
|
||||
id: `catalog-entity-detail-item-for-${extension.sanitizedExtensionId}-${getRandId({ sep: "-" })}`,
|
||||
instantiate: () => ({
|
||||
apiVersions: new Set(apiVersions),
|
||||
components,
|
||||
kind,
|
||||
orderNumber: priority ?? 50,
|
||||
}),
|
||||
injectionToken: catalogEntityDetailItemInjectionToken,
|
||||
});
|
||||
35
src/renderer/components/+catalog/entity-details/token.ts
Normal file
35
src/renderer/components/+catalog/entity-details/token.ts
Normal file
@ -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<T extends CatalogEntity> {
|
||||
entity: T;
|
||||
}
|
||||
|
||||
export type CatalogEntityDetailsComponent<T extends CatalogEntity> = React.ComponentType<CatalogEntityDetailsProps<T>>;
|
||||
|
||||
export interface CatalogEntityDetailComponents<T extends CatalogEntity> {
|
||||
Details: CatalogEntityDetailsComponent<T>;
|
||||
}
|
||||
|
||||
export interface CatalogEntityDetailRegistration<T extends CatalogEntity> {
|
||||
kind: string;
|
||||
apiVersions: string[];
|
||||
components: CatalogEntityDetailComponents<T>;
|
||||
priority?: number;
|
||||
}
|
||||
|
||||
export interface CatalogEntityDetailItem {
|
||||
kind: string;
|
||||
apiVersions: Set<string>;
|
||||
components: CatalogEntityDetailComponents<CatalogEntity>;
|
||||
orderNumber: number;
|
||||
}
|
||||
|
||||
export const catalogEntityDetailItemInjectionToken = getInjectionToken<CatalogEntityDetailItem>({
|
||||
id: "catalog-entity-detail-item-token",
|
||||
});
|
||||
127
src/renderer/components/+catalog/entity-details/view.tsx
Normal file
127
src/renderer/components/+catalog/entity-details/view.tsx
Normal file
@ -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 extends CatalogEntity> {
|
||||
entity: Entity;
|
||||
hideDetails(): void;
|
||||
onRun: () => void;
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
getLabelBadges: GetLabelBadges;
|
||||
isDevelopment: boolean;
|
||||
detailItems: IComputedValue<CatalogEntityDetailsComponent<CatalogEntity>[]>;
|
||||
}
|
||||
|
||||
@observer
|
||||
class NonInjectedCatalogEntityDetails<Entity extends CatalogEntity> extends Component<CatalogEntityDetailsProps<Entity> & Dependencies> {
|
||||
categoryIcon(category: CatalogCategory) {
|
||||
if (Icon.isSvg(category.metadata.icon)) {
|
||||
return <Icon svg={category.metadata.icon} smallest />;
|
||||
} else {
|
||||
return <Icon material={category.metadata.icon} smallest />;
|
||||
}
|
||||
}
|
||||
|
||||
renderContent(entity: Entity) {
|
||||
const { onRun, hideDetails, getLabelBadges, isDevelopment, detailItems } = this.props;
|
||||
const details = detailItems.get().map((Details, index) => <Details entity={entity} key={index} />);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex" data-testid={`catalog-entity-details-content-for-${entity.getId()}`}>
|
||||
<div className={styles.entityIcon}>
|
||||
<Avatar
|
||||
title={entity.getName()}
|
||||
colorHash={`${entity.getName()}-${entity.getSource()}`}
|
||||
size={128}
|
||||
src={entity.spec.icon?.src}
|
||||
data-testid="detail-panel-hot-bar-icon"
|
||||
background={entity.spec.icon?.background}
|
||||
onClick={onRun}
|
||||
className={styles.avatar}
|
||||
>
|
||||
{entity.spec.icon?.material && <Icon material={entity.spec.icon?.material}/>}
|
||||
</Avatar>
|
||||
{entity.isEnabled() && (
|
||||
<div className={styles.hint}>
|
||||
Click to open
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={cssNames("box grow", styles.metadata)}>
|
||||
<DrawerItem name="Name">
|
||||
{entity.getName()}
|
||||
</DrawerItem>
|
||||
<DrawerItem name="Kind">
|
||||
{entity.kind}
|
||||
</DrawerItem>
|
||||
<DrawerItem name="Source">
|
||||
{entity.getSource()}
|
||||
</DrawerItem>
|
||||
<DrawerItem name="Status">
|
||||
{entity.status.phase}
|
||||
</DrawerItem>
|
||||
<DrawerItem name="Labels">
|
||||
{getLabelBadges(entity, hideDetails)}
|
||||
</DrawerItem>
|
||||
{isDevelopment && (
|
||||
<DrawerItem name="Id">
|
||||
{entity.getId()}
|
||||
</DrawerItem>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="box grow">
|
||||
{details}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { entity, hideDetails } = this.props;
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
className={styles.entityDetails}
|
||||
usePortal={true}
|
||||
open={true}
|
||||
title={`${entity.kind}: ${entity.getName()}`}
|
||||
toolbar={<CatalogEntityDrawerMenu entity={entity} key={entity.getId()} />}
|
||||
onClose={hideDetails}
|
||||
data-testid="catalog-entity-details-drawer"
|
||||
>
|
||||
{this.renderContent(entity)}
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const CatalogEntityDetails = withInjectables<Dependencies, CatalogEntityDetailsProps<CatalogEntity>>(NonInjectedCatalogEntityDetails, {
|
||||
getProps: (di, props) => ({
|
||||
...props,
|
||||
getLabelBadges: di.inject(getLabelBadgesInjectable),
|
||||
isDevelopment: di.inject(isDevelopmentInjectable),
|
||||
detailItems: di.inject(catalogEntityDetailItemsInjectable, props.entity),
|
||||
}),
|
||||
}) as <Entity extends CatalogEntity>(props: CatalogEntityDetailsProps<Entity>) => React.ReactElement;
|
||||
@ -153,6 +153,7 @@ export interface BaseIconProps {
|
||||
focusable?: boolean;
|
||||
sticker?: boolean;
|
||||
disabled?: boolean;
|
||||
"data-testid"?: string;
|
||||
}
|
||||
|
||||
export interface IconProps extends React.HTMLAttributes<any>, BaseIconProps {}
|
||||
|
||||
@ -131,6 +131,7 @@ export type ItemListLayoutProps<Item extends ItemObject, PreLoadStores extends b
|
||||
failedToLoadMessage?: React.ReactNode;
|
||||
|
||||
filterCallbacks?: ItemsFilters<Item>;
|
||||
"data-testid"?: string;
|
||||
} & (
|
||||
PreLoadStores extends true
|
||||
? {
|
||||
@ -271,11 +272,12 @@ class NonInjectedItemListLayout<I extends ItemObject, PreLoadStores extends bool
|
||||
}
|
||||
|
||||
render() {
|
||||
const { renderHeaderTitle } = this.props;
|
||||
const { renderHeaderTitle, "data-testid": dataTestId } = this.props;
|
||||
|
||||
return untracked(() => (
|
||||
<div
|
||||
className={cssNames("ItemListLayout flex column", this.props.className)}
|
||||
data-testid={dataTestId}
|
||||
>
|
||||
<ItemListLayoutHeader
|
||||
getItems={() => this.items}
|
||||
|
||||
@ -91,15 +91,22 @@ class NonInjectedMenuActions extends React.Component<MenuActionsProps & Dependen
|
||||
}
|
||||
|
||||
renderTriggerIcon() {
|
||||
if (this.props.toolbar) return null;
|
||||
const { triggerIcon = "more_vert" } = this.props;
|
||||
let className: string;
|
||||
const {
|
||||
triggerIcon = "more_vert",
|
||||
toolbar,
|
||||
"data-testid": dataTestId,
|
||||
} = this.props;
|
||||
|
||||
if (toolbar) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isValidElement<HTMLElement>(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<MenuActionsProps & Dependen
|
||||
...(typeof triggerIcon === "object" ? triggerIcon : {}),
|
||||
};
|
||||
|
||||
if (dataTestId) {
|
||||
iconProps["data-testid"] = `icon-for-${dataTestId}`;
|
||||
}
|
||||
|
||||
if (iconProps.tooltip && this.isOpen) {
|
||||
delete iconProps.tooltip; // don't show tooltip for icon when menu is open
|
||||
}
|
||||
|
||||
@ -50,6 +50,7 @@ export interface MenuProps {
|
||||
children?: ReactNode;
|
||||
animated?: boolean;
|
||||
toggleEvent?: "click" | "contextmenu";
|
||||
"data-testid"?: string;
|
||||
}
|
||||
|
||||
interface State {
|
||||
@ -325,12 +326,11 @@ class NonInjectedMenu extends React.Component<MenuProps & Dependencies, State> {
|
||||
}
|
||||
|
||||
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<any>;
|
||||
|
||||
@ -351,12 +351,13 @@ class NonInjectedMenu extends React.Component<MenuProps & Dependencies, State> {
|
||||
<ul
|
||||
id={id}
|
||||
ref={this.bindRef}
|
||||
className={className}
|
||||
className={classNames}
|
||||
style={{
|
||||
left: this.state?.menuStyle?.left,
|
||||
top: this.state?.menuStyle?.top,
|
||||
}}
|
||||
onKeyDown={this.onKeyDown}
|
||||
data-testid={dataTestId}
|
||||
>
|
||||
{menuItems}
|
||||
</ul>
|
||||
@ -376,11 +377,13 @@ class NonInjectedMenu extends React.Component<MenuProps & Dependencies, State> {
|
||||
</MenuContext.Provider>
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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<ExtensionsStore> 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<HotbarStore> as HotbarStore);
|
||||
|
||||
di.override(fileSystemProvisionerStoreInjectable, () => ({}) as FileSystemProvisionerStore);
|
||||
|
||||
|
||||
@ -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<KubernetesCluster>) => (
|
||||
<>
|
||||
<DrawerTitle>Kubernetes Information</DrawerTitle>
|
||||
<div className="box grow EntityMetadata">
|
||||
<DrawerItem name="Distribution">
|
||||
{entity.metadata.distro || "unknown"}
|
||||
</DrawerItem>
|
||||
<DrawerItem name="Kubelet Version">
|
||||
{entity.metadata.kubeVersion || "unknown"}
|
||||
</DrawerItem>
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
},
|
||||
},
|
||||
]);
|
||||
CatalogEntityDetailRegistry.getInstance()
|
||||
.add([
|
||||
{
|
||||
apiVersions: [WebLink.apiVersion],
|
||||
kind: WebLink.kind,
|
||||
components: {
|
||||
Details: ({ entity }: CatalogEntityDetailsProps<WebLink>) => (
|
||||
<>
|
||||
<DrawerTitle>More Information</DrawerTitle>
|
||||
<DrawerItem name="URL">
|
||||
{entity.spec.url}
|
||||
</DrawerItem>
|
||||
</>
|
||||
),
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
@ -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";
|
||||
|
||||
@ -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();
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user