Replace CatalogEntityDetailRegistry with an injectable solution (#6605)

* Replace EntityDetailRegistry with an injectable solution

- Add some behavioural tests

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

* Update snapshots

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

* Fix import error

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

* Simplify loading extensions

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

* Fix lint

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

* Update snapshot

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

* Remove the last reminents of BaseRegistry

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

* Fix import errors

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

* Fix TypeError when loading extensions

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

* Update snapshots

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

* Cleanup LensExtensions

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

* Remove bad comment

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

* Fix type errors

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 2022-12-02 10:31:27 -05:00
1 parent 1acacbb7ef
commit 9ba92cb072
78 files changed
+15151 -573

No files matched your search

@@ -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>;
+29 -20
View File
@@ -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,
});
@@ -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",
});
@@ -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;
+1
View File
@@ -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}
@@ -0,0 +1,22 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { IconProps } from "../icon";
import type React from "react";
import type { PageTarget } from "../../routes/page-registration";
import type { IComputedValue } from "mobx";
export interface ClusterPageMenuRegistration {
id?: string;
parentId?: string;
target?: PageTarget;
title: React.ReactNode;
components: ClusterPageMenuComponents;
visible?: IComputedValue<boolean>;
}
export interface ClusterPageMenuComponents {
Icon: React.ComponentType<IconProps>;
}
+15 -4
View File
@@ -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
}
+11 -9
View File
@@ -50,6 +50,7 @@ export interface MenuProps {
children?: ReactNode;
animated?: boolean;
toggleEvent?: "click" | "contextmenu";
"data-testid"?: string;
}
interface State {
@@ -325,10 +326,8 @@ 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,
});
@@ -351,12 +350,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 +376,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);
}
}
@@ -2,7 +2,7 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { Mutable } from "type-fest";
import type { Writable } from "type-fest";
import fileSystemProvisionerStoreInjectable from "../../../extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store.injectable";
import { lensExtensionDependencies } from "../../../extensions/lens-extension";
import { LensMainExtension } from "../../../extensions/lens-main-extension";
@@ -15,6 +15,7 @@ import routesInjectable from "../../routes/routes.injectable";
import catalogEntityRegistryForMainInjectable from "../../../main/catalog/entity-registry.injectable";
import catalogEntityRegistryForRendererInjectable from "../../api/catalog/entity/registry.injectable";
import type { DiContainer } from "@ogre-tools/injectable";
import loggerInjectable from "../../../common/logger.injectable";
export class TestExtensionMain extends LensMainExtension {}
export class TestExtensionRenderer extends LensRendererExtension {}
@@ -45,12 +46,11 @@ export const getExtensionFakeForMain = (di: DiContainer, id: string, name: strin
Object.assign(instance, options);
(instance as Mutable<LensMainExtension>)[lensExtensionDependencies] = {
fileSystemProvisionerStore: di.inject(
fileSystemProvisionerStoreInjectable,
),
(instance as Writable<LensMainExtension>)[lensExtensionDependencies] = {
fileSystemProvisionerStore: di.inject(fileSystemProvisionerStoreInjectable),
entityRegistry: di.inject(catalogEntityRegistryForMainInjectable),
navigate: di.inject(navigateForExtensionInjectable),
logger: di.inject(loggerInjectable),
};
return instance;
@@ -75,13 +75,14 @@ export const getExtensionFakeForRenderer = (di: DiContainer, id: string, name: s
Object.assign(instance, options);
(instance as Mutable<LensRendererExtension>)[lensExtensionDependencies] = {
(instance as Writable<LensRendererExtension>)[lensExtensionDependencies] = {
categoryRegistry: di.inject(catalogCategoryRegistryInjectable),
entityRegistry: di.inject(catalogEntityRegistryForRendererInjectable),
fileSystemProvisionerStore: di.inject(fileSystemProvisionerStoreInjectable),
getExtensionPageParameters: di.inject(getExtensionPageParametersInjectable),
navigateToRoute: di.inject(navigateToRouteInjectable),
routes: di.inject(routesInjectable),
logger: di.inject(loggerInjectable),
};
return instance;