mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
catalog details panel (#2939)
Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> Co-authored-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
031c57962b
commit
0fb927f96b
@ -64,6 +64,8 @@ export async function waitForMinikubeDashboard(app: Application) {
|
|||||||
await app.client.setValue(".Input.SearchInput input", "minikube");
|
await app.client.setValue(".Input.SearchInput input", "minikube");
|
||||||
await app.client.waitUntilTextExists("div.TableCell", "minikube");
|
await app.client.waitUntilTextExists("div.TableCell", "minikube");
|
||||||
await app.client.click("div.TableRow");
|
await app.client.click("div.TableRow");
|
||||||
|
await app.client.waitUntilTextExists("div.drawer-title-text", "KubernetesCluster: minikube");
|
||||||
|
await app.client.click("div.EntityIcon div.HotbarIcon div div.MuiAvatar-root");
|
||||||
await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started");
|
await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started");
|
||||||
await app.client.waitForExist(`iframe[name="minikube"]`);
|
await app.client.waitForExist(`iframe[name="minikube"]`);
|
||||||
await app.client.frame("minikube");
|
await app.client.frame("minikube");
|
||||||
|
|||||||
@ -104,6 +104,7 @@ export class KubernetesCluster extends CatalogEntity<CatalogEntityMetadata, Kube
|
|||||||
context.menuItems = [
|
context.menuItems = [
|
||||||
{
|
{
|
||||||
title: "Settings",
|
title: "Settings",
|
||||||
|
icon: "edit",
|
||||||
onlyVisibleForSource: "local",
|
onlyVisibleForSource: "local",
|
||||||
onClick: async () => context.navigate(`/entity/${this.metadata.uid}/settings`)
|
onClick: async () => context.navigate(`/entity/${this.metadata.uid}/settings`)
|
||||||
},
|
},
|
||||||
@ -112,6 +113,7 @@ export class KubernetesCluster extends CatalogEntity<CatalogEntityMetadata, Kube
|
|||||||
if (this.metadata.labels["file"]?.startsWith(ClusterStore.storedKubeConfigFolder)) {
|
if (this.metadata.labels["file"]?.startsWith(ClusterStore.storedKubeConfigFolder)) {
|
||||||
context.menuItems.push({
|
context.menuItems.push({
|
||||||
title: "Delete",
|
title: "Delete",
|
||||||
|
icon: "delete",
|
||||||
onlyVisibleForSource: "local",
|
onlyVisibleForSource: "local",
|
||||||
onClick: async () => ClusterStore.getInstance().removeById(this.metadata.uid),
|
onClick: async () => ClusterStore.getInstance().removeById(this.metadata.uid),
|
||||||
confirm: {
|
confirm: {
|
||||||
@ -123,6 +125,7 @@ export class KubernetesCluster extends CatalogEntity<CatalogEntityMetadata, Kube
|
|||||||
if (this.status.phase == "connected") {
|
if (this.status.phase == "connected") {
|
||||||
context.menuItems.push({
|
context.menuItems.push({
|
||||||
title: "Disconnect",
|
title: "Disconnect",
|
||||||
|
icon: "link_off",
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
requestMain(clusterDisconnectHandler, this.metadata.uid);
|
requestMain(clusterDisconnectHandler, this.metadata.uid);
|
||||||
}
|
}
|
||||||
@ -130,6 +133,7 @@ export class KubernetesCluster extends CatalogEntity<CatalogEntityMetadata, Kube
|
|||||||
} else {
|
} else {
|
||||||
context.menuItems.push({
|
context.menuItems.push({
|
||||||
title: "Connect",
|
title: "Connect",
|
||||||
|
icon: "link",
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
context.navigate(`/cluster/${this.metadata.uid}`);
|
context.navigate(`/cluster/${this.metadata.uid}`);
|
||||||
}
|
}
|
||||||
@ -147,7 +151,7 @@ export class KubernetesClusterCategory extends CatalogCategory {
|
|||||||
public readonly kind = "CatalogCategory";
|
public readonly kind = "CatalogCategory";
|
||||||
public metadata = {
|
public metadata = {
|
||||||
name: "Kubernetes Clusters",
|
name: "Kubernetes Clusters",
|
||||||
icon: require(`!!raw-loader!./icons/kubernetes.svg`).default // eslint-disable-line
|
icon: require(`!!raw-loader!./icons/kubernetes.svg`).default, // eslint-disable-line
|
||||||
};
|
};
|
||||||
public spec: CatalogCategorySpec = {
|
public spec: CatalogCategorySpec = {
|
||||||
group: "entity.k8slens.dev",
|
group: "entity.k8slens.dev",
|
||||||
|
|||||||
@ -96,9 +96,25 @@ export interface CatalogEntityActionContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface CatalogEntityContextMenu {
|
export interface CatalogEntityContextMenu {
|
||||||
|
/**
|
||||||
|
* Menu title
|
||||||
|
*/
|
||||||
title: string;
|
title: string;
|
||||||
onlyVisibleForSource?: string; // show only if empty or if matches with entity source
|
/**
|
||||||
|
* Menu icon
|
||||||
|
*/
|
||||||
|
icon?: string;
|
||||||
|
/**
|
||||||
|
* Show only if empty or if value matches with entity.metadata.source
|
||||||
|
*/
|
||||||
|
onlyVisibleForSource?: string;
|
||||||
|
/**
|
||||||
|
* OnClick handler
|
||||||
|
*/
|
||||||
onClick: () => void | Promise<void>;
|
onClick: () => void | Promise<void>;
|
||||||
|
/**
|
||||||
|
* Confirm click with a message
|
||||||
|
*/
|
||||||
confirm?: {
|
confirm?: {
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
@ -175,7 +191,6 @@ export abstract class CatalogEntity<
|
|||||||
}
|
}
|
||||||
|
|
||||||
public abstract onRun?(context: CatalogEntityActionContext): void | Promise<void>;
|
public abstract onRun?(context: CatalogEntityActionContext): void | Promise<void>;
|
||||||
public abstract onDetailsOpen(context: CatalogEntityActionContext): void | Promise<void>;
|
|
||||||
public abstract onContextMenuOpen(context: CatalogEntityContextMenuContext): void | Promise<void>;
|
public abstract onContextMenuOpen(context: CatalogEntityContextMenuContext): void | Promise<void>;
|
||||||
public abstract onSettingsOpen(context: CatalogEntitySettingsContext): void | Promise<void>;
|
public abstract onSettingsOpen(context: CatalogEntitySettingsContext): void | Promise<void>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -250,6 +250,7 @@ export class ExtensionLoader extends Singleton {
|
|||||||
registries.statusBarRegistry.add(extension.statusBarItems),
|
registries.statusBarRegistry.add(extension.statusBarItems),
|
||||||
registries.commandRegistry.add(extension.commands),
|
registries.commandRegistry.add(extension.commands),
|
||||||
registries.welcomeMenuRegistry.add(extension.welcomeMenus),
|
registries.welcomeMenuRegistry.add(extension.welcomeMenus),
|
||||||
|
registries.catalogEntityDetailRegistry.add(extension.catalogEntityDetailItems),
|
||||||
];
|
];
|
||||||
|
|
||||||
this.events.on("remove", (removedExtension: LensRendererExtension) => {
|
this.events.on("remove", (removedExtension: LensRendererExtension) => {
|
||||||
|
|||||||
@ -20,7 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
AppPreferenceRegistration, ClusterPageMenuRegistration, KubeObjectDetailRegistration, KubeObjectMenuRegistration,
|
AppPreferenceRegistration, CatalogEntityDetailRegistration, ClusterPageMenuRegistration, KubeObjectDetailRegistration, KubeObjectMenuRegistration,
|
||||||
KubeObjectStatusRegistration, PageMenuRegistration, PageRegistration, StatusBarRegistration, WelcomeMenuRegistration, WorkloadsOverviewDetailRegistration,
|
KubeObjectStatusRegistration, PageMenuRegistration, PageRegistration, StatusBarRegistration, WelcomeMenuRegistration, WorkloadsOverviewDetailRegistration,
|
||||||
} from "./registries";
|
} from "./registries";
|
||||||
import type { Cluster } from "../main/cluster";
|
import type { Cluster } from "../main/cluster";
|
||||||
@ -43,6 +43,7 @@ export class LensRendererExtension extends LensExtension {
|
|||||||
kubeWorkloadsOverviewItems: WorkloadsOverviewDetailRegistration[] = [];
|
kubeWorkloadsOverviewItems: WorkloadsOverviewDetailRegistration[] = [];
|
||||||
commands: CommandRegistration[] = [];
|
commands: CommandRegistration[] = [];
|
||||||
welcomeMenus: WelcomeMenuRegistration[] = [];
|
welcomeMenus: WelcomeMenuRegistration[] = [];
|
||||||
|
catalogEntityDetailItems: CatalogEntityDetailRegistration[] = [];
|
||||||
|
|
||||||
async navigate<P extends object>(pageId?: string, params?: P) {
|
async navigate<P extends object>(pageId?: string, params?: P) {
|
||||||
const { navigate } = await import("../renderer/navigation");
|
const { navigate } = await import("../renderer/navigation");
|
||||||
|
|||||||
46
src/extensions/registries/catalog-entity-detail-registry.ts
Normal file
46
src/extensions/registries/catalog-entity-detail-registry.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type React from "react";
|
||||||
|
import { BaseRegistry } from "./base-registry";
|
||||||
|
|
||||||
|
export interface CatalogEntityDetailComponents {
|
||||||
|
Details: React.ComponentType<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CatalogEntityDetailRegistration {
|
||||||
|
kind: string;
|
||||||
|
apiVersions: string[];
|
||||||
|
components: CatalogEntityDetailComponents;
|
||||||
|
priority?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CatalogEntityDetailRegistry extends BaseRegistry<CatalogEntityDetailRegistration> {
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const catalogEntityDetailRegistry = new CatalogEntityDetailRegistry();
|
||||||
@ -33,4 +33,5 @@ export * from "./command-registry";
|
|||||||
export * from "./entity-setting-registry";
|
export * from "./entity-setting-registry";
|
||||||
export * from "./welcome-menu-registry";
|
export * from "./welcome-menu-registry";
|
||||||
export * from "./protocol-handler-registry";
|
export * from "./protocol-handler-registry";
|
||||||
|
export * from "./catalog-entity-detail-registry";
|
||||||
export * from "./workloads-overview-detail-registry";
|
export * from "./workloads-overview-detail-registry";
|
||||||
|
|||||||
43
src/renderer/components/+catalog/catalog-entity-details.scss
Normal file
43
src/renderer/components/+catalog/catalog-entity-details.scss
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.CatalogEntityDetails {
|
||||||
|
.EntityMetadata {
|
||||||
|
margin-right: $margin;
|
||||||
|
}
|
||||||
|
.EntityIcon.box.top.left {
|
||||||
|
margin-right: $margin * 2;
|
||||||
|
|
||||||
|
.IconHint {
|
||||||
|
text-align: center;
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-top: $margin;
|
||||||
|
cursor: default;
|
||||||
|
user-select: none;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
div * {
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
129
src/renderer/components/+catalog/catalog-entity-details.tsx
Normal file
129
src/renderer/components/+catalog/catalog-entity-details.tsx
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "./catalog-entity-details.scss";
|
||||||
|
import React, { Component } from "react";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { Drawer, DrawerItem, DrawerItemLabels } from "../drawer";
|
||||||
|
import { CatalogEntity, catalogEntityRunContext } from "../../api/catalog-entity";
|
||||||
|
import type { CatalogCategory } from "../../../common/catalog";
|
||||||
|
import { Icon } from "../icon";
|
||||||
|
import { KubeObject } from "../../api/kube-object";
|
||||||
|
import { CatalogEntityDrawerMenu } from "./catalog-entity-drawer-menu";
|
||||||
|
import { catalogEntityDetailRegistry } from "../../../extensions/registries";
|
||||||
|
import { HotbarIcon } from "../hotbar/hotbar-icon";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
entity: CatalogEntity;
|
||||||
|
hideDetails(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class CatalogEntityDetails extends Component<Props> {
|
||||||
|
private abortController?: AbortController;
|
||||||
|
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.abortController?.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
categoryIcon(category: CatalogCategory) {
|
||||||
|
if (category.metadata.icon.includes("<svg")) {
|
||||||
|
return <Icon svg={category.metadata.icon} smallest />;
|
||||||
|
} else {
|
||||||
|
return <Icon material={category.metadata.icon} smallest />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
openEntity() {
|
||||||
|
this.props.entity.onRun(catalogEntityRunContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderContent() {
|
||||||
|
const { entity } = this.props;
|
||||||
|
const labels = KubeObject.stringifyLabels(entity.metadata.labels);
|
||||||
|
const detailItems = catalogEntityDetailRegistry.getItemsForKind(entity.kind, entity.apiVersion);
|
||||||
|
const details = detailItems.map((item, index) => {
|
||||||
|
return <item.components.Details entity={entity} key={index}/>;
|
||||||
|
});
|
||||||
|
|
||||||
|
const showDetails = detailItems.find((item) => item.priority > 999) === undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{showDetails && (
|
||||||
|
<div className="flex CatalogEntityDetails">
|
||||||
|
<div className="EntityIcon box top left">
|
||||||
|
<HotbarIcon
|
||||||
|
uid={entity.metadata.uid}
|
||||||
|
title={entity.metadata.name}
|
||||||
|
source={entity.metadata.source}
|
||||||
|
onClick={() => this.openEntity()}
|
||||||
|
size={128} />
|
||||||
|
<div className="IconHint">
|
||||||
|
Click to open
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="box grow EntityMetadata">
|
||||||
|
<DrawerItem name="Name">
|
||||||
|
{entity.metadata.name}
|
||||||
|
</DrawerItem>
|
||||||
|
<DrawerItem name="Kind">
|
||||||
|
{entity.kind}
|
||||||
|
</DrawerItem>
|
||||||
|
<DrawerItem name="Source">
|
||||||
|
{entity.metadata.source}
|
||||||
|
</DrawerItem>
|
||||||
|
<DrawerItemLabels
|
||||||
|
name="Labels"
|
||||||
|
labels={labels}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="box grow">
|
||||||
|
{details}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { entity, hideDetails } = this.props;
|
||||||
|
const title = `${entity.kind}: ${entity.metadata.name}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
className="CatalogEntityDetails"
|
||||||
|
usePortal={true}
|
||||||
|
open={true}
|
||||||
|
title={title}
|
||||||
|
toolbar={<CatalogEntityDrawerMenu entity={entity} key={entity.getId()} />}
|
||||||
|
onClose={hideDetails}
|
||||||
|
>
|
||||||
|
{this.renderContent()}
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
127
src/renderer/components/+catalog/catalog-entity-drawer-menu.tsx
Normal file
127
src/renderer/components/+catalog/catalog-entity-drawer-menu.tsx
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { cssNames } from "../../utils";
|
||||||
|
import { MenuActions, MenuActionsProps } from "../menu/menu-actions";
|
||||||
|
import type { CatalogEntity, CatalogEntityContextMenu, CatalogEntityContextMenuContext } from "../../api/catalog-entity";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { makeObservable, observable } from "mobx";
|
||||||
|
import { navigate } from "../../navigation";
|
||||||
|
import { MenuItem } from "../menu";
|
||||||
|
import { ConfirmDialog } from "../confirm-dialog";
|
||||||
|
import { HotbarStore } from "../../../common/hotbar-store";
|
||||||
|
import { Icon } from "../icon";
|
||||||
|
|
||||||
|
export interface CatalogEntityDrawerMenuProps<T extends CatalogEntity> extends MenuActionsProps {
|
||||||
|
entity: T | null | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class CatalogEntityDrawerMenu<T extends CatalogEntity> extends React.Component<CatalogEntityDrawerMenuProps<T>> {
|
||||||
|
@observable private contextMenu: CatalogEntityContextMenuContext;
|
||||||
|
|
||||||
|
constructor(props: CatalogEntityDrawerMenuProps<T>) {
|
||||||
|
super(props);
|
||||||
|
makeObservable(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.contextMenu = {
|
||||||
|
menuItems: [],
|
||||||
|
navigate: (url: string) => navigate(url)
|
||||||
|
};
|
||||||
|
this.props.entity?.onContextMenuOpen(this.contextMenu);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMenuItemClick(menuItem: CatalogEntityContextMenu) {
|
||||||
|
if (menuItem.confirm) {
|
||||||
|
ConfirmDialog.open({
|
||||||
|
okButtonProps: {
|
||||||
|
primary: false,
|
||||||
|
accent: true,
|
||||||
|
},
|
||||||
|
ok: () => {
|
||||||
|
menuItem.onClick();
|
||||||
|
},
|
||||||
|
message: menuItem.confirm.message
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
menuItem.onClick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addToHotbar(entity: CatalogEntity): void {
|
||||||
|
HotbarStore.getInstance().addToHotbar(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
getMenuItems(entity: T): React.ReactChild[] {
|
||||||
|
if (!entity) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const menuItems = this.contextMenu.menuItems.filter((menuItem) => {
|
||||||
|
return menuItem.icon && !menuItem.onlyVisibleForSource || menuItem.onlyVisibleForSource === entity.metadata.source;
|
||||||
|
});
|
||||||
|
|
||||||
|
const items = menuItems.map((menuItem, index) => {
|
||||||
|
const props = menuItem.icon.includes("<svg") ? { svg: menuItem.icon } : { material: menuItem.icon };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuItem key={index} onClick={() => this.onMenuItemClick(menuItem)}>
|
||||||
|
<Icon
|
||||||
|
title={menuItem.title}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
items.unshift(
|
||||||
|
<MenuItem key="add-to-hotbar" onClick={() => this.addToHotbar(entity) }>
|
||||||
|
<Icon material="playlist_add" small title="Add to Hotbar" />
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
|
||||||
|
items.reverse();
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (!this.contextMenu) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { className, entity, ...menuProps } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuActions
|
||||||
|
className={cssNames("CatalogEntityDrawerMenu", className)}
|
||||||
|
toolbar
|
||||||
|
{...menuProps}
|
||||||
|
>
|
||||||
|
{this.getMenuItems(entity)}
|
||||||
|
</MenuActions>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -29,7 +29,7 @@ import { navigate } from "../../navigation";
|
|||||||
import { kebabCase } from "lodash";
|
import { kebabCase } from "lodash";
|
||||||
import { PageLayout } from "../layout/page-layout";
|
import { PageLayout } from "../layout/page-layout";
|
||||||
import { MenuItem, MenuActions } from "../menu";
|
import { MenuItem, MenuActions } from "../menu";
|
||||||
import { CatalogEntityContextMenu, CatalogEntityContextMenuContext, catalogEntityRunContext } from "../../api/catalog-entity";
|
import type { CatalogEntityContextMenu, CatalogEntityContextMenuContext } from "../../api/catalog-entity";
|
||||||
import { Badge } from "../badge";
|
import { Badge } from "../badge";
|
||||||
import { HotbarStore } from "../../../common/hotbar-store";
|
import { HotbarStore } from "../../../common/hotbar-store";
|
||||||
import { ConfirmDialog } from "../confirm-dialog";
|
import { ConfirmDialog } from "../confirm-dialog";
|
||||||
@ -40,6 +40,7 @@ import type { RouteComponentProps } from "react-router";
|
|||||||
import type { ICatalogViewRouteParam } from "./catalog.route";
|
import type { ICatalogViewRouteParam } from "./catalog.route";
|
||||||
import { Notifications } from "../notifications";
|
import { Notifications } from "../notifications";
|
||||||
import { Avatar } from "../avatar/avatar";
|
import { Avatar } from "../avatar/avatar";
|
||||||
|
import { CatalogEntityDetails } from "./catalog-entity-details";
|
||||||
|
|
||||||
enum sortBy {
|
enum sortBy {
|
||||||
name = "name",
|
name = "name",
|
||||||
@ -55,6 +56,7 @@ export class Catalog extends React.Component<Props> {
|
|||||||
@observable private catalogEntityStore?: CatalogEntityStore;
|
@observable private catalogEntityStore?: CatalogEntityStore;
|
||||||
@observable private contextMenu: CatalogEntityContextMenuContext;
|
@observable private contextMenu: CatalogEntityContextMenuContext;
|
||||||
@observable activeTab?: string;
|
@observable activeTab?: string;
|
||||||
|
@observable selectedItem?: CatalogEntityItem;
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
@ -103,7 +105,7 @@ export class Catalog extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onDetails(item: CatalogEntityItem) {
|
onDetails(item: CatalogEntityItem) {
|
||||||
item.onRun(catalogEntityRunContext);
|
this.selectedItem = item;
|
||||||
}
|
}
|
||||||
|
|
||||||
onMenuItemClick(menuItem: CatalogEntityContextMenu) {
|
onMenuItemClick(menuItem: CatalogEntityContextMenu) {
|
||||||
@ -181,12 +183,6 @@ export class Catalog extends React.Component<Props> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
renderIcon(item: CatalogEntityItem) {
|
renderIcon(item: CatalogEntityItem) {
|
||||||
const category = catalogCategoryRegistry.getCategoryForEntity(item.entity);
|
|
||||||
|
|
||||||
if (!category) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Avatar
|
<Avatar
|
||||||
title={item.name}
|
title={item.name}
|
||||||
@ -269,6 +265,7 @@ export class Catalog extends React.Component<Props> {
|
|||||||
item.labels.map((label) => <Badge key={label} label={label} title={label} />),
|
item.labels.map((label) => <Badge key={label} label={label} title={label} />),
|
||||||
{ title: item.phase, className: kebabCase(item.phase) }
|
{ title: item.phase, className: kebabCase(item.phase) }
|
||||||
]}
|
]}
|
||||||
|
detailsItem={this.selectedItem}
|
||||||
onDetails={(item: CatalogEntityItem) => this.onDetails(item) }
|
onDetails={(item: CatalogEntityItem) => this.onDetails(item) }
|
||||||
renderItemMenu={this.renderItemMenu}
|
renderItemMenu={this.renderItemMenu}
|
||||||
/>
|
/>
|
||||||
@ -287,7 +284,15 @@ export class Catalog extends React.Component<Props> {
|
|||||||
provideBackButtonNavigation={false}
|
provideBackButtonNavigation={false}
|
||||||
contentGaps={false}>
|
contentGaps={false}>
|
||||||
{ this.catalogEntityStore.activeCategory ? this.renderSingleCategoryList() : this.renderAllCategoriesList() }
|
{ this.catalogEntityStore.activeCategory ? this.renderSingleCategoryList() : this.renderAllCategoriesList() }
|
||||||
<CatalogAddButton category={this.catalogEntityStore.activeCategory} />
|
{ !this.selectedItem && (
|
||||||
|
<CatalogAddButton category={this.catalogEntityStore.activeCategory} />
|
||||||
|
)}
|
||||||
|
{ this.selectedItem && (
|
||||||
|
<CatalogEntityDetails
|
||||||
|
entity={this.selectedItem.entity}
|
||||||
|
hideDetails={() => this.selectedItem = null}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,6 +39,7 @@ interface Props extends DOMAttributes<HTMLElement> {
|
|||||||
errorClass?: IClassName;
|
errorClass?: IClassName;
|
||||||
add: (item: CatalogEntity, index: number) => void;
|
add: (item: CatalogEntity, index: number) => void;
|
||||||
remove: (uid: string) => void;
|
remove: (uid: string) => void;
|
||||||
|
size?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
|
|||||||
@ -31,7 +31,7 @@ import { MaterialTooltip } from "../material-tooltip/material-tooltip";
|
|||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Avatar } from "../avatar/avatar";
|
import { Avatar } from "../avatar/avatar";
|
||||||
|
|
||||||
interface Props extends DOMAttributes<HTMLElement> {
|
export interface HotbarIconProps extends DOMAttributes<HTMLElement> {
|
||||||
uid: string;
|
uid: string;
|
||||||
title: string;
|
title: string;
|
||||||
source: string;
|
source: string;
|
||||||
@ -40,6 +40,7 @@ interface Props extends DOMAttributes<HTMLElement> {
|
|||||||
active?: boolean;
|
active?: boolean;
|
||||||
menuItems?: CatalogEntityContextMenu[];
|
menuItems?: CatalogEntityContextMenu[];
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
size?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMenuItemClick(menuItem: CatalogEntityContextMenu) {
|
function onMenuItemClick(menuItem: CatalogEntityContextMenu) {
|
||||||
@ -59,7 +60,7 @@ function onMenuItemClick(menuItem: CatalogEntityContextMenu) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HotbarIcon = observer(({menuItems = [], ...props}: Props) => {
|
export const HotbarIcon = observer(({menuItems = [], size = 40, ...props}: HotbarIconProps) => {
|
||||||
const { uid, title, active, className, source, disabled, onMenuOpen, children, ...rest } = props;
|
const { uid, title, active, className, source, disabled, onMenuOpen, children, ...rest } = props;
|
||||||
const id = `hotbarIcon-${uid}`;
|
const id = `hotbarIcon-${uid}`;
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
@ -77,8 +78,8 @@ export const HotbarIcon = observer(({menuItems = [], ...props}: Props) => {
|
|||||||
title={title}
|
title={title}
|
||||||
colorHash={`${title}-${source}`}
|
colorHash={`${title}-${source}`}
|
||||||
className={active ? "active" : "default"}
|
className={active ? "active" : "default"}
|
||||||
width={40}
|
width={size}
|
||||||
height={40}
|
height={size}
|
||||||
/>
|
/>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -157,6 +157,7 @@ export class HotbarMenu extends React.Component<Props> {
|
|||||||
className={cssNames({ isDragging: snapshot.isDragging })}
|
className={cssNames({ isDragging: snapshot.isDragging })}
|
||||||
remove={this.removeItem}
|
remove={this.removeItem}
|
||||||
add={this.addItem}
|
add={this.addItem}
|
||||||
|
size={40}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<HotbarIcon
|
<HotbarIcon
|
||||||
@ -165,6 +166,7 @@ export class HotbarMenu extends React.Component<Props> {
|
|||||||
source={item.entity.source}
|
source={item.entity.source}
|
||||||
menuItems={disabledMenuItems}
|
menuItems={disabledMenuItems}
|
||||||
disabled
|
disabled
|
||||||
|
size={40}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user