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";
|
} from "react-beautiful-dnd";
|
||||||
|
|
||||||
export const DragDropContext = ({ children }: DragDropContextProps) => <>{ children }</>;
|
export const DragDropContext = ({ children }: DragDropContextProps) => <>{ children }</>;
|
||||||
export const Draggable = ({ children }: DraggableProps) => <>{ children }</>;
|
export const Draggable = ({ children }: DraggableProps) => <>{ children({} as any, {} as any, {} as any) }</>;
|
||||||
export const Droppable = ({ children }: DroppableProps) => <>{ children }</>;
|
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.
|
* 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 type { CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog";
|
||||||
import { CatalogCategory, CatalogEntity, categoryVersion } from "../catalog/catalog-entity";
|
import { CatalogCategory, CatalogEntity, categoryVersion } from "../catalog/catalog-entity";
|
||||||
import productNameInjectable from "../vars/product-name.injectable";
|
import productNameInjectable from "../vars/product-name.injectable";
|
||||||
import { WeblinkStore } from "../weblink-store";
|
import weblinkStoreInjectable from "../weblink-store.injectable";
|
||||||
|
|
||||||
export type WebLinkStatusPhase = "available" | "unavailable";
|
export type WebLinkStatusPhase = "available" | "unavailable";
|
||||||
|
|
||||||
@ -31,14 +31,15 @@ export class WebLink extends CatalogEntity<CatalogEntityMetadata, WebLinkStatus,
|
|||||||
}
|
}
|
||||||
|
|
||||||
onContextMenuOpen(context: CatalogEntityContextMenuContext) {
|
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);
|
const productName = di.inject(productNameInjectable);
|
||||||
|
|
||||||
if (this.metadata.source === "local") {
|
if (this.metadata.source === "local") {
|
||||||
context.menuItems.push({
|
context.menuItems.push({
|
||||||
title: "Delete",
|
title: "Delete",
|
||||||
icon: "delete",
|
icon: "delete",
|
||||||
onClick: async () => WeblinkStore.getInstance().removeById(this.getId()),
|
onClick: async () => di.inject(weblinkStoreInjectable).removeById(this.getId()),
|
||||||
confirm: {
|
confirm: {
|
||||||
message: `Remove Web Link "${this.getName()}" from ${productName}?`,
|
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 { isDefined, toJS } from "../../common/utils";
|
||||||
import type { InstalledExtension } from "../extension-discovery/extension-discovery";
|
import type { InstalledExtension } from "../extension-discovery/extension-discovery";
|
||||||
import type { LensExtension, LensExtensionConstructor, LensExtensionId } from "../lens-extension";
|
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 type { LensExtensionState } from "../extensions-store/extensions-store";
|
||||||
import { extensionLoaderFromMainChannel, extensionLoaderFromRendererChannel } from "../../common/ipc/extension-handling";
|
import { extensionLoaderFromMainChannel, extensionLoaderFromRendererChannel } from "../../common/ipc/extension-handling";
|
||||||
import { requestExtensionLoaderInitialState } from "../../renderer/ipc";
|
import { requestExtensionLoaderInitialState } from "../../renderer/ipc";
|
||||||
@ -252,28 +250,13 @@ export class ExtensionLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadOnMain() {
|
loadOnMain() {
|
||||||
this.autoInitExtensions(() => Promise.resolve([]));
|
this.autoInitExtensions(async () => []);
|
||||||
}
|
}
|
||||||
|
|
||||||
loadOnClusterManagerRenderer = () => {
|
loadOnClusterManagerRenderer = () => {
|
||||||
this.dependencies.logger.debug(`${logModule}: load on main renderer (cluster manager)`);
|
this.dependencies.logger.debug(`${logModule}: load on main renderer (cluster manager)`);
|
||||||
|
|
||||||
return this.autoInitExtensions(async (ext) => {
|
return this.autoInitExtensions(async () => []);
|
||||||
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;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
loadOnClusterRenderer = () => {
|
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-registry";
|
||||||
export * from "./page-menu-registry";
|
export * from "./page-menu-registry";
|
||||||
export * from "./catalog-entity-detail-registry";
|
|
||||||
export * from "./protocol-handler";
|
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();
|
await attachChromeDebugger();
|
||||||
rootElem.classList.toggle("is-mac", isMac);
|
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`);
|
logger.info(`${logPrefix} initializing CatalogCategoryRegistryEntries`);
|
||||||
initializers.initCatalogCategoryRegistryEntries({
|
initializers.initCatalogCategoryRegistryEntries({
|
||||||
navigateToAddCluster: di.inject(navigateToAddClusterInjectable),
|
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 type { CatalogEntityActionContext, CatalogEntityData } from "../../../common/catalog";
|
||||||
import { CatalogEntity } from "../../../common/catalog";
|
import { CatalogEntity } from "../../../common/catalog";
|
||||||
import type { CatalogEntityOnBeforeRun, CatalogEntityRegistry } from "../../api/catalog/entity/registry";
|
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 type { CatalogEntityStore } from "./catalog-entity-store/catalog-entity.store";
|
||||||
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
||||||
import type { DiContainer } from "@ogre-tools/injectable";
|
import type { DiContainer } from "@ogre-tools/injectable";
|
||||||
@ -71,8 +70,6 @@ describe("<Catalog />", () => {
|
|||||||
|
|
||||||
di.permitSideEffects(getConfigurationFileModelInjectable);
|
di.permitSideEffects(getConfigurationFileModelInjectable);
|
||||||
|
|
||||||
CatalogEntityDetailRegistry.createInstance();
|
|
||||||
|
|
||||||
render = renderFor(di);
|
render = renderFor(di);
|
||||||
onRun = jest.fn();
|
onRun = jest.fn();
|
||||||
catalogEntityItem = createMockCatalogEntity(onRun);
|
catalogEntityItem = createMockCatalogEntity(onRun);
|
||||||
@ -87,10 +84,6 @@ describe("<Catalog />", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
CatalogEntityDetailRegistry.resetInstance();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("can use catalogEntityRegistry.addOnBeforeRun to add hooks for catalog entities", () => {
|
describe("can use catalogEntityRegistry.addOnBeforeRun to add hooks for catalog entities", () => {
|
||||||
let onBeforeRunMock: AsyncFnMock<CatalogEntityOnBeforeRun>;
|
let onBeforeRunMock: AsyncFnMock<CatalogEntityOnBeforeRun>;
|
||||||
|
|
||||||
|
|||||||
@ -15,11 +15,11 @@ import { MenuItem, MenuActions } from "../menu";
|
|||||||
import type { CatalogEntityContextMenu } from "../../api/catalog-entity";
|
import type { CatalogEntityContextMenu } from "../../api/catalog-entity";
|
||||||
import type { CatalogCategory, CatalogCategoryRegistry, CatalogEntity } from "../../../common/catalog";
|
import type { CatalogCategory, CatalogCategoryRegistry, CatalogEntity } from "../../../common/catalog";
|
||||||
import { CatalogAddButton } from "./catalog-add-button";
|
import { CatalogAddButton } from "./catalog-add-button";
|
||||||
import { Notifications } from "../notifications";
|
import type { ShowNotification } from "../notifications";
|
||||||
import { MainLayout } from "../layout/main-layout";
|
import { MainLayout } from "../layout/main-layout";
|
||||||
import type { StorageLayer } from "../../utils";
|
import type { StorageLayer } from "../../utils";
|
||||||
import { prevDefault } from "../../utils";
|
import { prevDefault } from "../../utils";
|
||||||
import { CatalogEntityDetails } from "./catalog-entity-details";
|
import { CatalogEntityDetails } from "./entity-details/view";
|
||||||
import { CatalogMenu } from "./catalog-menu";
|
import { CatalogMenu } from "./catalog-menu";
|
||||||
import { RenderDelay } from "../render-delay/render-delay";
|
import { RenderDelay } from "../render-delay/render-delay";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
@ -48,6 +48,9 @@ import type { NormalizeCatalogEntityContextMenu } from "../../catalog/normalize-
|
|||||||
import normalizeCatalogEntityContextMenuInjectable from "../../catalog/normalize-menu-item.injectable";
|
import normalizeCatalogEntityContextMenuInjectable from "../../catalog/normalize-menu-item.injectable";
|
||||||
import type { EmitAppEvent } from "../../../common/app-event-bus/emit-event.injectable";
|
import type { EmitAppEvent } from "../../../common/app-event-bus/emit-event.injectable";
|
||||||
import emitAppEventInjectable 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 {
|
interface Dependencies {
|
||||||
catalogPreviousActiveTabStorage: StorageLayer<string | null>;
|
catalogPreviousActiveTabStorage: StorageLayer<string | null>;
|
||||||
@ -65,12 +68,14 @@ interface Dependencies {
|
|||||||
visitEntityContextMenu: VisitEntityContextMenu;
|
visitEntityContextMenu: VisitEntityContextMenu;
|
||||||
navigate: Navigate;
|
navigate: Navigate;
|
||||||
normalizeMenuItem: NormalizeCatalogEntityContextMenu;
|
normalizeMenuItem: NormalizeCatalogEntityContextMenu;
|
||||||
|
showErrorNotification: ShowNotification;
|
||||||
|
logger: Logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
class NonInjectedCatalog extends React.Component<Dependencies> {
|
class NonInjectedCatalog extends React.Component<Dependencies> {
|
||||||
private readonly menuItems = observable.array<CatalogEntityContextMenu>();
|
private readonly menuItems = observable.array<CatalogEntityContextMenu>();
|
||||||
@observable activeTab?: string;
|
@observable activeTab: string | undefined = undefined;
|
||||||
|
|
||||||
constructor(props: Dependencies) {
|
constructor(props: Dependencies) {
|
||||||
super(props);
|
super(props);
|
||||||
@ -79,7 +84,7 @@ class NonInjectedCatalog extends React.Component<Dependencies> {
|
|||||||
|
|
||||||
@computed
|
@computed
|
||||||
get routeActiveTab(): string {
|
get routeActiveTab(): string {
|
||||||
const { group, kind } = this.props.routeParameters;
|
const { routeParameters: { group, kind }, catalogPreviousActiveTabStorage } = this.props;
|
||||||
|
|
||||||
const dereferencedGroup = group.get();
|
const dereferencedGroup = group.get();
|
||||||
const dereferencedKind = kind.get();
|
const dereferencedKind = kind.get();
|
||||||
@ -88,13 +93,7 @@ class NonInjectedCatalog extends React.Component<Dependencies> {
|
|||||||
return `${dereferencedGroup}/${dereferencedKind}`;
|
return `${dereferencedGroup}/${dereferencedKind}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const previousTab = this.props.catalogPreviousActiveTabStorage.get();
|
return catalogPreviousActiveTabStorage.get() || browseCatalogTab;
|
||||||
|
|
||||||
if (previousTab) {
|
|
||||||
return previousTab;
|
|
||||||
}
|
|
||||||
|
|
||||||
return browseCatalogTab;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
@ -102,6 +101,8 @@ class NonInjectedCatalog extends React.Component<Dependencies> {
|
|||||||
catalogEntityStore,
|
catalogEntityStore,
|
||||||
catalogPreviousActiveTabStorage,
|
catalogPreviousActiveTabStorage,
|
||||||
catalogCategoryRegistry,
|
catalogCategoryRegistry,
|
||||||
|
logger,
|
||||||
|
showErrorNotification,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
disposeOnUnmount(this, [
|
disposeOnUnmount(this, [
|
||||||
@ -110,7 +111,11 @@ class NonInjectedCatalog extends React.Component<Dependencies> {
|
|||||||
catalogPreviousActiveTabStorage.set(this.routeActiveTab);
|
catalogPreviousActiveTabStorage.set(this.routeActiveTab);
|
||||||
|
|
||||||
try {
|
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);
|
const item = catalogCategoryRegistry.filteredItems.find(i => i.getId() === routeTab);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
@ -118,8 +123,8 @@ class NonInjectedCatalog extends React.Component<Dependencies> {
|
|||||||
catalogEntityStore.activeCategory.set(item);
|
catalogEntityStore.activeCategory.set(item);
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
logger.warn("Failed to find route tab", error);
|
||||||
Notifications.error((
|
showErrorNotification((
|
||||||
<p>
|
<p>
|
||||||
{"Unknown category: "}
|
{"Unknown category: "}
|
||||||
{routeTab}
|
{routeTab}
|
||||||
@ -198,9 +203,14 @@ class NonInjectedCatalog extends React.Component<Dependencies> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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
|
<MenuItem
|
||||||
key="open-details"
|
key="open-details"
|
||||||
|
data-testid={`open-details-menu-item-for-${entity.getId()}`}
|
||||||
onClick={() => this.props.catalogEntityStore.selectedItemId.set(entity.getId())}
|
onClick={() => this.props.catalogEntityStore.selectedItemId.set(entity.getId())}
|
||||||
>
|
>
|
||||||
View Details
|
View Details
|
||||||
@ -253,7 +263,7 @@ class NonInjectedCatalog extends React.Component<Dependencies> {
|
|||||||
|
|
||||||
renderViews = (activeCategory: CatalogCategory | undefined) => {
|
renderViews = (activeCategory: CatalogCategory | undefined) => {
|
||||||
if (!activeCategory) {
|
if (!activeCategory) {
|
||||||
return this.renderList(activeCategory);
|
return this.renderList(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
const customViews = this.props.customCategoryViews.get()
|
const customViews = this.props.customCategoryViews.get()
|
||||||
@ -301,15 +311,12 @@ class NonInjectedCatalog extends React.Component<Dependencies> {
|
|||||||
{...getCategoryColumns({ activeCategory })}
|
{...getCategoryColumns({ activeCategory })}
|
||||||
onDetails={this.onDetails}
|
onDetails={this.onDetails}
|
||||||
renderItemMenu={this.renderItemMenu}
|
renderItemMenu={this.renderItemMenu}
|
||||||
|
data-testid={`catalog-list-for-${activeCategory?.metadata.name ?? "browse-all"}`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (!this.props.catalogEntityStore) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const activeCategory = this.props.catalogEntityStore.activeCategory.get();
|
const activeCategory = this.props.catalogEntityStore.activeCategory.get();
|
||||||
const selectedItem = this.props.catalogEntityStore.selectedItem.get();
|
const selectedItem = this.props.catalogEntityStore.selectedItem.get();
|
||||||
|
|
||||||
@ -362,5 +369,7 @@ export const Catalog = withInjectables<Dependencies>(NonInjectedCatalog, {
|
|||||||
visitEntityContextMenu: di.inject(visitEntityContextMenuInjectable),
|
visitEntityContextMenu: di.inject(visitEntityContextMenuInjectable),
|
||||||
navigate: di.inject(navigateInjectable),
|
navigate: di.inject(navigateInjectable),
|
||||||
normalizeMenuItem: di.inject(normalizeCatalogEntityContextMenuInjectable),
|
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;
|
focusable?: boolean;
|
||||||
sticker?: boolean;
|
sticker?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
"data-testid"?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IconProps extends React.HTMLAttributes<any>, BaseIconProps {}
|
export interface IconProps extends React.HTMLAttributes<any>, BaseIconProps {}
|
||||||
|
|||||||
@ -131,6 +131,7 @@ export type ItemListLayoutProps<Item extends ItemObject, PreLoadStores extends b
|
|||||||
failedToLoadMessage?: React.ReactNode;
|
failedToLoadMessage?: React.ReactNode;
|
||||||
|
|
||||||
filterCallbacks?: ItemsFilters<Item>;
|
filterCallbacks?: ItemsFilters<Item>;
|
||||||
|
"data-testid"?: string;
|
||||||
} & (
|
} & (
|
||||||
PreLoadStores extends true
|
PreLoadStores extends true
|
||||||
? {
|
? {
|
||||||
@ -271,11 +272,12 @@ class NonInjectedItemListLayout<I extends ItemObject, PreLoadStores extends bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { renderHeaderTitle } = this.props;
|
const { renderHeaderTitle, "data-testid": dataTestId } = this.props;
|
||||||
|
|
||||||
return untracked(() => (
|
return untracked(() => (
|
||||||
<div
|
<div
|
||||||
className={cssNames("ItemListLayout flex column", this.props.className)}
|
className={cssNames("ItemListLayout flex column", this.props.className)}
|
||||||
|
data-testid={dataTestId}
|
||||||
>
|
>
|
||||||
<ItemListLayoutHeader
|
<ItemListLayoutHeader
|
||||||
getItems={() => this.items}
|
getItems={() => this.items}
|
||||||
|
|||||||
@ -91,15 +91,22 @@ class NonInjectedMenuActions extends React.Component<MenuActionsProps & Dependen
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderTriggerIcon() {
|
renderTriggerIcon() {
|
||||||
if (this.props.toolbar) return null;
|
const {
|
||||||
const { triggerIcon = "more_vert" } = this.props;
|
triggerIcon = "more_vert",
|
||||||
let className: string;
|
toolbar,
|
||||||
|
"data-testid": dataTestId,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (toolbar) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (isValidElement<HTMLElement>(triggerIcon)) {
|
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 });
|
return React.cloneElement(triggerIcon, { id: this.props.id, className });
|
||||||
}
|
}
|
||||||
|
|
||||||
const iconProps: IconProps & TooltipDecoratorProps = {
|
const iconProps: IconProps & TooltipDecoratorProps = {
|
||||||
id: this.props.id,
|
id: this.props.id,
|
||||||
interactive: true,
|
interactive: true,
|
||||||
@ -108,6 +115,10 @@ class NonInjectedMenuActions extends React.Component<MenuActionsProps & Dependen
|
|||||||
...(typeof triggerIcon === "object" ? triggerIcon : {}),
|
...(typeof triggerIcon === "object" ? triggerIcon : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (dataTestId) {
|
||||||
|
iconProps["data-testid"] = `icon-for-${dataTestId}`;
|
||||||
|
}
|
||||||
|
|
||||||
if (iconProps.tooltip && this.isOpen) {
|
if (iconProps.tooltip && this.isOpen) {
|
||||||
delete iconProps.tooltip; // don't show tooltip for icon when menu is open
|
delete iconProps.tooltip; // don't show tooltip for icon when menu is open
|
||||||
}
|
}
|
||||||
|
|||||||
@ -50,6 +50,7 @@ export interface MenuProps {
|
|||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
animated?: boolean;
|
animated?: boolean;
|
||||||
toggleEvent?: "click" | "contextmenu";
|
toggleEvent?: "click" | "contextmenu";
|
||||||
|
"data-testid"?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
@ -325,12 +326,11 @@ class NonInjectedMenu extends React.Component<MenuProps & Dependencies, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { position, id, animated } = this.props;
|
const { position, id, animated, "data-testid": dataTestId, usePortal, className } = this.props;
|
||||||
let { className, usePortal } = this.props;
|
const classNames = cssNames("Menu", className, this.state.position || position, {
|
||||||
|
|
||||||
className = cssNames("Menu", className, this.state.position || position, {
|
|
||||||
portal: usePortal,
|
portal: usePortal,
|
||||||
});
|
});
|
||||||
|
// const menuChildren =
|
||||||
|
|
||||||
let children = this.props.children as ReactElement<any>;
|
let children = this.props.children as ReactElement<any>;
|
||||||
|
|
||||||
@ -351,12 +351,13 @@ class NonInjectedMenu extends React.Component<MenuProps & Dependencies, State> {
|
|||||||
<ul
|
<ul
|
||||||
id={id}
|
id={id}
|
||||||
ref={this.bindRef}
|
ref={this.bindRef}
|
||||||
className={className}
|
className={classNames}
|
||||||
style={{
|
style={{
|
||||||
left: this.state?.menuStyle?.left,
|
left: this.state?.menuStyle?.left,
|
||||||
top: this.state?.menuStyle?.top,
|
top: this.state?.menuStyle?.top,
|
||||||
}}
|
}}
|
||||||
onKeyDown={this.onKeyDown}
|
onKeyDown={this.onKeyDown}
|
||||||
|
data-testid={dataTestId}
|
||||||
>
|
>
|
||||||
{menuItems}
|
{menuItems}
|
||||||
</ul>
|
</ul>
|
||||||
@ -376,11 +377,13 @@ class NonInjectedMenu extends React.Component<MenuProps & Dependencies, State> {
|
|||||||
</MenuContext.Provider>
|
</MenuContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (usePortal === true) usePortal = document.body;
|
if (!usePortal) {
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
|
||||||
return usePortal instanceof HTMLElement
|
const portal = usePortal === true ? document.body : usePortal;
|
||||||
? createPortal(menu, usePortal)
|
|
||||||
: menu;
|
return createPortal(menu, portal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -145,13 +145,19 @@ export const getDiForUnitTesting = (
|
|||||||
callForPublicHelmRepositoriesInjectable,
|
callForPublicHelmRepositoriesInjectable,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// eslint-disable-next-line unused-imports/no-unused-vars-ts
|
di.override(extensionsStoreInjectable, () => ({
|
||||||
di.override(extensionsStoreInjectable, () => ({ isEnabled: ({ id, isBundled }) => false }) as ExtensionsStore);
|
isEnabled: () => false,
|
||||||
|
}) as Partial<ExtensionsStore> as ExtensionsStore);
|
||||||
|
|
||||||
di.override(hotbarStoreInjectable, () => ({
|
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",
|
getDisplayIndex: () => "0",
|
||||||
}) as unknown as HotbarStore);
|
isAddedToActive: () => false,
|
||||||
|
}) as Partial<HotbarStore> as HotbarStore);
|
||||||
|
|
||||||
di.override(fileSystemProvisionerStoreInjectable, () => ({}) as FileSystemProvisionerStore);
|
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.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export * from "./catalog-entity-detail-registry";
|
|
||||||
export * from "./catalog";
|
export * from "./catalog";
|
||||||
export * from "./ipc";
|
export * from "./ipc";
|
||||||
export * from "./registries";
|
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