From d2490f5aa9feff904f80e9741e11ff1284cee999 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pol=20Amor=C3=B3s?= Date: Wed, 29 May 2024 15:30:20 +0200 Subject: [PATCH] feat(console): icon for custom resources (#6588) Resolves https://github.com/winglang/wing/issues/6587 --- .../design-system/src/resource-icon.tsx | 2 + .../design-system/src/utils/icon-utils.ts | 106 +++++++++--------- .../console/server/src/router/app.ts | 2 + .../server/src/utils/constructTreeNodeMap.ts | 1 + .../console/ui/src/features/map-view.tsx | 21 +++- .../console/ui/src/services/use-explorer.tsx | 1 + .../console/ui/src/ui/edge-metadata.tsx | 2 + .../console/ui/src/ui/explorer.tsx | 1 + .../console/ui/src/ui/resource-metadata.tsx | 17 ++- docs/docs/04-standard-library/std/node.md | 18 +++ libs/wingsdk/src/core/tree.ts | 9 +- libs/wingsdk/src/std/node.ts | 9 ++ 12 files changed, 131 insertions(+), 58 deletions(-) diff --git a/apps/wing-console/console/design-system/src/resource-icon.tsx b/apps/wing-console/console/design-system/src/resource-icon.tsx index 84f4bf426cf..62a6aa65b0e 100644 --- a/apps/wing-console/console/design-system/src/resource-icon.tsx +++ b/apps/wing-console/console/design-system/src/resource-icon.tsx @@ -16,6 +16,7 @@ export interface ResourceIconProps extends IconProps { forceDarken?: boolean; solid?: boolean; color?: Colors | string; + icon?: string; } export interface IconComponent extends FunctionComponent {} @@ -32,6 +33,7 @@ export const ResourceIcon = ({ const Component = getResourceIconComponent(resourceType, { solid, resourceId: resourcePath, + icon: props.icon, }); const colors = getResourceIconColors({ resourceType, diff --git a/apps/wing-console/console/design-system/src/utils/icon-utils.ts b/apps/wing-console/console/design-system/src/utils/icon-utils.ts index c5d5ab472f4..ef00163d5c5 100644 --- a/apps/wing-console/console/design-system/src/utils/icon-utils.ts +++ b/apps/wing-console/console/design-system/src/utils/icon-utils.ts @@ -1,32 +1,6 @@ -import { - ArchiveBoxIcon, - BeakerIcon, - BoltIcon, - CalculatorIcon, - ClockIcon, - CloudIcon, - CubeIcon, - GlobeAltIcon, - MegaphoneIcon, - QueueListIcon, - TableCellsIcon, - KeyIcon, -} from "@heroicons/react/24/outline"; -import { - ArchiveBoxIcon as SolidArchiveBoxIcon, - BeakerIcon as SolidBeakerIcon, - BoltIcon as SolidBoltIcon, - CalculatorIcon as SolidCalculatorIcon, - ClockIcon as SolidClockIcon, - CloudIcon as SolidCloudIcon, - GlobeAltIcon as SolidGlobeAltIcon, - MegaphoneIcon as SolidMegaphoneIcon, - QueueListIcon as SolidQueueListIcon, - TableCellsIcon as SolidTableCellsIcon, - KeyIcon as SolidKeyIcon, -} from "@heroicons/react/24/solid"; +import * as OutlineHeroIcons from "@heroicons/react/24/outline"; +import * as SolidHeroIcons from "@heroicons/react/24/solid"; -import { ReactIcon } from "../icons/react-icon.js"; import { RedisIcon } from "../icons/redis-icon.js"; import type { Colors } from "./colors.js"; @@ -38,52 +12,75 @@ const matchTest = (path: string) => { return isTest.test(path); }; +const getHeroIconName = (heroiconId: string): string => { + const parts = heroiconId.split("-"); + const resourceName = parts + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(""); + return `${resourceName}Icon`; +}; + export const getResourceIconComponent = ( resourceType: string | undefined, - { solid = true, resourceId }: { solid?: boolean; resourceId?: string } = {}, + { + solid = true, + resourceId, + icon, + }: { solid?: boolean; resourceId?: string; icon?: string } = {}, ) => { + const iconSet = solid ? SolidHeroIcons : OutlineHeroIcons; + if (resourceId && matchTest(resourceId)) { - return solid ? SolidBeakerIcon : BeakerIcon; + return iconSet.BeakerIcon; } + if (icon) { + // icon is a heroicon id string, e.g. "academic-cap" so we need to convert it to the actual component + // @ts-ignore + let iconComponent = iconSet[getHeroIconName(icon)]; + if (iconComponent) { + return iconComponent; + } + } + switch (resourceType) { case "@winglang/sdk.cloud.Bucket": { - return solid ? SolidArchiveBoxIcon : ArchiveBoxIcon; + return iconSet.ArchiveBoxIcon; } case "@winglang/sdk.cloud.Function": { - return solid ? SolidBoltIcon : BoltIcon; + return iconSet.BoltIcon; } case "@winglang/sdk.cloud.Queue": { - return solid ? SolidQueueListIcon : QueueListIcon; + return iconSet.QueueListIcon; } case "@winglang/sdk.cloud.Website": { - return solid ? SolidGlobeAltIcon : GlobeAltIcon; + return iconSet.GlobeAltIcon; } case "@winglang/sdk.cloud.Counter": { - return solid ? SolidCalculatorIcon : CalculatorIcon; + return iconSet.CalculatorIcon; } case "@winglang/sdk.cloud.Topic": { - return solid ? SolidMegaphoneIcon : MegaphoneIcon; + return iconSet.MegaphoneIcon; } case "@winglang/sdk.cloud.Api": { - return solid ? SolidCloudIcon : CloudIcon; + return iconSet.CloudIcon; } case "@winglang/sdk.ex.Table": { - return solid ? SolidTableCellsIcon : TableCellsIcon; + return iconSet.TableCellsIcon; } case "@winglang/sdk.cloud.Schedule": { - return solid ? SolidClockIcon : ClockIcon; + return iconSet.ClockIcon; } case "@winglang/sdk.ex.Redis": { return RedisIcon; } case "@winglang/sdk.std.Test": { - return solid ? SolidBeakerIcon : BeakerIcon; + return iconSet.BeakerIcon; } case "@winglang/sdk.cloud.Secret": { - return solid ? SolidKeyIcon : KeyIcon; + return iconSet.KeyIcon; } default: { - return CubeIcon; + return iconSet.CubeIcon; } } }; @@ -159,6 +156,21 @@ export const getResourceIconColors = (options: { forceDarken?: boolean; color?: Colors | string; }) => { + let color: Colors = + options.color && Object.keys(colors).includes(options.color) + ? (options.color as Colors) + : "slate"; + + let defaultColor = [ + colors[color].default, + options.darkenOnGroupHover && colors[color].groupHover, + options.forceDarken && colors[color].forceDarken, + ]; + + if (options.color) { + return defaultColor; + } + switch (options.resourceType) { case "@winglang/sdk.cloud.Bucket": { return [ @@ -231,15 +243,7 @@ export const getResourceIconColors = (options: { ]; } default: { - let color: Colors = - options.color && Object.keys(colors).includes(options.color) - ? (options.color as Colors) - : "slate"; - return [ - colors[color].default, - options.darkenOnGroupHover && colors[color].groupHover, - options.forceDarken && colors[color].forceDarken, - ]; + return defaultColor; } } }; diff --git a/apps/wing-console/console/server/src/router/app.ts b/apps/wing-console/console/server/src/router/app.ts index 9af6c579345..d940ed5561e 100644 --- a/apps/wing-console/console/server/src/router/app.ts +++ b/apps/wing-console/console/server/src/router/app.ts @@ -320,11 +320,13 @@ export const createAppRouter = () => { id: sourceNode.id, path: sourceNode.path, type: getResourceType(sourceNode, simulator), + display: sourceNode.display, }, target: { id: targetNode?.id ?? "", path: targetNode?.path ?? "", type: (targetNode && getResourceType(targetNode, simulator)) ?? "", + display: targetNode.display, }, inflights: targetInflight ? [ diff --git a/apps/wing-console/console/server/src/utils/constructTreeNodeMap.ts b/apps/wing-console/console/server/src/utils/constructTreeNodeMap.ts index f54e324cf2c..e1a620f4d1d 100644 --- a/apps/wing-console/console/server/src/utils/constructTreeNodeMap.ts +++ b/apps/wing-console/console/server/src/utils/constructTreeNodeMap.ts @@ -6,6 +6,7 @@ export interface NodeDisplay { sourceModule?: string; hidden?: boolean; color?: string; + icon?: string; } export interface NodeConnection { diff --git a/apps/wing-console/console/ui/src/features/map-view.tsx b/apps/wing-console/console/ui/src/features/map-view.tsx index 50bb34f436f..bfc5d18a614 100644 --- a/apps/wing-console/console/ui/src/features/map-view.tsx +++ b/apps/wing-console/console/ui/src/features/map-view.tsx @@ -48,10 +48,12 @@ interface WrapperProps { fqn: string; highlight?: boolean; onClick?: () => void; + color?: string; + icon?: string; } const Wrapper: FunctionComponent> = memo( - ({ name, fqn, highlight, onClick, children }) => { + ({ name, fqn, highlight, onClick, children, color, icon }) => { return ( // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
> = memo( "border-b border-slate-200 dark:border-slate-800", )} > - + void; + color?: string; + icon?: string; } const ContainerNode: FunctionComponent> = @@ -127,6 +136,8 @@ const ContainerNode: FunctionComponent> = fqn={props.resourceType!} highlight={props.highlight} onClick={props.onClick} + color={props.color} + icon={props.icon} >
@@ -153,6 +164,7 @@ interface ConstructNodeProps { hasChildNodes?: boolean; onSelectedNodeIdChange: (id: string | undefined) => void; color?: string; + icon?: string; } const ConstructNode: FunctionComponent> = @@ -167,6 +179,7 @@ const ConstructNode: FunctionComponent> = children, hasChildNodes, color, + icon, }) => { const select = useCallback( () => onSelectedNodeIdChange(id), @@ -243,6 +256,7 @@ const ConstructNode: FunctionComponent> = className="size-4 -ml-0.5" resourceType={fqn} color={color} + icon={icon} /> > = resourceType={fqn} highlight={highlight} onClick={select} + color={color} + icon={icon} > {inflights.length > 0 && renderedNode} @@ -594,6 +610,7 @@ export const MapView = memo( name={name ?? ""} fqn={fqn ?? ""} color={props.constructTreeNode.display?.color} + icon={props.constructTreeNode.display?.icon} inflights={info.type === "construct" ? info.inflights : []} onSelectedNodeIdChange={props.onSelectedNodeIdChange} highlight={props.selectedNodeId === props.constructTreeNode.path} diff --git a/apps/wing-console/console/ui/src/services/use-explorer.tsx b/apps/wing-console/console/ui/src/services/use-explorer.tsx index 5b7c1d9ed3b..357b8a6f1d6 100644 --- a/apps/wing-console/console/ui/src/services/use-explorer.tsx +++ b/apps/wing-console/console/ui/src/services/use-explorer.tsx @@ -19,6 +19,7 @@ const createTreeMenuItemFromExplorerTreeItem = ( resourcePath={item.id} className="w-4 h-4" color={item.display?.color} + icon={item.display?.icon} /> ) : undefined, children: item.childItems?.map((item) => diff --git a/apps/wing-console/console/ui/src/ui/edge-metadata.tsx b/apps/wing-console/console/ui/src/ui/edge-metadata.tsx index eea537edac3..2812a35e4cd 100644 --- a/apps/wing-console/console/ui/src/ui/edge-metadata.tsx +++ b/apps/wing-console/console/ui/src/ui/edge-metadata.tsx @@ -110,6 +110,7 @@ export const EdgeMetadata = ({ resourceType={source.type} resourcePath={source.path} color={source.display?.color} + icon={source.display?.icon} />
{source.id}
@@ -134,6 +135,7 @@ export const EdgeMetadata = ({ resourceType={target.type} resourcePath={target.path} color={target.display?.color} + icon={target.display?.icon} />
{target.id}
diff --git a/apps/wing-console/console/ui/src/ui/explorer.tsx b/apps/wing-console/console/ui/src/ui/explorer.tsx index 0018818af70..c9ef698d781 100644 --- a/apps/wing-console/console/ui/src/ui/explorer.tsx +++ b/apps/wing-console/console/ui/src/ui/explorer.tsx @@ -45,6 +45,7 @@ const createTreeMenuItemFromExplorerTreeItem = ( resourcePath={item.label} className="w-4 h-4" color={item.display?.color} + icon={item.display?.icon} /> ) : undefined, children: item.childItems?.map((item) => diff --git a/apps/wing-console/console/ui/src/ui/resource-metadata.tsx b/apps/wing-console/console/ui/src/ui/resource-metadata.tsx index ad19637c385..0c155468164 100644 --- a/apps/wing-console/console/ui/src/ui/resource-metadata.tsx +++ b/apps/wing-console/console/ui/src/ui/resource-metadata.tsx @@ -17,6 +17,7 @@ import { getResourceIconComponent, Attribute, ScrollableArea, + getResourceIconColors, } from "@wingconsole/design-system"; import type { NodeDisplay } from "@wingconsole/server"; import classNames from "classnames"; @@ -82,13 +83,18 @@ export const ResourceMetadata = memo( "interact", "interact-actions", ]); + + const icon = useMemo(() => { + return getResourceIconComponent(node.type, { + resourceId: node.id, + icon: node.display?.icon, + }); + }, [node]); + const { resourceGroup, connectionsGroups } = useMemo(() => { const connectionsGroupsArray: ConnectionsGroup[] = []; let resourceGroup: AttributeGroup | undefined; if (node.props) { - const icon = getResourceIconComponent(node.type, { - resourceId: node.id, - }); switch (node.type) { case "@winglang/sdk.cloud.Function": { resourceGroup = { @@ -181,6 +187,7 @@ export const ResourceMetadata = memo( resourcePath={relationship.path} className="w-4 h-4" color={relationship.display?.color} + icon={relationship.display?.icon} /> ), })), @@ -199,6 +206,7 @@ export const ResourceMetadata = memo( resourcePath={relationship.path} className="w-4 h-4" color={relationship.display?.color} + icon={relationship.display?.icon} /> ), })), @@ -247,6 +255,7 @@ export const ResourceMetadata = memo( resourceType={node.type} resourcePath={node.path} color={node.display?.color} + icon={node.display?.icon} /> @@ -259,7 +268,7 @@ export const ResourceMetadata = memo( {resourceUI.data && resourceUI.data.length > 0 && ( toggleInspectorSection("resourceUI")} diff --git a/docs/docs/04-standard-library/std/node.md b/docs/docs/04-standard-library/std/node.md index d229a827d5a..4036320d9a0 100644 --- a/docs/docs/04-standard-library/std/node.md +++ b/docs/docs/04-standard-library/std/node.md @@ -273,6 +273,7 @@ Invokes the `validate()` method on all validations added through | defaultChild | constructs.IConstruct | Returns the child construct that has the id `Default` or `Resource"`. | | description | str | Description of the construct for display purposes. | | hidden | bool | Whether the construct should be hidden by default in tree visualizations. | +| icon | str | The icon of the construct for display purposes. | | sourceModule | str | The source file or library where the construct was defined. | | title | str | Title of the construct for display purposes. | @@ -511,6 +512,23 @@ Whether the construct should be hidden by default in tree visualizations. --- +##### `icon`Optional + +```wing +icon: str; +``` + +- *Type:* str + +The icon of the construct for display purposes. + +Supported icons are from Heroicons: +- https://heroicons.com/ +e.g. +- "academic-cap" + +--- + ##### `sourceModule`Optional ```wing diff --git a/libs/wingsdk/src/core/tree.ts b/libs/wingsdk/src/core/tree.ts index 274db0bdad6..669fffd7ef4 100644 --- a/libs/wingsdk/src/core/tree.ts +++ b/libs/wingsdk/src/core/tree.ts @@ -81,6 +81,11 @@ export interface DisplayInfo { * The color of the resource in the UI. */ readonly color?: Colors; + + /** + * The icon of the resource in the UI. + */ + readonly icon?: string; } /** @internal */ @@ -241,7 +246,8 @@ function synthDisplay(construct: IConstruct): DisplayInfo | undefined { display.title || display.hidden || ui || - display.color + display.color || + display.icon ) { return { title: display.title, @@ -250,6 +256,7 @@ function synthDisplay(construct: IConstruct): DisplayInfo | undefined { sourceModule: display.sourceModule, ui: ui.length > 0 ? ui : undefined, color: isOfTypeColors(display.color) ? display.color : undefined, + icon: display.icon, }; } return; diff --git a/libs/wingsdk/src/std/node.ts b/libs/wingsdk/src/std/node.ts index 9a1877202db..778f49ef316 100644 --- a/libs/wingsdk/src/std/node.ts +++ b/libs/wingsdk/src/std/node.ts @@ -80,6 +80,15 @@ export class Node { */ public color?: string; + /** + * The icon of the construct for display purposes. + * Supported icons are from Heroicons: + * - https://heroicons.com/ + * e.g. + * - "academic-cap" + */ + public icon?: string; + private readonly _constructsNode: ConstructsNode; private readonly _connections: Connections; private _app: IApp | undefined;