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

View File

@ -25,7 +25,7 @@ import GraphemeSplitter from "grapheme-splitter";
import { Avatar as MaterialAvatar, AvatarTypeMap } from "@material-ui/core";
import { iter } from "../../utils";
interface Props extends DOMAttributes<HTMLElement>, Partial<AvatarTypeMap> {
export interface AvatarProps extends DOMAttributes<HTMLElement>, Partial<AvatarTypeMap> {
title: string;
colorHash?: string;
width?: number;
@ -33,6 +33,7 @@ interface Props extends DOMAttributes<HTMLElement>, Partial<AvatarTypeMap> {
src?: string;
className?: string;
background?: string;
style?: React.CSSProperties;
}
function getNameParts(name: string): string[] {
@ -69,8 +70,8 @@ function getIconString(title: string) {
].filter(Boolean).join("");
}
export function Avatar(props: Props) {
const { title, width = 32, height = 32, colorHash, children, background, ...settings } = props;
export function Avatar(props: AvatarProps) {
const { title, width = 32, height = 32, colorHash, children, style, background, ...settings } = props;
const getBackgroundColor = () => {
if (background) {
@ -89,7 +90,8 @@ export function Avatar(props: Props) {
backgroundColor: getBackgroundColor(),
width,
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 { HotbarIcon } from "./hotbar-icon";
import { HotbarStore } from "../../../common/hotbar-store";
import { catalogEntityRunContext } from "../../api/catalog-entity";
interface Props extends DOMAttributes<HTMLElement> {
entity: CatalogEntity;
@ -44,20 +45,16 @@ interface Props extends DOMAttributes<HTMLElement> {
@observer
export class HotbarEntityIcon extends React.Component<Props> {
@observable private contextMenu: CatalogEntityContextMenuContext;
@observable private contextMenu: CatalogEntityContextMenuContext = {
menuItems: [],
navigate: (url: string) => navigate(url),
};
constructor(props: Props) {
super(props);
makeObservable(this);
}
componentDidMount() {
this.contextMenu = {
menuItems: [],
navigate: (url: string) => navigate(url),
};
}
get kindIcon() {
const className = "badge";
const category = catalogCategoryRegistry.getCategoryForEntity(this.props.entity);
@ -92,20 +89,17 @@ export class HotbarEntityIcon extends React.Component<Props> {
}
render() {
if (!this.contextMenu) {
return null;
}
const {
entity, errorClass, add, remove,
index, children, ...elemProps
index, children, className, ...elemProps
} = this.props;
const className = cssNames("HotbarEntityIcon", this.props.className, {
const classNames = cssNames("HotbarEntityIcon", className, {
interactive: true,
active: this.isActive(entity),
disabled: !entity
});
console.log(entity);
const isPersisted = this.isPersisted(entity);
const onOpen = async () => {
const menuItems: CatalogEntityContextMenu[] = [];
@ -136,11 +130,12 @@ export class HotbarEntityIcon extends React.Component<Props> {
src={entity.spec.icon?.src}
material={entity.spec.icon?.material}
background={entity.spec.icon?.background}
className={className}
className={classNames}
active={isActive}
onMenuOpen={onOpen}
menuItems={this.contextMenu.menuItems}
tooltip={`${entity.metadata.name} (${entity.metadata.source})`}
onClick={() => entity.onRun(catalogEntityRunContext)}
{...elemProps}
>
{ this.ledIcon }

View File

@ -64,23 +64,6 @@
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 {
position: absolute;
left: 3px;

View File

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

View File

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

View File

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