1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Use new EntityIcon for non-hotbar uses

- Allow executing entity's onRun from catalog

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2021-06-21 12:03:28 -04:00
parent baef6944aa
commit b56ce826b0
10 changed files with 208 additions and 132 deletions

View File

@ -28,7 +28,7 @@ import { action, makeObservable, observable, reaction, runInAction, when } from
import { CatalogEntityItem, CatalogEntityStore } from "./catalog-entity.store"; import { CatalogEntityItem, CatalogEntityStore } from "./catalog-entity.store";
import { navigate } from "../../navigation"; import { navigate } from "../../navigation";
import { MenuItem, MenuActions } from "../menu"; import { MenuItem, MenuActions } from "../menu";
import type { CatalogEntityContextMenu, CatalogEntityContextMenuContext } from "../../api/catalog-entity"; import { CatalogEntityContextMenu, CatalogEntityContextMenuContext, catalogEntityRunContext } from "../../api/catalog-entity";
import { HotbarStore } from "../../../common/hotbar-store"; import { HotbarStore } from "../../../common/hotbar-store";
import { ConfirmDialog } from "../confirm-dialog"; import { ConfirmDialog } from "../confirm-dialog";
import { catalogCategoryRegistry, CatalogEntity } from "../../../common/catalog"; import { catalogCategoryRegistry, CatalogEntity } from "../../../common/catalog";
@ -41,7 +41,7 @@ import { makeCss } from "../../../common/utils/makeCss";
import { CatalogEntityDetails } from "./catalog-entity-details"; import { CatalogEntityDetails } from "./catalog-entity-details";
import { catalogURL, CatalogViewRouteParam } from "../../../common/routes"; import { catalogURL, CatalogViewRouteParam } from "../../../common/routes";
import { CatalogMenu } from "./catalog-menu"; import { CatalogMenu } from "./catalog-menu";
import { HotbarIcon } from "../hotbar/hotbar-icon"; import { EntityIcon } from "../entity-icon";
export const previousActiveTab = createAppStorage("catalog-previous-active-tab", ""); export const previousActiveTab = createAppStorage("catalog-previous-active-tab", "");
@ -172,13 +172,14 @@ export class Catalog extends React.Component<Props> {
renderIcon(item: CatalogEntityItem<CatalogEntity>) { renderIcon(item: CatalogEntityItem<CatalogEntity>) {
return ( return (
<HotbarIcon <EntityIcon
uid={`catalog-icon-${item.getId()}`}
title={item.getName()} title={item.getName()}
source={item.source} source={item.source}
src={item.entity.spec.icon?.src} src={item.entity.spec.icon?.src}
material={item.entity.spec.icon?.material} material={item.entity.spec.icon?.material}
background={item.entity.spec.icon?.background} background={item.entity.spec.icon?.background}
onClick={() => item.onRun(catalogEntityRunContext)}
hoverWidth="1.5px"
size={24} size={24}
/> />
); );

View File

@ -25,7 +25,7 @@ import GraphemeSplitter from "grapheme-splitter";
import { Avatar as MaterialAvatar, AvatarTypeMap } from "@material-ui/core"; import { Avatar as MaterialAvatar, AvatarTypeMap } from "@material-ui/core";
import { iter } from "../../utils"; import { iter } from "../../utils";
interface Props extends DOMAttributes<HTMLElement>, Partial<AvatarTypeMap> { export interface AvatarProps extends DOMAttributes<HTMLElement>, Partial<AvatarTypeMap> {
title: string; title: string;
colorHash?: string; colorHash?: string;
width?: number; width?: number;
@ -33,6 +33,7 @@ interface Props extends DOMAttributes<HTMLElement>, Partial<AvatarTypeMap> {
src?: string; src?: string;
className?: string; className?: string;
background?: string; background?: string;
style?: React.CSSProperties;
} }
function getNameParts(name: string): string[] { function getNameParts(name: string): string[] {
@ -69,8 +70,8 @@ function getIconString(title: string) {
].filter(Boolean).join(""); ].filter(Boolean).join("");
} }
export function Avatar(props: Props) { export function Avatar(props: AvatarProps) {
const { title, width = 32, height = 32, colorHash, children, background, ...settings } = props; const { title, width = 32, height = 32, colorHash, children, style, background, ...settings } = props;
const getBackgroundColor = () => { const getBackgroundColor = () => {
if (background) { if (background) {
@ -89,7 +90,8 @@ export function Avatar(props: Props) {
backgroundColor: getBackgroundColor(), backgroundColor: getBackgroundColor(),
width, width,
height, height,
textTransform: "uppercase" textTransform: "uppercase",
...style,
}; };
}; };

View File

@ -0,0 +1,32 @@
/**
* 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.
*/
.EntityIcon {
&.active {
box-shadow: 0 0 0px var(--hover-width) var(--clusterMenuBackground), 0 0 0px calc(2*var(--hover-width)) var(--textColorAccent);
}
&:hover {
&:not(.active) {
box-shadow: 0 0 0px var(--hover-width) var(--clusterMenuBackground), 0 0 0px calc(2*var(--hover-width)) #ffffff50;
}
}
}

View File

@ -0,0 +1,56 @@
/**
* 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 "./entity-icon.scss";
import React, { DOMAttributes } from "react";
import { cssNames, IClassName } from "../../utils";
import { Avatar } from "../avatar/avatar";
import { Icon } from "../icon";
export interface EntityIconProps extends DOMAttributes<HTMLElement> {
title: string;
size?: number;
source: string;
src?: string;
material?: string;
background?: string;
active?: boolean;
className?: IClassName;
hoverWidth?: string;
}
export function EntityIcon({ active, size = 40, hoverWidth = "3px", material, className, ...props }: EntityIconProps) {
return (
<Avatar
width={size}
height={size}
colorHash={`${props.title}-${props.source}`}
className={cssNames("EntityIcon", className, active ? "active" : "default")}
style={{
"--hover-width": hoverWidth,
} as React.CSSProperties}
{...props}
>
{material && <Icon className="materialIcon" material={material} />}
</Avatar>
);
}

View File

@ -0,0 +1,22 @@
/**
* 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.
*/
export * from "./entity-icon";

View File

@ -31,6 +31,7 @@ import { cssNames, IClassName } from "../../utils";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { HotbarIcon } from "./hotbar-icon"; import { HotbarIcon } from "./hotbar-icon";
import { HotbarStore } from "../../../common/hotbar-store"; import { HotbarStore } from "../../../common/hotbar-store";
import { catalogEntityRunContext } from "../../api/catalog-entity";
interface Props extends DOMAttributes<HTMLElement> { interface Props extends DOMAttributes<HTMLElement> {
entity: CatalogEntity; entity: CatalogEntity;
@ -44,20 +45,16 @@ interface Props extends DOMAttributes<HTMLElement> {
@observer @observer
export class HotbarEntityIcon extends React.Component<Props> { export class HotbarEntityIcon extends React.Component<Props> {
@observable private contextMenu: CatalogEntityContextMenuContext; @observable private contextMenu: CatalogEntityContextMenuContext = {
menuItems: [],
navigate: (url: string) => navigate(url),
};
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
makeObservable(this); makeObservable(this);
} }
componentDidMount() {
this.contextMenu = {
menuItems: [],
navigate: (url: string) => navigate(url),
};
}
get kindIcon() { get kindIcon() {
const className = "badge"; const className = "badge";
const category = catalogCategoryRegistry.getCategoryForEntity(this.props.entity); const category = catalogCategoryRegistry.getCategoryForEntity(this.props.entity);
@ -92,20 +89,17 @@ export class HotbarEntityIcon extends React.Component<Props> {
} }
render() { render() {
if (!this.contextMenu) {
return null;
}
const { const {
entity, errorClass, add, remove, entity, errorClass, add, remove,
index, children, ...elemProps index, children, className, ...elemProps
} = this.props; } = this.props;
const className = cssNames("HotbarEntityIcon", this.props.className, { const classNames = cssNames("HotbarEntityIcon", className, {
interactive: true, interactive: true,
active: this.isActive(entity), active: this.isActive(entity),
disabled: !entity disabled: !entity
}); });
console.log(entity);
const isPersisted = this.isPersisted(entity); const isPersisted = this.isPersisted(entity);
const onOpen = async () => { const onOpen = async () => {
const menuItems: CatalogEntityContextMenu[] = []; const menuItems: CatalogEntityContextMenu[] = [];
@ -136,11 +130,12 @@ export class HotbarEntityIcon extends React.Component<Props> {
src={entity.spec.icon?.src} src={entity.spec.icon?.src}
material={entity.spec.icon?.material} material={entity.spec.icon?.material}
background={entity.spec.icon?.background} background={entity.spec.icon?.background}
className={className} className={classNames}
active={isActive} active={isActive}
onMenuOpen={onOpen} onMenuOpen={onOpen}
menuItems={this.contextMenu.menuItems} menuItems={this.contextMenu.menuItems}
tooltip={`${entity.metadata.name} (${entity.metadata.source})`} tooltip={`${entity.metadata.name} (${entity.metadata.source})`}
onClick={() => entity.onRun(catalogEntityRunContext)}
{...elemProps} {...elemProps}
> >
{ this.ledIcon } { this.ledIcon }

View File

@ -64,23 +64,6 @@
box-shadow: none; box-shadow: none;
} }
div.MuiAvatar-root {
width: var(--size);
height: var(--size);
&.active {
box-shadow: 0 0 0px 3px var(--clusterMenuBackground), 0 0 0px 6px var(--textColorAccent);
}
&.interactive {
&:hover {
&:not(.active) {
box-shadow: 0 0 0px 3px var(--clusterMenuBackground), 0 0 0px 6px #ffffff50;
}
}
}
}
.led { .led {
position: absolute; position: absolute;
left: 3px; left: 3px;

View File

@ -28,9 +28,8 @@ import { cssNames, IClassName } from "../../utils";
import { ConfirmDialog } from "../confirm-dialog"; import { ConfirmDialog } from "../confirm-dialog";
import { Menu, MenuItem } from "../menu"; import { Menu, MenuItem } from "../menu";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { Avatar } from "../avatar/avatar"; import { EntityIcon } from "../entity-icon";
import { Icon } from "../icon"; import { Tooltip } from "@material-ui/core";
import { Tooltip } from "../tooltip";
export interface HotbarIconProps extends DOMAttributes<HTMLElement> { export interface HotbarIconProps extends DOMAttributes<HTMLElement> {
uid: string; uid: string;
@ -74,34 +73,27 @@ export const HotbarIcon = observer(({menuItems = [], size = 40, tooltip, ...prop
setMenuOpen(!menuOpen); setMenuOpen(!menuOpen);
}; };
const renderIcon = () => {
return (
<Avatar
{...rest}
title={title}
colorHash={`${title}-${source}`}
className={cssNames(active ? "active" : "default", { interactive: !!onClick })}
width={size}
height={size}
src={src}
onClick={(event) => {
if (!disabled) {
onClick?.(event);
}
}}
>
{material && <Icon className="materialIcon" material={material}/>}
</Avatar>
);
};
return ( return (
<div className={cssNames("HotbarIcon flex", className, { disabled, contextMenuAvailable: menuItems.length > 0 })}> <div className={cssNames("HotbarIcon flex", className, { disabled })}>
{tooltip && <Tooltip targetId={id}>{tooltip}</Tooltip>} <Tooltip title={`${title || "unknown"} (${source || "unknown"})`} placement="right">
<div id={id}> <div id={id}>
{renderIcon()} <EntityIcon
{children} title={title}
</div> size={size}
active={active}
src={src}
source={source}
material={material}
onClick={event => {
if (!disabled) {
onClick?.(event);
}
}}
{...rest}
/>
{children}
</div>
</Tooltip>
<Menu <Menu
usePortal usePortal
htmlFor={id} htmlFor={id}

View File

@ -27,7 +27,7 @@ import { HotbarEntityIcon } from "./hotbar-entity-icon";
import { cssNames, IClassName } from "../../utils"; import { cssNames, IClassName } from "../../utils";
import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
import { defaultHotbarCells, HotbarItem, HotbarStore } from "../../../common/hotbar-store"; import { defaultHotbarCells, HotbarItem, HotbarStore } from "../../../common/hotbar-store";
import { CatalogEntity, catalogEntityRunContext } from "../../api/catalog-entity"; import type { CatalogEntity } from "../../api/catalog-entity";
import { DragDropContext, Draggable, Droppable, DropResult } from "react-beautiful-dnd"; import { DragDropContext, Draggable, Droppable, DropResult } from "react-beautiful-dnd";
import { HotbarSelector } from "./hotbar-selector"; import { HotbarSelector } from "./hotbar-selector";
import { HotbarCell } from "./hotbar-cell"; import { HotbarCell } from "./hotbar-cell";
@ -126,51 +126,45 @@ export class HotbarMenu extends React.Component<Props> {
> >
{item && ( {item && (
<Draggable draggableId={item.entity.uid} key={item.entity.uid} index={0} > <Draggable draggableId={item.entity.uid} key={item.entity.uid} index={0} >
{(provided, snapshot) => { {(provided, snapshot) => (
const style = { <div
zIndex: defaultHotbarCells - index, key={item.entity.uid}
position: "absolute", ref={provided.innerRef}
...provided.draggableProps.style, {...provided.draggableProps}
} as React.CSSProperties; {...provided.dragHandleProps}
style={{
return ( zIndex: defaultHotbarCells - index,
<div position: "absolute",
key={item.entity.uid} ...provided.draggableProps.style,
ref={provided.innerRef} }}
{...provided.draggableProps} >
{...provided.dragHandleProps} {entity ? (
style={style} <HotbarEntityIcon
> key={index}
{entity ? ( index={index}
<HotbarEntityIcon entity={entity}
key={index} className={cssNames({ isDragging: snapshot.isDragging })}
index={index} remove={this.removeItem}
entity={entity} add={this.addItem}
onClick={() => entity.onRun(catalogEntityRunContext)} size={40}
className={cssNames({ isDragging: snapshot.isDragging })} />
remove={this.removeItem} ) : (
add={this.addItem} <HotbarIcon
size={40} uid={item.entity.uid}
/> title={item.entity.name}
) : ( source={item.entity.source}
<HotbarIcon menuItems={[
uid={`hotbar-icon-${item.entity.uid}`} {
title={item.entity.name} title: "Unpin from Hotbar",
source={item.entity.source} onClick: () => this.removeItem(item.entity.uid)
tooltip={`${item.entity.name} (${item.entity.source})`} }
menuItems={[ ]}
{ disabled
title: "Unpin from Hotbar", size={40}
onClick: () => this.removeItem(item.entity.uid) />
} )}
]} </div>
disabled )}
size={40}
/>
)}
</div>
);
}}
</Draggable> </Draggable>
)} )}
{provided.placeholder} {provided.placeholder}

View File

@ -21,25 +21,24 @@
// Helper for combining css classes inside components // Helper for combining css classes inside components
export type IClassName = string | string[] | IClassNameMap; export type IClassName = string | string[] | Record<string, any> | undefined | null;
export type IClassNameMap = {
[className: string]: boolean | any;
};
export function cssNames(...args: IClassName[]): string { export function cssNames(...args: IClassName[]): string {
const map: IClassNameMap = {}; const names: string[] = [];
args.forEach(className => { for (const arg of args) {
if (typeof className === "string" || Array.isArray(className)) { if (typeof arg === "string") {
[].concat(className).forEach(name => map[name] = true); names.push(arg.trim());
} else if (Array.isArray(arg)) {
names.push(...arg.map(name => name.trim()));
} else if (arg && typeof arg === "object") {
for (const [name, isActive] of Object.entries(arg)) {
if (isActive) {
names.push(name.trim());
}
}
} }
else { }
Object.assign(map, className);
}
});
return Object.entries(map) return names.filter(Boolean).join(" ");
.filter(([, isActive]) => !!isActive)
.map(([className]) => className.trim())
.join(" ");
} }