Skip to content

Commit b88416a

Browse files
committed
feat(ui): Add Canvas UI support for nested children
1 parent 848664c commit b88416a

26 files changed

+934
-424
lines changed

ui/src/api/flow.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,5 @@ export interface RouterKey {
4242
name: string
4343
attributes?: Attributes
4444
}
45+
46+
export const DEFAULT_NAMESPACE = "integration"
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import {
2+
Button,
3+
Popover,
4+
PopoverContent,
5+
TreeNode,
6+
TreeNodeProps,
7+
TreeView,
8+
} from "@carbon/react"
9+
import { ParentChild } from "@carbon/react/icons"
10+
import { ReactElement, useState } from "react"
11+
import { useNodeId } from "reactflow"
12+
import { updateSelectedChildNode } from "../../singletons/store/appActions"
13+
import {
14+
getEipId,
15+
getEnabledChildrenView,
16+
getSelectedChildNode,
17+
} from "../../singletons/store/storeViews"
18+
19+
const renderTree = (idPath: string[]): ReactElement<TreeNodeProps> => {
20+
const id = idPath[idPath.length - 1]
21+
const children = getEnabledChildrenView(id)
22+
23+
return (
24+
<TreeNode
25+
className={"child-tree-view__node"}
26+
key={id}
27+
id={id}
28+
label={getEipId(id)?.name}
29+
onSelect={() => updateSelectedChildNode(idPath)}
30+
>
31+
{children && children.length > 0
32+
? children.map((childId) => renderTree([...idPath, childId]))
33+
: null}
34+
</TreeNode>
35+
)
36+
}
37+
38+
const ChildTree = () => {
39+
const rootId = useNodeId()
40+
41+
if (!rootId) {
42+
return null
43+
}
44+
45+
return (
46+
<div>
47+
<TreeView
48+
className={"child-tree-view"}
49+
label={"children-menu"}
50+
hideLabel
51+
size="xs"
52+
>
53+
{renderTree([rootId]).props.children}
54+
</TreeView>
55+
</div>
56+
)
57+
}
58+
59+
export const ChildrenNavigationPopover = () => {
60+
const [open, setOpen] = useState(false)
61+
62+
return (
63+
<Popover
64+
className="eip-children-popover"
65+
highContrast
66+
open={open}
67+
// Keep popover open when interacting with node config side panel
68+
onRequestClose={() => !getSelectedChildNode() && setOpen(false)}
69+
onKeyDown={(ev: React.KeyboardEvent) =>
70+
ev.key === "Escape" && setOpen(false)
71+
}
72+
>
73+
<Button
74+
className="eip-children-popover__button nodrag"
75+
hasIconOnly
76+
iconDescription="children"
77+
kind="primary"
78+
renderIcon={ParentChild}
79+
size="sm"
80+
tooltipPosition="left"
81+
onClick={() => setOpen(!open)}
82+
/>
83+
<PopoverContent>
84+
<ChildTree />
85+
</PopoverContent>
86+
</Popover>
87+
)
88+
}

ui/src/components/canvas/EipNode.tsx

Lines changed: 8 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,20 @@
1-
import { Button, Stack, Tile } from "@carbon/react"
2-
import { ServiceId } from "@carbon/react/icons"
1+
import { Tile } from "@carbon/react"
2+
33
import { Handle, NodeProps, Position } from "reactflow"
44
import { EipNodeData, Layout } from "../../api/flow"
55
import { ConnectionType, EipRole } from "../../api/generated/eipComponentDef"
6-
import { EipId } from "../../api/generated/eipFlow"
76
import { lookupEipComponent } from "../../singletons/eipDefinitions"
87
import getIconUrl from "../../singletons/eipIconCatalog"
9-
import {
10-
clearSelectedChildNode,
11-
updateSelectedChildNode,
12-
} from "../../singletons/store/appActions"
8+
import { clearSelectedChildNode } from "../../singletons/store/appActions"
139
import {
1410
useGetEnabledChildren,
1511
useGetLayout,
16-
useGetSelectedChildNode,
1712
} from "../../singletons/store/getterHooks"
1813
import { getEipId } from "../../singletons/store/storeViews"
19-
import { toTitleCase } from "../../utils/titleTransform"
14+
import { getNamespacedTitle } from "../../utils/titleTransform"
15+
import { ChildrenNavigationPopover } from "./ChildrenNavigationPopover"
2016
import "./nodes.scss"
2117

22-
interface ChildrenIconsProps {
23-
childIds: string[]
24-
}
25-
26-
interface ChildIconButtonProps {
27-
id: string
28-
}
29-
30-
const DEFAULT_NAMESPACE = "integration"
3118
const DEFAULT_NODE_LABEL = "New Node"
3219

3320
const renderHorizontalHandles = (connectionType: ConnectionType) => {
@@ -91,64 +78,19 @@ const renderHandles = (
9178
? renderHorizontalHandles(connectionType)
9279
: renderVerticalHandles(connectionType)
9380

94-
const getNamespacedTitle = (eipId: EipId) => {
95-
if (eipId.namespace === DEFAULT_NAMESPACE) {
96-
return toTitleCase(eipId.name)
97-
}
98-
return toTitleCase(eipId.namespace) + " " + toTitleCase(eipId.name)
99-
}
100-
10181
const getClassNames = (props: NodeProps<EipNodeData>, role: EipRole) => {
10282
const roleClsName =
10383
role === "channel" ? "eip-channel-node" : "eip-endpoint-node"
10484
const selectedClsName = props.selected ? "eip-node-selected" : ""
10585
return ["eip-node", roleClsName, selectedClsName].join(" ")
10686
}
10787

108-
const ChildIconButton = (props: ChildIconButtonProps) => {
109-
const currSelection = useGetSelectedChildNode()
110-
const isSelected = currSelection === props.id
111-
112-
const clsNames = ["child-icon-button"]
113-
isSelected && clsNames.push("child-icon-button-focused")
114-
115-
const eipId = getEipId(props.id)
116-
117-
return eipId ? (
118-
<Button
119-
className={clsNames.join(" ")}
120-
hasIconOnly
121-
renderIcon={ServiceId}
122-
iconDescription={eipId.name}
123-
size="sm"
124-
tooltipPosition="bottom"
125-
kind="primary"
126-
onClick={(ev) => {
127-
ev.stopPropagation()
128-
updateSelectedChildNode(props.id)
129-
}}
130-
/>
131-
) : null
132-
}
133-
134-
// TODO: Account for a large number of children to be displayed
135-
// TODO: Create a mapping of children to icons (with a fallback option)
136-
const ChildrenIcons = ({ childIds }: ChildrenIconsProps) => {
137-
return (
138-
<Stack className="eip-node-children" orientation="horizontal" gap={2}>
139-
{childIds.map((id) => (
140-
<ChildIconButton key={id} id={id} />
141-
))}
142-
</Stack>
143-
)
144-
}
145-
14688
// TODO: Consider separating into Endpoint and Channel custom node types
14789
export const EipNode = (props: NodeProps<EipNodeData>) => {
14890
// TODO: clearSelectedChildNode is used in too many different components. See if that can be reduced (or elimnated).
14991
const layout = useGetLayout()
150-
const childrenState = useGetEnabledChildren(props.id)
151-
const hasChildren = childrenState.length > 0
92+
const children = useGetEnabledChildren(props.id)
93+
const hasChildren = children.length > 0
15294

15395
const eipId = getEipId(props.id)
15496
const componentDefinition = eipId && lookupEipComponent(eipId)
@@ -176,7 +118,7 @@ export const EipNode = (props: NodeProps<EipNodeData>) => {
176118
>
177119
<strong>{data.label || DEFAULT_NODE_LABEL}</strong>
178120
</div>
179-
{hasChildren && <ChildrenIcons childIds={childrenState} />}
121+
{hasChildren && <ChildrenNavigationPopover />}
180122
{handles}
181123
</Tile>
182124
)

ui/src/components/canvas/nodes.scss

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,25 +22,28 @@ $icon-button-focus-shadow:
2222
background-color: themes.$layer-hover-02;
2323
}
2424

25-
.eip-node-children {
25+
.eip-children-popover {
2626
position: absolute;
2727
left: layout.$spacing-02;
2828
}
2929

30-
.eip-node-children .child-icon-button {
30+
.eip-children-popover__button {
3131
border-radius: 100%;
3232
padding: 0;
3333
align-items: center;
3434
}
3535

36-
.eip-node-children .child-icon-button:focus {
37-
box-shadow: $icon-button-focus-shadow;
36+
.child-tree-view {
37+
inline-size: 10rem;
38+
max-block-size: 15rem;
39+
overflow-y: auto;
3840
}
3941

40-
// Used from React to control appearance of a focused button
41-
.eip-node-children .child-icon-button-focused {
42-
box-shadow: $icon-button-focus-shadow;
42+
43+
.child-tree-view__node {
44+
background-color: themes.$background;
4345
}
46+
4447
}
4548

4649
.eip-node-selected {
@@ -55,6 +58,7 @@ $icon-button-focus-shadow:
5558
overflow-wrap: break-word;
5659
max-height: 3rem;
5760
overflow-y: hidden;
61+
margin-bottom: layout.$spacing-02;
5862
}
5963

6064
.eip-endpoint-node .eip-node-image {

ui/src/components/config-panel/AttributeConfigForm.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { useGetEipAttribute } from "../../singletons/store/getterHooks"
1818
import debounce from "../../utils/debounce"
1919
import DescriptionTooltipWrapper from "./DescriptionTooltipWrapper"
2020

21-
const addPaddingClass = "cfg-panel__container__padding-add"
21+
const addPaddingClass = "cfg-panel__container__side-padding-add"
2222

2323
interface AttributeInputFactoryProps {
2424
attr: Attribute

0 commit comments

Comments
 (0)