From 7b269651a78a659535243efb5914c330561ecb9d Mon Sep 17 00:00:00 2001 From: Jenny <32821331+jenny-s51@users.noreply.github.com> Date: Mon, 4 Mar 2024 15:35:50 -0500 Subject: [PATCH 1/9] add collapsible and expandable logic styling wip update classnames wip add proper styling, wip hover clean up unused vars, add drop shadow to action icon remove css class to fix build fix :hot build add correct styling to collapsible groups revert action label styling to default, adds isIconExternal prop add collapsible groups to subgroups layout add ref to revert to default label styling add withSelection, update padding clean up css use PF vars for css styles updates per Jeff, WIP remove data from group nodes PR feedback from Jeff: add correct params to PipelinesDefaultGroupExpanded, wrap label with layer add props to fix build fix label position remove custom css in favor of existing pf styles PR feedback from Jeff, apply pill styling to collapsed group fix build --- .../pipelineGroupsDemo/DemoTaskGroup.tsx | 34 ++- .../createDemoPipelineGroupsNodes.ts | 2 +- .../pipelineGroupsComponentFactory.tsx | 2 +- .../pipelinesDemo/DemoPipelinesGroup.tsx | 78 +++++ .../pipelineComponentFactory.tsx | 3 +- .../pipelinesDemo/useDemoPipelineNodes.tsx | 9 +- .../stylesDemo/stylesComponentFactory.tsx | 2 +- .../nodes/labels/PipelinesNodeLabel.tsx | 253 ++++++++++++++++ .../src/components/nodes/labels/index.ts | 2 + .../module/src/css/topology-pipelines.css | 54 ++++ .../components/groups/DefaultTaskGroup.tsx | 285 +++++++++--------- .../groups/PipelinesDefaultGroup.tsx | 140 +++++++++ .../groups/PipelinesDefaultGroupCollapsed.tsx | 130 ++++++++ .../groups/PipelinesDefaultGroupExpanded.tsx | 32 ++ .../src/pipelines/components/groups/index.ts | 3 + 15 files changed, 875 insertions(+), 154 deletions(-) create mode 100644 packages/demo-app-ts/src/demos/pipelinesDemo/DemoPipelinesGroup.tsx create mode 100644 packages/module/src/components/nodes/labels/PipelinesNodeLabel.tsx create mode 100644 packages/module/src/pipelines/components/groups/PipelinesDefaultGroup.tsx create mode 100644 packages/module/src/pipelines/components/groups/PipelinesDefaultGroupCollapsed.tsx create mode 100644 packages/module/src/pipelines/components/groups/PipelinesDefaultGroupExpanded.tsx diff --git a/packages/demo-app-ts/src/demos/pipelineGroupsDemo/DemoTaskGroup.tsx b/packages/demo-app-ts/src/demos/pipelineGroupsDemo/DemoTaskGroup.tsx index 09cc7430..28806320 100644 --- a/packages/demo-app-ts/src/demos/pipelineGroupsDemo/DemoTaskGroup.tsx +++ b/packages/demo-app-ts/src/demos/pipelineGroupsDemo/DemoTaskGroup.tsx @@ -3,37 +3,57 @@ import { observer } from 'mobx-react'; import { AnchorEnd, DagreLayoutOptions, - DefaultGroup, + PipelinesDefaultGroup, GraphElement, isNode, LabelPosition, Node, TOP_TO_BOTTOM, useAnchor, + WithContextMenuProps, + WithSelectionProps, + ShapeProps, + WithDragNodeProps, } from '@patternfly/react-topology'; import TaskGroupSourceAnchor from './TaskGroupSourceAnchor'; import TaskGroupTargetAnchor from './TaskGroupTargetAnchor'; -interface DemoTaskNodeProps { +type DemoTaskGroupProps = { element: GraphElement; -} + collapsible?: boolean; + collapsedWidth?: number; + collapsedHeight?: number; + onCollapseChange?: (group: Node, collapsed: boolean) => void; + getCollapsedShape?: (node: Node) => React.FunctionComponent; + collapsedShadowOffset?: number; // defaults to 10 +} & WithContextMenuProps & + WithDragNodeProps & + WithSelectionProps; -const DemoTaskGroup: React.FunctionComponent = ({ element, ...rest }) => { +const DemoTaskGroup: React.FunctionComponent = ({ element, collapsedWidth, collapsedHeight, ...rest }) => { const verticalLayout = (element.getGraph().getLayoutOptions?.() as DagreLayoutOptions)?.rankdir === TOP_TO_BOTTOM; useAnchor( - React.useCallback((node: Node) =>new TaskGroupSourceAnchor(node, verticalLayout), [verticalLayout]), + React.useCallback((node: Node) => new TaskGroupSourceAnchor(node, verticalLayout), [verticalLayout]), AnchorEnd.source ); useAnchor( - React.useCallback((node: Node) => new TaskGroupTargetAnchor(node, verticalLayout),[verticalLayout]), + React.useCallback((node: Node) => new TaskGroupTargetAnchor(node, verticalLayout), [verticalLayout]), AnchorEnd.target ); if (!isNode(element)) { return null; } return ( - + ); }; diff --git a/packages/demo-app-ts/src/demos/pipelineGroupsDemo/createDemoPipelineGroupsNodes.ts b/packages/demo-app-ts/src/demos/pipelineGroupsDemo/createDemoPipelineGroupsNodes.ts index a9c9f664..ee0ef30a 100644 --- a/packages/demo-app-ts/src/demos/pipelineGroupsDemo/createDemoPipelineGroupsNodes.ts +++ b/packages/demo-app-ts/src/demos/pipelineGroupsDemo/createDemoPipelineGroupsNodes.ts @@ -4,7 +4,7 @@ import { PipelineNodeModel, RunStatus } from '@patternfly/react-topology'; export const NODE_PADDING_VERTICAL = 15; export const NODE_PADDING_HORIZONTAL = 15; -export const GROUP_PADDING_VERTICAL = 15; +export const GROUP_PADDING_VERTICAL = 50; export const GROUP_PADDING_HORIZONTAL = 25; export const DEFAULT_TASK_WIDTH = 180; diff --git a/packages/demo-app-ts/src/demos/pipelineGroupsDemo/pipelineGroupsComponentFactory.tsx b/packages/demo-app-ts/src/demos/pipelineGroupsDemo/pipelineGroupsComponentFactory.tsx index 4acb646e..0832175e 100644 --- a/packages/demo-app-ts/src/demos/pipelineGroupsDemo/pipelineGroupsComponentFactory.tsx +++ b/packages/demo-app-ts/src/demos/pipelineGroupsDemo/pipelineGroupsComponentFactory.tsx @@ -22,7 +22,7 @@ const pipelineGroupsComponentFactory: ComponentFactory = ( } switch (type) { case 'Execution': - return DemoTaskGroup; + return withSelection()(DemoTaskGroup); case 'Task': return withSelection()(DemoTaskNode); case DEFAULT_SPACER_NODE_TYPE: diff --git a/packages/demo-app-ts/src/demos/pipelinesDemo/DemoPipelinesGroup.tsx b/packages/demo-app-ts/src/demos/pipelinesDemo/DemoPipelinesGroup.tsx new file mode 100644 index 00000000..9f4f6878 --- /dev/null +++ b/packages/demo-app-ts/src/demos/pipelinesDemo/DemoPipelinesGroup.tsx @@ -0,0 +1,78 @@ +import * as React from 'react'; +import { + GraphElement, + Node, + ScaleDetailsLevel, + ShapeProps, + WithContextMenuProps, + WithDragNodeProps, + WithSelectionProps, + PipelinesDefaultGroup, + AnchorEnd, + DagreLayoutOptions, + TOP_TO_BOTTOM, + useAnchor, + Dimensions +} from '@patternfly/react-topology'; + +export enum DataTypes { + Default, + Alternate +} + +type DemoPipelinesGroupProps = { + element: GraphElement; + collapsible?: boolean; + collapsedWidth?: number; + collapsedHeight?: number; + onCollapseChange?: (group: Node, collapsed: boolean) => void; + getCollapsedShape?: (node: Node) => React.FunctionComponent; + collapsedShadowOffset?: number; // defaults to 10 +} & WithContextMenuProps & + WithDragNodeProps & + WithSelectionProps; + +import TaskGroupSourceAnchor from '../pipelineGroupsDemo/TaskGroupSourceAnchor'; +import TaskGroupTargetAnchor from '../pipelineGroupsDemo/TaskGroupTargetAnchor'; + +export const DemoPipelinesGroup: React.FunctionComponent = ({ + element, + onCollapseChange, + ...rest +}) => { + const data = element.getData(); + const detailsLevel = element.getGraph().getDetailsLevel(); + const verticalLayout = (element.getGraph().getLayoutOptions?.() as DagreLayoutOptions)?.rankdir === TOP_TO_BOTTOM; + + const handleCollapse = (group: Node, collapsed: boolean): void => { + if (collapsed && rest.collapsedWidth !== undefined && rest.collapsedHeight !== undefined) { + group.setDimensions(new Dimensions(rest.collapsedWidth, rest.collapsedHeight)); + } + group.setCollapsed(collapsed); + onCollapseChange && onCollapseChange(group, collapsed); + }; + + useAnchor( + React.useCallback((node: Node) => new TaskGroupSourceAnchor(node, verticalLayout), [verticalLayout]), + AnchorEnd.source + ); + + useAnchor( + React.useCallback((node: Node) => new TaskGroupTargetAnchor(node, verticalLayout), [verticalLayout]), + AnchorEnd.target + ); + + return ( + + ); +}; diff --git a/packages/demo-app-ts/src/demos/pipelinesDemo/pipelineComponentFactory.tsx b/packages/demo-app-ts/src/demos/pipelinesDemo/pipelineComponentFactory.tsx index 970dca11..4aadd455 100644 --- a/packages/demo-app-ts/src/demos/pipelinesDemo/pipelineComponentFactory.tsx +++ b/packages/demo-app-ts/src/demos/pipelinesDemo/pipelineComponentFactory.tsx @@ -22,6 +22,7 @@ import DemoTaskNode from './DemoTaskNode'; import DemoFinallyNode from './DemoFinallyNode'; import DemoTaskGroupEdge from './DemoTaskGroupEdge'; import DemoTaskEdge from './DemoTaskEdge'; +import { DemoPipelinesGroup } from "./DemoPipelinesGroup"; export const GROUPED_EDGE_TYPE = 'GROUPED_EDGE'; export const SPACER_EDGE_TYPE = 'spacer-edge'; @@ -55,7 +56,7 @@ const pipelineComponentFactory: ComponentFactory = ( case DEFAULT_FINALLY_NODE_TYPE: return withContextMenu(() => defaultMenu)(withSelection()(DemoFinallyNode)); case 'task-group': - return DefaultTaskGroup; + return withSelection()(DemoPipelinesGroup); case 'finally-group': return DefaultTaskGroup; case DEFAULT_SPACER_NODE_TYPE: diff --git a/packages/demo-app-ts/src/demos/pipelinesDemo/useDemoPipelineNodes.tsx b/packages/demo-app-ts/src/demos/pipelinesDemo/useDemoPipelineNodes.tsx index 8daa5109..7cd82b16 100644 --- a/packages/demo-app-ts/src/demos/pipelinesDemo/useDemoPipelineNodes.tsx +++ b/packages/demo-app-ts/src/demos/pipelinesDemo/useDemoPipelineNodes.tsx @@ -4,6 +4,7 @@ import { DEFAULT_TASK_NODE_TYPE, DEFAULT_WHEN_OFFSET, DEFAULT_WHEN_SIZE, + LabelPosition, PipelineNodeModel, RunStatus, WhenStatus @@ -149,7 +150,10 @@ export const useDemoPipelineNodes = ( type: 'task-group', children: parallelTasks.map((t) => t.id), group: true, - label: 'Parallel tasks' + label: 'Parallel tasks', + data: { + badge: 'Label', + } }); } } @@ -190,7 +194,8 @@ export const useDemoPipelineNodes = ( type: 'task-group', children: [], group: true, - label: `Group ${task.data.columnGroup}` + label: `Group ${task.data.columnGroup}`, + labelPosition: LabelPosition.top }; acc.push(taskGroup); } diff --git a/packages/demo-app-ts/src/demos/stylesDemo/stylesComponentFactory.tsx b/packages/demo-app-ts/src/demos/stylesDemo/stylesComponentFactory.tsx index b747aa51..495c6fcb 100644 --- a/packages/demo-app-ts/src/demos/stylesDemo/stylesComponentFactory.tsx +++ b/packages/demo-app-ts/src/demos/stylesDemo/stylesComponentFactory.tsx @@ -140,4 +140,4 @@ const stylesComponentFactory: ComponentFactory = ( } }; -export default stylesComponentFactory; +export default stylesComponentFactory; \ No newline at end of file diff --git a/packages/module/src/components/nodes/labels/PipelinesNodeLabel.tsx b/packages/module/src/components/nodes/labels/PipelinesNodeLabel.tsx new file mode 100644 index 00000000..05f939d1 --- /dev/null +++ b/packages/module/src/components/nodes/labels/PipelinesNodeLabel.tsx @@ -0,0 +1,253 @@ +import * as React from 'react'; +import { css } from '@patternfly/react-styles'; +import styles from '../../../css/topology-components'; +import pipelineStyles from '../../../css/topology-pipelines'; +import { truncateMiddle } from '../../../utils/truncate-middle'; +import { createSvgIdUrl, useCombineRefs, useHover, useSize } from '../../../utils'; +import { WithContextMenuProps, WithDndDragProps } from '../../../behavior'; +import NodeShadows, { NODE_SHADOW_FILTER_ID_DANGER, NODE_SHADOW_FILTER_ID_HOVER } from '../NodeShadows'; +import LabelBadge from './LabelBadge'; +import LabelIcon from './LabelIcon'; +import LabelActionIcon from './LabelActionIcon'; +import { BadgeLocation, LabelPosition, NodeStatus } from '../../../types'; + +type PipelinesNodeLabelProps = { + children?: string; + className?: string; + paddingX?: number; + paddingY?: number; + x?: number; + y?: number; + position?: LabelPosition; + cornerRadius?: number; + status?: NodeStatus; + truncateLength?: number; // Defaults to 13 + labelIconClass?: string; // Icon to show in label + labelIcon?: React.ReactNode; + labelIconPadding?: number; + dragRef?: WithDndDragProps['dndDragRef']; + hover?: boolean; + dragging?: boolean; + edgeDragging?: boolean; + dropTarget?: boolean; + actionIcon?: React.ReactElement; + actionIconClassName?: string; + onActionIconClick?: (e: React.MouseEvent) => void; + badge?: string; + badgeColor?: string; + badgeTextColor?: string; + badgeBorderColor?: string; + badgeClassName?: string; + badgeLocation?: BadgeLocation; +} & Partial; + +/** + * Renders a `` component with a `` box behind. + */ +const PipelinesNodeLabel: React.FunctionComponent = ({ + children, + className, + paddingX = 0, + paddingY = 0, + cornerRadius = 22, + x = 0, + y = 0, + position = LabelPosition.bottom, + status, + badge, + badgeColor, + badgeTextColor, + badgeBorderColor, + badgeClassName, + badgeLocation = BadgeLocation.inner, + labelIconClass, + labelIcon, + labelIconPadding = 4, + truncateLength, + dragRef, + hover, + dragging, + edgeDragging, + dropTarget, + actionIcon, + actionIconClassName, + onActionIconClick, + ...other +}) => { + const [labelHover, labelHoverRef] = useHover(); + const refs = useCombineRefs(dragRef, typeof truncateLength === 'number' ? labelHoverRef : undefined); + + const [textSize, textRef] = useSize([children, truncateLength, className, labelHover]); + const [badgeSize, badgeRef] = useSize([badge]); + const [actionSize, actionRef] = useSize([actionIcon, paddingX]); + + const { + width, + height, + backgroundHeight, + startX, + startY, + badgeStartX, + badgeStartY, + actionStartX, + iconSpace, + badgeSpace + } = React.useMemo(() => { + if (!textSize) { + return { + width: 0, + height: 0, + backgroundHeight: 0, + startX: 0, + startY: 0, + badgeStartX: 0, + badgeStartY: 0, + actionStartX: 0, + contextStartX: 0, + iconSpace: 0, + badgeSpace: 0 + }; + } + const badgeSpace = badge && badgeSize && badgeLocation === BadgeLocation.inner ? badgeSize.width + paddingX : 0; + const height = Math.max(textSize.height, badgeSize?.height ?? 0) + paddingY * 2; + const iconSpace = labelIconClass || labelIcon ? (height + paddingY * 0.5) / 2 : 0; + const actionSpace = actionIcon && actionSize ? actionSize.width : 0; + const primaryWidth = iconSpace + badgeSpace + paddingX + textSize.width + actionSpace + paddingX; + const width = primaryWidth; + + let startX: number; + let startY: number; + if (position === LabelPosition.top) { + startX = x - width / 2; + startY = -y - height - paddingY; + } else if (position === LabelPosition.right) { + startX = x + iconSpace; + startY = y - height / 2; + } else if (position === LabelPosition.left) { + startX = - width - paddingX; + startY = y - height / 2 + paddingY; + } else { + startX = x - width / 2 + iconSpace / 2; + startY = y; + } + const actionStartX = iconSpace + badgeSpace + paddingX + textSize.width + paddingX; + const contextStartX = actionStartX + actionSpace; + const backgroundHeight = height; + let badgeStartX = 0; + let badgeStartY = 0; + if (badgeSize) { + if (badgeLocation === BadgeLocation.below) { + badgeStartX = (width - badgeSize.width) / 2; + badgeStartY = height + paddingY; + } else { + badgeStartX = iconSpace + paddingX; + badgeStartY = (height - badgeSize.height) / 2; + } + } + + return { + width, + height, + backgroundHeight, + startX, + startY, + actionStartX, + contextStartX, + badgeStartX, + badgeStartY, + iconSpace, + badgeSpace: badgeSize && badgeLocation === BadgeLocation.inner ? badgeSpace : 0 + }; + }, [ + textSize, + badge, + badgeSize, + badgeLocation, + paddingX, + paddingY, + labelIconClass, + labelIcon, + actionIcon, + actionSize, + position, + x, + y + ]); + + let filterId; + if (status === 'danger') { + filterId = NODE_SHADOW_FILTER_ID_DANGER; + } else if (hover || dragging || edgeDragging || dropTarget) { + filterId = NODE_SHADOW_FILTER_ID_HOVER; + } + + return ( + + + {textSize && ( + + )} + {textSize && badge && ( + + )} + {textSize && (labelIconClass || labelIcon) && ( + + )} + + {truncateLength > 0 && !labelHover ? truncateMiddle(children, { length: truncateLength }) : children} + + {textSize && actionIcon && ( + <> + + + + )} + + ); +}; + +export default PipelinesNodeLabel; diff --git a/packages/module/src/components/nodes/labels/index.ts b/packages/module/src/components/nodes/labels/index.ts index 11dcc59c..e16cc915 100644 --- a/packages/module/src/components/nodes/labels/index.ts +++ b/packages/module/src/components/nodes/labels/index.ts @@ -3,3 +3,5 @@ export { default as LabelBadge } from './LabelBadge'; export { default as LabelContextMenu } from './LabelContextMenu'; export { default as LabelIcon } from './LabelIcon'; export { default as NodeLabel } from './NodeLabel'; +export { default as PipelinesNodeLabel } from './PipelinesNodeLabel'; + diff --git a/packages/module/src/css/topology-pipelines.css b/packages/module/src/css/topology-pipelines.css index 070a9637..2649f59a 100644 --- a/packages/module/src/css/topology-pipelines.css +++ b/packages/module/src/css/topology-pipelines.css @@ -101,6 +101,9 @@ --pf-topology-pipelines__status-icon--m-selected--m-running--color: var(--pf-v5-global--Color--light-100); --pf-topology-pipelines__status-icon--m-selected--m-idle--color: var(--pf-v5-global--Color--light-100); + /* pipelines node label */ + --pf-topology-pipelines__node__label__background--fill: var(--pf-v5-global--Color--light-100); + --pf-topology-pipelines__node__label__background--m-selected--fill: var(--pf-v5-global--active-color--100); /* when expression */ --pf-topology-pipelines__when-expression-background--Stroke: var(--pf-v5-global--BorderColor--200); @@ -555,3 +558,54 @@ .pf-topology-pipelines__status-icon-background.pf-m-idle.pf-m-selected { --pf-topology-pipelines__pill-background--Fill: var(--pf-topology-pipelines__pill-background--m-idle--Stroke); } + +.pf-topology-pipelines__group__label > text { + fill: var(--pf-topology-pipelines__pill-text--Color); + font-size: var(--pf-v5-global--FontSize--sm); + pointer-events: none; +} + +.pf-topology-pipelines__group.pf-m-selected .pf-topology-pipelines__group__label>text { + fill: var(--pf-v5-global--Color--light-100); + font-size: var(--pf-v5-global--FontSize--sm); + pointer-events: none; +} + +.pf-topology-pipelines__node__background { + fill: var(--pf-topology__node__background--Fill); + stroke-width: var(--pf-topology__node__background--StrokeWidth); + stroke: var(--pf-topology__node__background--Stroke); +} + +.pf-topology-pipelines__node__label__background { + fill: var(--pf-topology-pipelines__node__label__background--fill); +} + +.pf-topology-pipelines__group-inner.pf-m-selected .pf-topology__group__background { + --pf-topology__group__background--Fill: var(--pf-topology__group--m-selected--topology__group__background--Fill); + --pf-topology__group__background--Stroke: var(--pf-topology__group--m-selected--topology__group__background--Stroke); +} + +.pf-topology-pipelines__group-inner.pf-m-hover .pf-topology__group__background { + --pf-topology__group__background--Stroke: var(--pf-topology__group--m-hover--topology__group__background--Stroke); + --pf-topology__group__label__node__label__background--Stroke: var(--pf-topology__group--m-hover--label__node__label__background--Stroke); +} + +.pf-topology-pipelines__group-inner.pf-m-hover.pf-m-selected .pf-topology__group__background { + --pf-topology__group__background--Fill: var(--pf-topology__group--m-selected--topology__group__background--Fill); + --pf-topology__group__background--Stroke: var(--pf-topology__group--m-selected--m-hover--topology__group__background--Stroke); + --pf-topology__group__label__node__label__background--Stroke: var(--pf-topology__group--m-hover--label__node__label__background--Stroke); +} + +.pf-topology__node__action-icon { + fill: var(--pf-v5-global--Color--light-100); + stroke: var(--pf-v5-global--active-color--100); +} + +.pf-topology-pipelines__group.pf-m-selected .pf-topology-pipelines__node__label__background { + fill: var(--pf-v5-global--active-color--100); +} + +.pf-topology-pipelines__group.pf-m-selected .pf-topology-pipelines__group__label .pf-topology__node__action-icon .pf-topology__node__action-icon__icon { + color: white; +} \ No newline at end of file diff --git a/packages/module/src/pipelines/components/groups/DefaultTaskGroup.tsx b/packages/module/src/pipelines/components/groups/DefaultTaskGroup.tsx index 70b8b6f6..6673038e 100644 --- a/packages/module/src/pipelines/components/groups/DefaultTaskGroup.tsx +++ b/packages/module/src/pipelines/components/groups/DefaultTaskGroup.tsx @@ -5,17 +5,9 @@ import styles from '../../../css/topology-components'; import CollapseIcon from '@patternfly/react-icons/dist/esm/icons/compress-alt-icon'; import NodeLabel from '../../../components/nodes/labels/NodeLabel'; import { Layer } from '../../../components/layers'; -import { GROUPS_LAYER } from '../../../const'; +import { GROUPS_LAYER, TOP_LAYER } from '../../../const'; import { maxPadding, useCombineRefs, useHover } from '../../../utils'; -import { - BadgeLocation, - GraphElement, - isGraph, - isNode, - LabelPosition, - Node, - NodeStyle -} from '../../../types'; +import { BadgeLocation, GraphElement, isGraph, isNode, LabelPosition, Node, NodeStyle } from '../../../types'; import { useDragNode, WithContextMenuProps, @@ -36,6 +28,7 @@ type DefaultTaskGroupProps = { label?: string; // Defaults to element.getLabel() secondaryLabel?: string; showLabel?: boolean; // Defaults to true + labelPosition?: LabelPosition; truncateLength?: number; // Defaults to 13 badge?: string; badgeColor?: string; @@ -51,145 +44,155 @@ type DefaultTaskGroupProps = { type DefaultTaskGroupInnerProps = Omit & { element: Node }; -const DefaultTaskGroupInner: React.FunctionComponent = observer(({ - className, - element, - collapsible, - selected, - onSelect, - hover, - label, - secondaryLabel, - showLabel = true, - truncateLength, - canDrop, - dropTarget, - onContextMenu, - contextMenuOpen, - dragging, - dragNodeRef, - badge, - badgeColor, - badgeTextColor, - badgeBorderColor, - badgeClassName, - badgeLocation, - labelOffset = 17, - labelIconClass, - labelIcon, - labelIconPadding, - onCollapseChange -}) => { - const [hovered, hoverRef] = useHover(); - const [labelHover, labelHoverRef] = useHover(); - const dragLabelRef = useDragNode()[1]; - const refs = useCombineRefs(hoverRef, dragNodeRef); - const isHover = hover !== undefined ? hover : hovered; - const labelPosition = element.getLabelPosition(); +const DefaultTaskGroupInner: React.FunctionComponent = observer( + ({ + className, + element, + collapsible, + selected, + onSelect, + hover, + label, + secondaryLabel, + showLabel = true, + truncateLength, + canDrop, + dropTarget, + onContextMenu, + contextMenuOpen, + dragging, + dragNodeRef, + badge, + badgeColor, + badgeTextColor, + badgeBorderColor, + badgeClassName, + badgeLocation, + labelOffset = 17, + labelIconClass, + labelIcon, + labelIconPadding, + onCollapseChange + }) => { + const [hovered, hoverRef] = useHover(); + const [labelHover, labelHoverRef] = useHover(); + const dragLabelRef = useDragNode()[1]; + const refs = useCombineRefs(hoverRef, dragNodeRef); + const isHover = hover !== undefined ? hover : hovered; + const labelPosition = element.getLabelPosition(); - let parent = element.getParent(); - let altGroup = false; - while (!isGraph(parent)) { - altGroup = !altGroup; - parent = parent.getParent(); - } + let parent = element.getParent(); + let altGroup = false; + while (!isGraph(parent)) { + altGroup = !altGroup; + parent = parent.getParent(); + } - const children = element.getNodes().filter(c => c.isVisible()); + const children = element.getNodes().filter((c) => c.isVisible()); - // cast to number and coerce - const padding = maxPadding(element.getStyle().padding ?? 17); + // cast to number and coerce + const padding = maxPadding(element.getStyle().padding ?? 17); - const { minX, minY, maxX, maxY } = children.reduce( - (acc, child) => { - const bounds = child.getBounds(); - return { - minX: Math.min(acc.minX, bounds.x - padding), - minY: Math.min(acc.minY, bounds.y - padding), - maxX: Math.max(acc.maxX, bounds.x + bounds.width + padding), - maxY: Math.max(acc.maxY, bounds.y + bounds.height + padding) - }; - }, - { minX: Infinity, minY: Infinity, maxX: 0, maxY: 0 } - ); + const { minX, minY, maxX, maxY } = children.reduce( + (acc, child) => { + const bounds = child.getBounds(); + return { + minX: Math.min(acc.minX, bounds.x - padding), + minY: Math.min(acc.minY, bounds.y - padding), + maxX: Math.max(acc.maxX, bounds.x + bounds.width + padding), + maxY: Math.max(acc.maxY, bounds.y + bounds.height + padding) + }; + }, + { minX: Infinity, minY: Infinity, maxX: 0, maxY: 0 } + ); - const [labelX, labelY] = React.useMemo(() => { - if (!showLabel || !(label || element.getLabel())) { - return [0, 0]; - } - switch (labelPosition) { - case LabelPosition.top: - return [minX + (maxX - minX) / 2, -minY + labelOffset]; - case LabelPosition.right: - return [maxX + labelOffset, minY + (maxY - minY) / 2]; - case LabelPosition.bottom: - default: - return [minX + (maxX - minX) / 2, maxY + labelOffset]; + const [labelX, labelY] = React.useMemo(() => { + if (!showLabel || !(label || element.getLabel())) { + return [0, 0]; + } + switch (labelPosition) { + case LabelPosition.top: + return [minX + (maxX - minX) / 2, -minY + labelOffset]; + case LabelPosition.right: + return [maxX + labelOffset, minY + (maxY - minY) / 2]; + case LabelPosition.bottom: + default: + return [minX + (maxX - minX) / 2, maxY + labelOffset]; + } + }, [element, label, labelOffset, labelPosition, maxX, maxY, minX, minY, showLabel]); + + if (children.length === 0) { + return null; } - }, [element, label, labelOffset, labelPosition, maxX, maxY, minX, minY, showLabel]); - if (children.length === 0) { - return null; - } + const groupClassName = css( + styles.topologyGroup, + className, + altGroup && 'pf-m-alt-group', + canDrop && 'pf-m-highlight', + dragging && 'pf-m-dragging', + selected && 'pf-m-selected' + ); + const innerGroupClassName = css( + styles.topologyGroup, + className, + altGroup && 'pf-m-alt-group', + canDrop && 'pf-m-highlight', + dragging && 'pf-m-dragging', + selected && 'pf-m-selected', + (isHover || labelHover) && 'pf-m-hover', + canDrop && dropTarget && 'pf-m-drop-target' + ); - const groupClassName = css( - styles.topologyGroup, - className, - altGroup && 'pf-m-alt-group', - canDrop && 'pf-m-highlight', - dragging && 'pf-m-dragging', - selected && 'pf-m-selected' - ); - const innerGroupClassName = css( - styles.topologyGroup, - className, - altGroup && 'pf-m-alt-group', - canDrop && 'pf-m-highlight', - dragging && 'pf-m-dragging', - selected && 'pf-m-selected', - (isHover || labelHover) && 'pf-m-hover', - canDrop && dropTarget && 'pf-m-drop-target' - ); - - return ( - - - - - - - {showLabel && (label || element.getLabel()) && ( - : undefined} - onActionIconClick={() => onCollapseChange(element, true)} - > - {label || element.getLabel()} - - )} - - ); -}); + return ( + + + + + + + {showLabel && (label || element.getLabel()) && ( + + : undefined} + onActionIconClick={() => onCollapseChange(element, true)} + > + {label || element.getLabel()} + + + )} + + ); + } +); const DefaultTaskGroup: React.FunctionComponent = ({ element, diff --git a/packages/module/src/pipelines/components/groups/PipelinesDefaultGroup.tsx b/packages/module/src/pipelines/components/groups/PipelinesDefaultGroup.tsx new file mode 100644 index 00000000..a6dae061 --- /dev/null +++ b/packages/module/src/pipelines/components/groups/PipelinesDefaultGroup.tsx @@ -0,0 +1,140 @@ +import * as React from 'react'; +import { observer } from 'mobx-react'; +import { OnSelect, WithDndDragProps, ConnectDragSource, ConnectDropTarget, WithSelectionProps } from '../../../behavior'; +import { ShapeProps } from '../../../components'; +import { Dimensions } from '../../../geom'; +import { GraphElement, LabelPosition, BadgeLocation, isNode, Node } from '../../../types'; +import PipelinesDefaultGroupCollapsed from './PipelinesDefaultGroupCollapsed'; +import PipelinesDefaultGroupExpanded from './PipelinesDefaultGroupExpanded'; + +interface PipelinesDefaultGroupProps { + /** Additional content added to the node */ + children?: React.ReactNode; + /** Additional classes added to the group */ + className?: string; + /** The graph group node element to represent */ + element: GraphElement; + /** Flag if the node accepts drop operations */ + droppable?: boolean; + /** Flag if the current drag operation can be dropped on the node */ + canDrop?: boolean; + /** Flag if the node is the current drop target */ + dropTarget?: boolean; + /** Flag if the user is dragging the node */ + dragging?: boolean; + /** Flag if drag operation is a regroup operation */ + dragRegroupable?: boolean; + /** Flag if the user is hovering on the node */ + hover?: boolean; + /** Label for the node. Defaults to element.getLabel() */ + label?: string; // Defaults to element.getLabel() + /** Secondary label for the node */ + secondaryLabel?: string; + /** Flag to show the label */ + showLabel?: boolean; // Defaults to true + /** Position of the label, top or bottom. Defaults to element.getLabelPosition() or bottom */ + labelPosition?: LabelPosition; + /** The maximum length of the label before truncation */ + truncateLength?: number; + /** The Icon class to show in the label, ignored when labelIcon is specified */ + labelIconClass?: string; + /** The label icon component to show in the label, takes precedence over labelIconClass */ + labelIcon?: string; + /** Padding for the label's icon */ + labelIconPadding?: number; + /** Text for the label's badge */ + badge?: string; + /** Color to use for the label's badge background */ + badgeColor?: string; + /** Color to use for the label's badge text */ + badgeTextColor?: string; + /** Color to use for the label's badge border */ + badgeBorderColor?: string; + /** Additional classes to use for the label's badge */ + badgeClassName?: string; + /** Location of the badge relative to the label's text, inner or below */ + badgeLocation?: BadgeLocation; + /** Flag if the group is collapsible */ + collapsible?: boolean; + /** Width of the collapsed group */ + collapsedWidth?: number; + /** Height of the collapsed group */ + collapsedHeight?: number; + /** Callback when the group is collapsed */ + onCollapseChange?: (group: Node, collapsed: boolean) => void; + /** Shape of the collapsed group */ + getCollapsedShape?: (node: Node) => React.FunctionComponent; + /** Shadow offset for the collapsed group */ + collapsedShadowOffset?: number; + /** Flag if the element selected. Part of WithSelectionProps */ + selected?: boolean; + /** Function to call when the element should become selected (or deselected). Part of WithSelectionProps */ + onSelect?: OnSelect; + /** A ref to add to the node for dragging. Part of WithDragNodeProps */ + dragNodeRef?: WithDndDragProps['dndDragRef']; + /** A ref to add to the node for drag and drop. Part of WithDndDragProps */ + dndDragRef?: ConnectDragSource; + /** A ref to add to the node for dropping. Part of WithDndDropProps */ + dndDropRef?: ConnectDropTarget; + /** Function to call to show a context menu for the node */ + onContextMenu?: (e: React.MouseEvent) => void; + /** Flag indicating that the context menu for the node is currently open */ + contextMenuOpen?: boolean; + /** Flag indicating whether to use hull layout or rect layout for expanded groups. Defaults to hull (true) */ + hulledOutline?: boolean; +} + +type PipelinesDefaultGroupInnerProps = Omit & { element: Node } & WithSelectionProps; + +const PipelinesDefaultGroupInner: React.FunctionComponent = observer( + ({ className, element, onCollapseChange, ...rest }) => { + const childCount = element.getAllNodeChildren().length; + const handleCollapse = (group: Node, collapsed: boolean): void => { + if (collapsed && rest.collapsedWidth !== undefined && rest.collapsedHeight !== undefined) { + group.setDimensions(new Dimensions(rest.collapsedWidth, rest.collapsedHeight)); + } + group.setCollapsed(collapsed); + onCollapseChange && onCollapseChange(group, collapsed); + }; + + if (element.isCollapsed()) { + return ( + + ); + } + return ( + + ); + } +); + +const PipelinesDefaultGroup: React.FunctionComponent = ({ + element, + ...rest +}: PipelinesDefaultGroupProps) => { + if (!isNode(element)) { + throw new Error('DefaultGroup must be used only on Node elements'); + } + + return ; +}; + +export default PipelinesDefaultGroup; diff --git a/packages/module/src/pipelines/components/groups/PipelinesDefaultGroupCollapsed.tsx b/packages/module/src/pipelines/components/groups/PipelinesDefaultGroupCollapsed.tsx new file mode 100644 index 00000000..bc4a992f --- /dev/null +++ b/packages/module/src/pipelines/components/groups/PipelinesDefaultGroupCollapsed.tsx @@ -0,0 +1,130 @@ +import * as React from 'react'; +import { observer } from 'mobx-react'; +import { css } from '@patternfly/react-styles'; +import styles from '@patternfly/react-topology/src/css/topology-pipelines'; +import ExpandIcon from '@patternfly/react-icons/dist/esm/icons/expand-alt-icon'; +import { WithDragNodeProps, WithSelectionProps, WithDndDropProps, WithContextMenuProps, useDragNode } from "../../../behavior"; +import { CollapsibleGroupProps, Stadium, Layer, PipelinesNodeLabel } from "../../../components"; +import { GROUPS_LAYER } from "../../../const"; +import { LabelPosition, BadgeLocation, Node } from "../../../types"; +import { useHover, useCombineRefs } from "../../../utils"; + +type PipelinesDefaultGroupCollapsedProps = { + children?: React.ReactNode; + className?: string; + element: Node; + droppable?: boolean; + canDrop?: boolean; + dropTarget?: boolean; + dragging?: boolean; + hover?: boolean; + label?: string; // Defaults to element.getLabel() + secondaryLabel?: string; + showLabel?: boolean; // Defaults to true + labelPosition?: LabelPosition; // Defaults to bottom + truncateLength?: number; // Defaults to 13 + labelIconClass?: string; // Icon to show in label + labelIcon?: string; + labelIconPadding?: number; + badge?: string; + badgeColor?: string; + badgeTextColor?: string; + badgeBorderColor?: string; + badgeClassName?: string; + badgeLocation?: BadgeLocation; +} & CollapsibleGroupProps & WithDragNodeProps & WithSelectionProps & WithDndDropProps & WithContextMenuProps; + +const PipelinesDefaultGroupCollapsed: React.FunctionComponent = ({ + className, + element, + collapsible, + selected, + onSelect, + children, + hover, + label, + showLabel = true, + truncateLength, + collapsedWidth, + collapsedHeight, + onCollapseChange, + collapsedShadowOffset = 8, + dragNodeRef, + canDrop, + dropTarget, + onContextMenu, + dragging, + labelPosition, + badge, + badgeColor, + badgeTextColor, + badgeBorderColor, + badgeClassName, + badgeLocation, + labelIconClass, + labelIcon, + labelIconPadding +}) => { + const [hovered, hoverRef] = useHover(); + const [labelHover, labelHoverRef] = useHover(); + const dragLabelRef = useDragNode()[1]; + const refs = useCombineRefs(hoverRef, dragNodeRef); + const isHover = hover !== undefined ? hover : hovered; + + const groupClassName = css( + styles.topologyPipelinesGroup, + className, + canDrop && 'pf-m-highlight', + canDrop && dropTarget && 'pf-m-drop-target', + dragging && 'pf-m-dragging', + selected && 'pf-m-selected' + ); + + return ( + + + + <> + + + + + + + {showLabel && ( + : undefined} + onActionIconClick={() => onCollapseChange(element, false)} + > + {label || element.getLabel()} + + )} + {children} + + ); +}; + +export default observer(PipelinesDefaultGroupCollapsed); diff --git a/packages/module/src/pipelines/components/groups/PipelinesDefaultGroupExpanded.tsx b/packages/module/src/pipelines/components/groups/PipelinesDefaultGroupExpanded.tsx new file mode 100644 index 00000000..e71913ff --- /dev/null +++ b/packages/module/src/pipelines/components/groups/PipelinesDefaultGroupExpanded.tsx @@ -0,0 +1,32 @@ +import * as React from 'react'; +import { observer } from 'mobx-react'; +import { LabelPosition, Node } from '../../../types'; +import DefaultTaskGroup from './DefaultTaskGroup'; +import { CollapsibleGroupProps } from '../../../components'; + +type PipelinesDefaultGroupExpandedProps = { + className?: string; + element: Node; + labelPosition?: LabelPosition; + showLabel?: boolean; +} & CollapsibleGroupProps; + +const PipelinesDefaultGroupExpanded: React.FunctionComponent = ({ + element, + showLabel = true, + labelPosition = LabelPosition.top, + onCollapseChange, + ...rest +}) => { + return ( + + ); +}; + +export default observer(PipelinesDefaultGroupExpanded); diff --git a/packages/module/src/pipelines/components/groups/index.ts b/packages/module/src/pipelines/components/groups/index.ts index ff1e08c3..c4d5386b 100644 --- a/packages/module/src/pipelines/components/groups/index.ts +++ b/packages/module/src/pipelines/components/groups/index.ts @@ -1 +1,4 @@ export { default as DefaultTaskGroup } from './DefaultTaskGroup'; +export { default as PipelinesDefaultGroup } from './PipelinesDefaultGroup'; +export { default as PipelinesDefaultGroupCollapsed } from './PipelinesDefaultGroupCollapsed'; +export { default as PipelinesDefaultGroupExpanded } from './PipelinesDefaultGroupExpanded'; From b4272629eac37a1f69f14b50d47ea2a01dcb33ca Mon Sep 17 00:00:00 2001 From: Jenny <32821331+jenny-s51@users.noreply.github.com> Date: Thu, 21 Mar 2024 16:03:46 -0400 Subject: [PATCH 2/9] add badge styles css --- packages/module/src/css/topology-pipelines.css | 10 ++++++++++ .../components/groups/PipelinesDefaultGroup.tsx | 13 ++++--------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/module/src/css/topology-pipelines.css b/packages/module/src/css/topology-pipelines.css index 2649f59a..e9b16d96 100644 --- a/packages/module/src/css/topology-pipelines.css +++ b/packages/module/src/css/topology-pipelines.css @@ -559,6 +559,16 @@ --pf-topology-pipelines__pill-background--Fill: var(--pf-topology-pipelines__pill-background--m-idle--Stroke); } +.pf-topology-pipelines__group__collapsed .pf-topology__node__label__badge > rect, +.pf-topology-pipelines__group__expanded .pf-topology__node__label__badge > rect { + stroke: var(--pf-v5-global--BorderColor--100); + fill: var(--pf-v5-global--palette--black-150); +} + +.pf-topology__node__label__badge > text { + fill: var(--pf-v5-global--Color--100); +} + .pf-topology-pipelines__group__label > text { fill: var(--pf-topology-pipelines__pill-text--Color); font-size: var(--pf-v5-global--FontSize--sm); diff --git a/packages/module/src/pipelines/components/groups/PipelinesDefaultGroup.tsx b/packages/module/src/pipelines/components/groups/PipelinesDefaultGroup.tsx index a6dae061..1f67f5df 100644 --- a/packages/module/src/pipelines/components/groups/PipelinesDefaultGroup.tsx +++ b/packages/module/src/pipelines/components/groups/PipelinesDefaultGroup.tsx @@ -6,7 +6,8 @@ import { Dimensions } from '../../../geom'; import { GraphElement, LabelPosition, BadgeLocation, isNode, Node } from '../../../types'; import PipelinesDefaultGroupCollapsed from './PipelinesDefaultGroupCollapsed'; import PipelinesDefaultGroupExpanded from './PipelinesDefaultGroupExpanded'; - +import styles from '../../../css/topology-pipelines'; +import { css } from "@patternfly/react-styles"; interface PipelinesDefaultGroupProps { /** Additional content added to the node */ children?: React.ReactNode; @@ -100,26 +101,20 @@ const PipelinesDefaultGroupInner: React.FunctionComponent ); } return ( ); From f71f08aa187fd31f4873b761404a8b27fe2212c6 Mon Sep 17 00:00:00 2001 From: Jeffrey Phillips Date: Sat, 23 Mar 2024 10:17:04 -0400 Subject: [PATCH 3/9] Handle expand/collapse groups --- .../pipelineGroupsDemo/DemoTaskGroup.tsx | 21 +- .../pipelineGroupsDemo/PipelineGroupsDemo.tsx | 3 + .../pipelinesDemo/DemoPipelinesGroup.tsx | 67 +--- .../pipelineComponentFactory.tsx | 2 +- packages/module/src/elements/BaseNode.ts | 19 +- .../components/groups/DefaultTaskGroup.tsx | 303 ++++++++---------- .../groups/DefaultTaskGroupCollapsed.tsx | 51 +++ .../groups/DefaultTaskGroupExpanded.tsx | 197 ++++++++++++ .../groups/PipelinesDefaultGroup.tsx | 135 -------- .../groups/PipelinesDefaultGroupCollapsed.tsx | 130 -------- .../groups/PipelinesDefaultGroupExpanded.tsx | 32 -- .../src/pipelines/components/groups/index.ts | 4 +- .../pipelines/components/nodes/TaskNode.tsx | 24 ++ .../pipelines/elements/BasePipelineNode.ts | 29 ++ .../elements/pipelineElementFactory.ts | 13 + packages/module/src/pipelines/utils/utils.ts | 26 +- 16 files changed, 523 insertions(+), 533 deletions(-) create mode 100644 packages/module/src/pipelines/components/groups/DefaultTaskGroupCollapsed.tsx create mode 100644 packages/module/src/pipelines/components/groups/DefaultTaskGroupExpanded.tsx delete mode 100644 packages/module/src/pipelines/components/groups/PipelinesDefaultGroup.tsx delete mode 100644 packages/module/src/pipelines/components/groups/PipelinesDefaultGroupCollapsed.tsx delete mode 100644 packages/module/src/pipelines/components/groups/PipelinesDefaultGroupExpanded.tsx create mode 100644 packages/module/src/pipelines/elements/BasePipelineNode.ts create mode 100644 packages/module/src/pipelines/elements/pipelineElementFactory.ts diff --git a/packages/demo-app-ts/src/demos/pipelineGroupsDemo/DemoTaskGroup.tsx b/packages/demo-app-ts/src/demos/pipelineGroupsDemo/DemoTaskGroup.tsx index 28806320..57e70f2d 100644 --- a/packages/demo-app-ts/src/demos/pipelineGroupsDemo/DemoTaskGroup.tsx +++ b/packages/demo-app-ts/src/demos/pipelineGroupsDemo/DemoTaskGroup.tsx @@ -3,7 +3,7 @@ import { observer } from 'mobx-react'; import { AnchorEnd, DagreLayoutOptions, - PipelinesDefaultGroup, + DefaultTaskGroup, GraphElement, isNode, LabelPosition, @@ -14,6 +14,7 @@ import { WithSelectionProps, ShapeProps, WithDragNodeProps, + EdgeCreationTypes, } from '@patternfly/react-topology'; import TaskGroupSourceAnchor from './TaskGroupSourceAnchor'; import TaskGroupTargetAnchor from './TaskGroupTargetAnchor'; @@ -30,7 +31,15 @@ type DemoTaskGroupProps = { WithDragNodeProps & WithSelectionProps; -const DemoTaskGroup: React.FunctionComponent = ({ element, collapsedWidth, collapsedHeight, ...rest }) => { +export const DEFAULT_TASK_WIDTH = 180; +export const DEFAULT_TASK_HEIGHT = 32; + +const getEdgeCreationTypes = (): EdgeCreationTypes => ({ + edgeType: 'edge', + spacerEdgeType: 'edge' +}); + +const DemoTaskGroup: React.FunctionComponent = ({ element, ...rest }) => { const verticalLayout = (element.getGraph().getLayoutOptions?.() as DagreLayoutOptions)?.rankdir === TOP_TO_BOTTOM; useAnchor( @@ -45,13 +54,13 @@ const DemoTaskGroup: React.FunctionComponent = ({ element, c return null; } return ( - ); diff --git a/packages/demo-app-ts/src/demos/pipelineGroupsDemo/PipelineGroupsDemo.tsx b/packages/demo-app-ts/src/demos/pipelineGroupsDemo/PipelineGroupsDemo.tsx index 65c13f96..43871040 100644 --- a/packages/demo-app-ts/src/demos/pipelineGroupsDemo/PipelineGroupsDemo.tsx +++ b/packages/demo-app-ts/src/demos/pipelineGroupsDemo/PipelineGroupsDemo.tsx @@ -25,6 +25,8 @@ import { createDemoPipelineGroupsNodes } from './createDemoPipelineGroupsNodes'; import { PipelineGroupsDemoContext, PipelineGroupsDemoModel } from './PipelineGroupsDemoContext'; import OptionsBar from './OptionsBar'; import DemoControlBar from '../DemoControlBar'; +import pipelineElementFactory + from '@patternfly/react-topology/dist/esm/pipelines/elements/pipelineElementFactory'; const TopologyPipelineGroups: React.FC<{ nodes: PipelineNodeModel[] }> = observer(({ nodes }) => { const controller = useVisualizationController(); @@ -64,6 +66,7 @@ TopologyPipelineGroups.displayName = 'TopologyPipelineLayout'; export const PipelineGroupsDemo = observer(() => { const controller = new Visualization(); + controller.registerElementFactory(pipelineElementFactory); controller.registerComponentFactory(pipelineGroupsComponentFactory); controller.registerLayoutFactory( (type: string, graph: Graph): Layout | undefined => diff --git a/packages/demo-app-ts/src/demos/pipelinesDemo/DemoPipelinesGroup.tsx b/packages/demo-app-ts/src/demos/pipelinesDemo/DemoPipelinesGroup.tsx index 9f4f6878..ca0f6440 100644 --- a/packages/demo-app-ts/src/demos/pipelinesDemo/DemoPipelinesGroup.tsx +++ b/packages/demo-app-ts/src/demos/pipelinesDemo/DemoPipelinesGroup.tsx @@ -1,78 +1,45 @@ import * as React from 'react'; import { + DefaultTaskGroup, + EdgeCreationTypes, GraphElement, - Node, + LabelPosition, + observer, ScaleDetailsLevel, - ShapeProps, WithContextMenuProps, WithDragNodeProps, WithSelectionProps, - PipelinesDefaultGroup, - AnchorEnd, - DagreLayoutOptions, - TOP_TO_BOTTOM, - useAnchor, - Dimensions } from '@patternfly/react-topology'; - -export enum DataTypes { - Default, - Alternate -} +import { GROUPED_EDGE_TYPE } from './pipelineComponentFactory'; type DemoPipelinesGroupProps = { element: GraphElement; - collapsible?: boolean; - collapsedWidth?: number; - collapsedHeight?: number; - onCollapseChange?: (group: Node, collapsed: boolean) => void; - getCollapsedShape?: (node: Node) => React.FunctionComponent; - collapsedShadowOffset?: number; // defaults to 10 } & WithContextMenuProps & WithDragNodeProps & WithSelectionProps; -import TaskGroupSourceAnchor from '../pipelineGroupsDemo/TaskGroupSourceAnchor'; -import TaskGroupTargetAnchor from '../pipelineGroupsDemo/TaskGroupTargetAnchor'; +const getEdgeCreationTypes = (): EdgeCreationTypes => ({ + edgeType: GROUPED_EDGE_TYPE, + spacerEdgeType: GROUPED_EDGE_TYPE, + finallyEdgeType: GROUPED_EDGE_TYPE, +}); -export const DemoPipelinesGroup: React.FunctionComponent = ({ - element, - onCollapseChange, - ...rest -}) => { +const DemoPipelinesGroup: React.FunctionComponent = ({ element }) => { const data = element.getData(); - const detailsLevel = element.getGraph().getDetailsLevel(); - const verticalLayout = (element.getGraph().getLayoutOptions?.() as DagreLayoutOptions)?.rankdir === TOP_TO_BOTTOM; - - const handleCollapse = (group: Node, collapsed: boolean): void => { - if (collapsed && rest.collapsedWidth !== undefined && rest.collapsedHeight !== undefined) { - group.setDimensions(new Dimensions(rest.collapsedWidth, rest.collapsedHeight)); - } - group.setCollapsed(collapsed); - onCollapseChange && onCollapseChange(group, collapsed); - }; - - useAnchor( - React.useCallback((node: Node) => new TaskGroupSourceAnchor(node, verticalLayout), [verticalLayout]), - AnchorEnd.source - ); - - useAnchor( - React.useCallback((node: Node) => new TaskGroupTargetAnchor(node, verticalLayout), [verticalLayout]), - AnchorEnd.target - ); + const detailsLevel = element.getGraph().getDetailsLevel() return ( - ); }; + +export default observer(DemoPipelinesGroup); diff --git a/packages/demo-app-ts/src/demos/pipelinesDemo/pipelineComponentFactory.tsx b/packages/demo-app-ts/src/demos/pipelinesDemo/pipelineComponentFactory.tsx index 4aadd455..6d5635b6 100644 --- a/packages/demo-app-ts/src/demos/pipelinesDemo/pipelineComponentFactory.tsx +++ b/packages/demo-app-ts/src/demos/pipelinesDemo/pipelineComponentFactory.tsx @@ -22,7 +22,7 @@ import DemoTaskNode from './DemoTaskNode'; import DemoFinallyNode from './DemoFinallyNode'; import DemoTaskGroupEdge from './DemoTaskGroupEdge'; import DemoTaskEdge from './DemoTaskEdge'; -import { DemoPipelinesGroup } from "./DemoPipelinesGroup"; +import DemoPipelinesGroup from "./DemoPipelinesGroup"; export const GROUPED_EDGE_TYPE = 'GROUPED_EDGE'; export const SPACER_EDGE_TYPE = 'spacer-edge'; diff --git a/packages/module/src/elements/BaseNode.ts b/packages/module/src/elements/BaseNode.ts index 2c8fd079..035a0322 100644 --- a/packages/module/src/elements/BaseNode.ts +++ b/packages/module/src/elements/BaseNode.ts @@ -168,6 +168,23 @@ export default class BaseNode extends }, []); } + getPositionableChildren(): Node[] { + return super.getChildren().reduce((total, nexChild) => { + if (isNode(nexChild)) { + if (nexChild.isGroup() && !nexChild.isCollapsed()) { + return total.concat(nexChild.getAllNodeChildren()); + } + total.push(nexChild); + } + return total; + }, []); + } + + // Return all children regardless of collapse status + protected getAllChildren(): GraphElement[] { + return super.getChildren(); + } + getKind(): ModelKind { return ModelKind.node; } @@ -202,7 +219,7 @@ export default class BaseNode extends updateChildrenPositions(point: Point, prevLocation: Point): void { const xOffset = point.x - prevLocation.x; const yOffset = point.y - prevLocation.y; - this.getAllNodeChildren().forEach(child => { + this.getPositionableChildren().forEach(child => { if (isNode(child)) { const node = child as Node; const position = node.getPosition(); diff --git a/packages/module/src/pipelines/components/groups/DefaultTaskGroup.tsx b/packages/module/src/pipelines/components/groups/DefaultTaskGroup.tsx index 6673038e..d5322666 100644 --- a/packages/module/src/pipelines/components/groups/DefaultTaskGroup.tsx +++ b/packages/module/src/pipelines/components/groups/DefaultTaskGroup.tsx @@ -1,209 +1,176 @@ import * as React from 'react'; import { observer } from 'mobx-react'; -import { css } from '@patternfly/react-styles'; -import styles from '../../../css/topology-components'; -import CollapseIcon from '@patternfly/react-icons/dist/esm/icons/compress-alt-icon'; -import NodeLabel from '../../../components/nodes/labels/NodeLabel'; -import { Layer } from '../../../components/layers'; -import { GROUPS_LAYER, TOP_LAYER } from '../../../const'; -import { maxPadding, useCombineRefs, useHover } from '../../../utils'; -import { BadgeLocation, GraphElement, isGraph, isNode, LabelPosition, Node, NodeStyle } from '../../../types'; -import { - useDragNode, - WithContextMenuProps, - WithDndDropProps, - WithDragNodeProps, - WithSelectionProps -} from '../../../behavior'; -import { CollapsibleGroupProps } from '../../../components'; +import { OnSelect, WithDndDragProps, ConnectDragSource, ConnectDropTarget, WithSelectionProps } from '../../../behavior'; +import { ShapeProps } from '../../../components'; +import { Dimensions } from '../../../geom'; +import { GraphElement, LabelPosition, BadgeLocation, isNode, Node } from '../../../types'; +import { getEdgesFromNodes, getSpacerNodes } from '../../utils'; +import DefaultTaskGroupCollapsed from './DefaultTaskGroupCollapsed'; +import DefaultTaskGroupExpanded from './DefaultTaskGroupExpanded'; -type DefaultTaskGroupProps = { +export interface EdgeCreationTypes { + spacerNodeType?: string, + edgeType?: string; + spacerEdgeType?: string; + finallyNodeTypes?: string[]; + finallyEdgeType?: string; +} + +interface PipelinesDefaultGroupProps { + /** Additional content added to the node */ + children?: React.ReactNode; + /** Additional classes added to the group */ className?: string; + /** The graph group node element to represent */ element: GraphElement; + /** Flag if the node accepts drop operations */ droppable?: boolean; + /** Flag if the current drag operation can be dropped on the node */ canDrop?: boolean; + /** Flag if the node is the current drop target */ dropTarget?: boolean; + /** Flag if the user is dragging the node */ dragging?: boolean; + /** Flag if drag operation is a regroup operation */ + dragRegroupable?: boolean; + /** Flag if the user is hovering on the node */ hover?: boolean; + /** Label for the node. Defaults to element.getLabel() */ label?: string; // Defaults to element.getLabel() + /** Secondary label for the node */ secondaryLabel?: string; + /** Flag to show the label */ showLabel?: boolean; // Defaults to true + /** Position of the label, top or bottom. Defaults to element.getLabelPosition() or bottom */ labelPosition?: LabelPosition; - truncateLength?: number; // Defaults to 13 + /** The maximum length of the label before truncation */ + truncateLength?: number; + /** The Icon class to show in the label, ignored when labelIcon is specified */ + labelIconClass?: string; + /** The label icon component to show in the label, takes precedence over labelIconClass */ + labelIcon?: string; + /** Padding for the label's icon */ + labelIconPadding?: number; + /** Text for the label's badge */ badge?: string; + /** Color to use for the label's badge background */ badgeColor?: string; + /** Color to use for the label's badge text */ badgeTextColor?: string; + /** Color to use for the label's badge border */ badgeBorderColor?: string; + /** Additional classes to use for the label's badge */ badgeClassName?: string; + /** Location of the badge relative to the label's text, inner or below */ badgeLocation?: BadgeLocation; - labelOffset?: number; // Space between the label and the group - labelIconClass?: string; // Icon to show in label - labelIcon?: string; - labelIconPadding?: number; -} & Partial; - -type DefaultTaskGroupInnerProps = Omit & { element: Node }; + /** Flag if the group is collapsible */ + collapsible?: boolean; + /** Width of the collapsed group */ + collapsedWidth?: number; + /** Height of the collapsed group */ + collapsedHeight?: number; + /** Callback when the group is collapsed */ + onCollapseChange?: (group: Node, collapsed: boolean) => void; + /** Shape of the collapsed group */ + getCollapsedShape?: (node: Node) => React.FunctionComponent; + /** Shadow offset for the collapsed group */ + collapsedShadowOffset?: number; + /** Flag if the element selected. Part of WithSelectionProps */ + selected?: boolean; + /** Function to call when the element should become selected (or deselected). Part of WithSelectionProps */ + onSelect?: OnSelect; + /** A ref to add to the node for dragging. Part of WithDragNodeProps */ + dragNodeRef?: WithDndDragProps['dndDragRef']; + /** A ref to add to the node for drag and drop. Part of WithDndDragProps */ + dndDragRef?: ConnectDragSource; + /** A ref to add to the node for dropping. Part of WithDndDropProps */ + dndDropRef?: ConnectDropTarget; + /** Function to call to show a context menu for the node */ + onContextMenu?: (e: React.MouseEvent) => void; + /** Flag indicating that the context menu for the node is currently open */ + contextMenuOpen?: boolean; + /** Function to return types used to re-create edges on a group collapse/expand (should be the same as calls to getEdgesFromNodes) */ + getEdgeCreationTypes?: () => { + spacerNodeType?: string, + edgeType?: string; + spacerEdgeType?: string; + finallyNodeTypes?: string[]; + finallyEdgeType?: string; + }; +} -const DefaultTaskGroupInner: React.FunctionComponent = observer( - ({ - className, - element, - collapsible, - selected, - onSelect, - hover, - label, - secondaryLabel, - showLabel = true, - truncateLength, - canDrop, - dropTarget, - onContextMenu, - contextMenuOpen, - dragging, - dragNodeRef, - badge, - badgeColor, - badgeTextColor, - badgeBorderColor, - badgeClassName, - badgeLocation, - labelOffset = 17, - labelIconClass, - labelIcon, - labelIconPadding, - onCollapseChange - }) => { - const [hovered, hoverRef] = useHover(); - const [labelHover, labelHoverRef] = useHover(); - const dragLabelRef = useDragNode()[1]; - const refs = useCombineRefs(hoverRef, dragNodeRef); - const isHover = hover !== undefined ? hover : hovered; - const labelPosition = element.getLabelPosition(); - - let parent = element.getParent(); - let altGroup = false; - while (!isGraph(parent)) { - altGroup = !altGroup; - parent = parent.getParent(); - } +type PipelinesDefaultGroupInnerProps = Omit & { element: Node } & WithSelectionProps; - const children = element.getNodes().filter((c) => c.isVisible()); +const DefaultTaskGroupInner: React.FunctionComponent = observer( + ({ className, element, onCollapseChange, getEdgeCreationTypes, ...rest }) => { + const childCount = element.getAllNodeChildren().length; - // cast to number and coerce - const padding = maxPadding(element.getStyle().padding ?? 17); + const handleCollapse = (group: Node, collapsed: boolean): void => { + if (collapsed && rest.collapsedWidth !== undefined && rest.collapsedHeight !== undefined) { + group.setDimensions(new Dimensions(rest.collapsedWidth, rest.collapsedHeight)); + } + group.setCollapsed(collapsed); - const { minX, minY, maxX, maxY } = children.reduce( - (acc, child) => { - const bounds = child.getBounds(); - return { - minX: Math.min(acc.minX, bounds.x - padding), - minY: Math.min(acc.minY, bounds.y - padding), - maxX: Math.max(acc.maxX, bounds.x + bounds.width + padding), - maxY: Math.max(acc.maxY, bounds.y + bounds.height + padding) - }; - }, - { minX: Infinity, minY: Infinity, maxX: 0, maxY: 0 } - ); + const controller = group.hasController() && group.getController(); + if (controller) { + const model = controller.toModel(); + const creationTypes: EdgeCreationTypes = getEdgeCreationTypes ? getEdgeCreationTypes() : {}; - const [labelX, labelY] = React.useMemo(() => { - if (!showLabel || !(label || element.getLabel())) { - return [0, 0]; + const pipelineNodes = model.nodes.filter((n) => n.type !== creationTypes.spacerNodeType).map((n) => ({ ...n, visible: true })); + const spacerNodes = getSpacerNodes(pipelineNodes, creationTypes.spacerNodeType, creationTypes.finallyNodeTypes); + const nodes = [...pipelineNodes, ...spacerNodes]; + const edges = getEdgesFromNodes( + pipelineNodes, + creationTypes.spacerNodeType, + creationTypes.edgeType, + creationTypes.edgeType, + creationTypes.finallyNodeTypes, + creationTypes.finallyEdgeType + ); + controller.fromModel({nodes, edges}, true); + controller.getGraph().layout(); } - switch (labelPosition) { - case LabelPosition.top: - return [minX + (maxX - minX) / 2, -minY + labelOffset]; - case LabelPosition.right: - return [maxX + labelOffset, minY + (maxY - minY) / 2]; - case LabelPosition.bottom: - default: - return [minX + (maxX - minX) / 2, maxY + labelOffset]; - } - }, [element, label, labelOffset, labelPosition, maxX, maxY, minX, minY, showLabel]); + onCollapseChange && onCollapseChange(group, collapsed); + }; - if (children.length === 0) { - return null; + if (element.isCollapsed()) { + return ( + + ); } - - const groupClassName = css( - styles.topologyGroup, - className, - altGroup && 'pf-m-alt-group', - canDrop && 'pf-m-highlight', - dragging && 'pf-m-dragging', - selected && 'pf-m-selected' - ); - const innerGroupClassName = css( - styles.topologyGroup, - className, - altGroup && 'pf-m-alt-group', - canDrop && 'pf-m-highlight', - dragging && 'pf-m-dragging', - selected && 'pf-m-selected', - (isHover || labelHover) && 'pf-m-hover', - canDrop && dropTarget && 'pf-m-drop-target' - ); - return ( - - - - - - - {showLabel && (label || element.getLabel()) && ( - - : undefined} - onActionIconClick={() => onCollapseChange(element, true)} - > - {label || element.getLabel()} - - - )} - + ); } ); -const DefaultTaskGroup: React.FunctionComponent = ({ +const DefaultTaskGroup: React.FunctionComponent = ({ element, - showLabel = true, - labelOffset = 17, ...rest -}: DefaultTaskGroupProps) => { +}: PipelinesDefaultGroupProps) => { if (!isNode(element)) { throw new Error('DefaultTaskGroup must be used only on Node elements'); } - return ; + + return ; }; export default DefaultTaskGroup; diff --git a/packages/module/src/pipelines/components/groups/DefaultTaskGroupCollapsed.tsx b/packages/module/src/pipelines/components/groups/DefaultTaskGroupCollapsed.tsx new file mode 100644 index 00000000..54cb2758 --- /dev/null +++ b/packages/module/src/pipelines/components/groups/DefaultTaskGroupCollapsed.tsx @@ -0,0 +1,51 @@ +import * as React from 'react'; +import { observer } from 'mobx-react'; +import ExpandIcon from '@patternfly/react-icons/dist/esm/icons/expand-alt-icon'; +import { WithDragNodeProps, WithSelectionProps, WithDndDropProps, WithContextMenuProps } from '../../../behavior'; +import { CollapsibleGroupProps } from "../../../components"; +import { LabelPosition, BadgeLocation, Node } from '../../../types'; +import { TaskNode } from '../nodes'; + +type DefaultTaskGroupCollapsedProps = { + children?: React.ReactNode; + className?: string; + element: Node; + droppable?: boolean; + canDrop?: boolean; + dropTarget?: boolean; + dragging?: boolean; + hover?: boolean; + label?: string; // Defaults to element.getLabel() + secondaryLabel?: string; + showLabel?: boolean; // Defaults to true + labelPosition?: LabelPosition; // Defaults to bottom + truncateLength?: number; // Defaults to 13 + labelIconClass?: string; // Icon to show in label + labelIcon?: string; + labelIconPadding?: number; + badge?: string; + badgeColor?: string; + badgeTextColor?: string; + badgeBorderColor?: string; + badgeClassName?: string; + badgeLocation?: BadgeLocation; +} & CollapsibleGroupProps & WithDragNodeProps & WithSelectionProps & WithDndDropProps & WithContextMenuProps; + +const DefaultTaskGroupCollapsed: React.FunctionComponent = ({ + element, + collapsible, + onCollapseChange, + ...rest +}) => { + + return ( + : undefined} + onActionIconClick={() => onCollapseChange(element, false)} + shadowCount={2} + /> + ); +}; + +export default observer(DefaultTaskGroupCollapsed); diff --git a/packages/module/src/pipelines/components/groups/DefaultTaskGroupExpanded.tsx b/packages/module/src/pipelines/components/groups/DefaultTaskGroupExpanded.tsx new file mode 100644 index 00000000..0249a485 --- /dev/null +++ b/packages/module/src/pipelines/components/groups/DefaultTaskGroupExpanded.tsx @@ -0,0 +1,197 @@ +import * as React from 'react'; +import { observer } from 'mobx-react'; +import { css } from '@patternfly/react-styles'; +import styles from '../../../css/topology-components'; +import CollapseIcon from '@patternfly/react-icons/dist/esm/icons/compress-alt-icon'; +import NodeLabel from '../../../components/nodes/labels/NodeLabel'; +import { Layer } from '../../../components/layers'; +import { GROUPS_LAYER, TOP_LAYER } from '../../../const'; +import { maxPadding, useCombineRefs, useHover } from '../../../utils'; +import { BadgeLocation, GraphElement, isGraph, LabelPosition, Node, NodeStyle } from '../../../types'; +import { + useDragNode, + WithContextMenuProps, + WithDndDropProps, + WithDragNodeProps, + WithSelectionProps +} from '../../../behavior'; +import { CollapsibleGroupProps } from '../../../components'; + +type DefaultTaskGroupProps = { + className?: string; + element: GraphElement; + droppable?: boolean; + canDrop?: boolean; + dropTarget?: boolean; + dragging?: boolean; + hover?: boolean; + label?: string; // Defaults to element.getLabel() + secondaryLabel?: string; + showLabel?: boolean; // Defaults to true + labelPosition?: LabelPosition; + truncateLength?: number; // Defaults to 13 + badge?: string; + badgeColor?: string; + badgeTextColor?: string; + badgeBorderColor?: string; + badgeClassName?: string; + badgeLocation?: BadgeLocation; + labelOffset?: number; // Space between the label and the group + labelIconClass?: string; // Icon to show in label + labelIcon?: string; + labelIconPadding?: number; +} & Partial; + +type DefaultTaskGroupInnerProps = Omit & { element: Node }; + +const DefaultTaskGroupExpanded: React.FunctionComponent = observer( + ({ + className, + element, + collapsible, + selected, + onSelect, + hover, + label, + secondaryLabel, + showLabel = true, + truncateLength, + canDrop, + dropTarget, + onContextMenu, + contextMenuOpen, + dragging, + dragNodeRef, + badge, + badgeColor, + badgeTextColor, + badgeBorderColor, + badgeClassName, + badgeLocation, + labelOffset = 17, + labelIconClass, + labelIcon, + labelIconPadding, + onCollapseChange + }) => { + const [hovered, hoverRef] = useHover(); + const [labelHover, labelHoverRef] = useHover(); + const dragLabelRef = useDragNode()[1]; + const refs = useCombineRefs(hoverRef, dragNodeRef); + const isHover = hover !== undefined ? hover : hovered; + const labelPosition = element.getLabelPosition(); + + let parent = element.getParent(); + let altGroup = false; + while (!isGraph(parent)) { + altGroup = !altGroup; + parent = parent.getParent(); + } + + const children = element.getNodes().filter((c) => c.isVisible()); + + // cast to number and coerce + const padding = maxPadding(element.getStyle().padding ?? 17); + + const { minX, minY, maxX, maxY } = children.reduce( + (acc, child) => { + const bounds = child.getBounds(); + return { + minX: Math.min(acc.minX, bounds.x - padding), + minY: Math.min(acc.minY, bounds.y - padding), + maxX: Math.max(acc.maxX, bounds.x + bounds.width + padding), + maxY: Math.max(acc.maxY, bounds.y + bounds.height + padding) + }; + }, + { minX: Infinity, minY: Infinity, maxX: 0, maxY: 0 } + ); + + const [labelX, labelY] = React.useMemo(() => { + if (!showLabel || !(label || element.getLabel())) { + return [0, 0]; + } + switch (labelPosition) { + case LabelPosition.top: + return [minX + (maxX - minX) / 2, -minY + labelOffset]; + case LabelPosition.right: + return [maxX + labelOffset, minY + (maxY - minY) / 2]; + case LabelPosition.bottom: + default: + return [minX + (maxX - minX) / 2, maxY + labelOffset]; + } + }, [element, label, labelOffset, labelPosition, maxX, maxY, minX, minY, showLabel]); + + if (children.length === 0) { + return null; + } + + const groupClassName = css( + styles.topologyGroup, + className, + altGroup && 'pf-m-alt-group', + canDrop && 'pf-m-highlight', + dragging && 'pf-m-dragging', + selected && 'pf-m-selected' + ); + const innerGroupClassName = css( + styles.topologyGroup, + className, + altGroup && 'pf-m-alt-group', + canDrop && 'pf-m-highlight', + dragging && 'pf-m-dragging', + selected && 'pf-m-selected', + (isHover || labelHover) && 'pf-m-hover', + canDrop && dropTarget && 'pf-m-drop-target' + ); + + return ( + + + + + + + {showLabel && (label || element.getLabel()) && ( + + : undefined} + onActionIconClick={() => onCollapseChange(element, true)} + > + {label || element.getLabel()} + + + )} + + ); + } +); + +export default DefaultTaskGroupExpanded; \ No newline at end of file diff --git a/packages/module/src/pipelines/components/groups/PipelinesDefaultGroup.tsx b/packages/module/src/pipelines/components/groups/PipelinesDefaultGroup.tsx deleted file mode 100644 index 1f67f5df..00000000 --- a/packages/module/src/pipelines/components/groups/PipelinesDefaultGroup.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import * as React from 'react'; -import { observer } from 'mobx-react'; -import { OnSelect, WithDndDragProps, ConnectDragSource, ConnectDropTarget, WithSelectionProps } from '../../../behavior'; -import { ShapeProps } from '../../../components'; -import { Dimensions } from '../../../geom'; -import { GraphElement, LabelPosition, BadgeLocation, isNode, Node } from '../../../types'; -import PipelinesDefaultGroupCollapsed from './PipelinesDefaultGroupCollapsed'; -import PipelinesDefaultGroupExpanded from './PipelinesDefaultGroupExpanded'; -import styles from '../../../css/topology-pipelines'; -import { css } from "@patternfly/react-styles"; -interface PipelinesDefaultGroupProps { - /** Additional content added to the node */ - children?: React.ReactNode; - /** Additional classes added to the group */ - className?: string; - /** The graph group node element to represent */ - element: GraphElement; - /** Flag if the node accepts drop operations */ - droppable?: boolean; - /** Flag if the current drag operation can be dropped on the node */ - canDrop?: boolean; - /** Flag if the node is the current drop target */ - dropTarget?: boolean; - /** Flag if the user is dragging the node */ - dragging?: boolean; - /** Flag if drag operation is a regroup operation */ - dragRegroupable?: boolean; - /** Flag if the user is hovering on the node */ - hover?: boolean; - /** Label for the node. Defaults to element.getLabel() */ - label?: string; // Defaults to element.getLabel() - /** Secondary label for the node */ - secondaryLabel?: string; - /** Flag to show the label */ - showLabel?: boolean; // Defaults to true - /** Position of the label, top or bottom. Defaults to element.getLabelPosition() or bottom */ - labelPosition?: LabelPosition; - /** The maximum length of the label before truncation */ - truncateLength?: number; - /** The Icon class to show in the label, ignored when labelIcon is specified */ - labelIconClass?: string; - /** The label icon component to show in the label, takes precedence over labelIconClass */ - labelIcon?: string; - /** Padding for the label's icon */ - labelIconPadding?: number; - /** Text for the label's badge */ - badge?: string; - /** Color to use for the label's badge background */ - badgeColor?: string; - /** Color to use for the label's badge text */ - badgeTextColor?: string; - /** Color to use for the label's badge border */ - badgeBorderColor?: string; - /** Additional classes to use for the label's badge */ - badgeClassName?: string; - /** Location of the badge relative to the label's text, inner or below */ - badgeLocation?: BadgeLocation; - /** Flag if the group is collapsible */ - collapsible?: boolean; - /** Width of the collapsed group */ - collapsedWidth?: number; - /** Height of the collapsed group */ - collapsedHeight?: number; - /** Callback when the group is collapsed */ - onCollapseChange?: (group: Node, collapsed: boolean) => void; - /** Shape of the collapsed group */ - getCollapsedShape?: (node: Node) => React.FunctionComponent; - /** Shadow offset for the collapsed group */ - collapsedShadowOffset?: number; - /** Flag if the element selected. Part of WithSelectionProps */ - selected?: boolean; - /** Function to call when the element should become selected (or deselected). Part of WithSelectionProps */ - onSelect?: OnSelect; - /** A ref to add to the node for dragging. Part of WithDragNodeProps */ - dragNodeRef?: WithDndDragProps['dndDragRef']; - /** A ref to add to the node for drag and drop. Part of WithDndDragProps */ - dndDragRef?: ConnectDragSource; - /** A ref to add to the node for dropping. Part of WithDndDropProps */ - dndDropRef?: ConnectDropTarget; - /** Function to call to show a context menu for the node */ - onContextMenu?: (e: React.MouseEvent) => void; - /** Flag indicating that the context menu for the node is currently open */ - contextMenuOpen?: boolean; - /** Flag indicating whether to use hull layout or rect layout for expanded groups. Defaults to hull (true) */ - hulledOutline?: boolean; -} - -type PipelinesDefaultGroupInnerProps = Omit & { element: Node } & WithSelectionProps; - -const PipelinesDefaultGroupInner: React.FunctionComponent = observer( - ({ className, element, onCollapseChange, ...rest }) => { - const childCount = element.getAllNodeChildren().length; - const handleCollapse = (group: Node, collapsed: boolean): void => { - if (collapsed && rest.collapsedWidth !== undefined && rest.collapsedHeight !== undefined) { - group.setDimensions(new Dimensions(rest.collapsedWidth, rest.collapsedHeight)); - } - group.setCollapsed(collapsed); - onCollapseChange && onCollapseChange(group, collapsed); - }; - - if (element.isCollapsed()) { - return ( - - ); - } - return ( - - ); - } -); - -const PipelinesDefaultGroup: React.FunctionComponent = ({ - element, - ...rest -}: PipelinesDefaultGroupProps) => { - if (!isNode(element)) { - throw new Error('DefaultGroup must be used only on Node elements'); - } - - return ; -}; - -export default PipelinesDefaultGroup; diff --git a/packages/module/src/pipelines/components/groups/PipelinesDefaultGroupCollapsed.tsx b/packages/module/src/pipelines/components/groups/PipelinesDefaultGroupCollapsed.tsx deleted file mode 100644 index bc4a992f..00000000 --- a/packages/module/src/pipelines/components/groups/PipelinesDefaultGroupCollapsed.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import * as React from 'react'; -import { observer } from 'mobx-react'; -import { css } from '@patternfly/react-styles'; -import styles from '@patternfly/react-topology/src/css/topology-pipelines'; -import ExpandIcon from '@patternfly/react-icons/dist/esm/icons/expand-alt-icon'; -import { WithDragNodeProps, WithSelectionProps, WithDndDropProps, WithContextMenuProps, useDragNode } from "../../../behavior"; -import { CollapsibleGroupProps, Stadium, Layer, PipelinesNodeLabel } from "../../../components"; -import { GROUPS_LAYER } from "../../../const"; -import { LabelPosition, BadgeLocation, Node } from "../../../types"; -import { useHover, useCombineRefs } from "../../../utils"; - -type PipelinesDefaultGroupCollapsedProps = { - children?: React.ReactNode; - className?: string; - element: Node; - droppable?: boolean; - canDrop?: boolean; - dropTarget?: boolean; - dragging?: boolean; - hover?: boolean; - label?: string; // Defaults to element.getLabel() - secondaryLabel?: string; - showLabel?: boolean; // Defaults to true - labelPosition?: LabelPosition; // Defaults to bottom - truncateLength?: number; // Defaults to 13 - labelIconClass?: string; // Icon to show in label - labelIcon?: string; - labelIconPadding?: number; - badge?: string; - badgeColor?: string; - badgeTextColor?: string; - badgeBorderColor?: string; - badgeClassName?: string; - badgeLocation?: BadgeLocation; -} & CollapsibleGroupProps & WithDragNodeProps & WithSelectionProps & WithDndDropProps & WithContextMenuProps; - -const PipelinesDefaultGroupCollapsed: React.FunctionComponent = ({ - className, - element, - collapsible, - selected, - onSelect, - children, - hover, - label, - showLabel = true, - truncateLength, - collapsedWidth, - collapsedHeight, - onCollapseChange, - collapsedShadowOffset = 8, - dragNodeRef, - canDrop, - dropTarget, - onContextMenu, - dragging, - labelPosition, - badge, - badgeColor, - badgeTextColor, - badgeBorderColor, - badgeClassName, - badgeLocation, - labelIconClass, - labelIcon, - labelIconPadding -}) => { - const [hovered, hoverRef] = useHover(); - const [labelHover, labelHoverRef] = useHover(); - const dragLabelRef = useDragNode()[1]; - const refs = useCombineRefs(hoverRef, dragNodeRef); - const isHover = hover !== undefined ? hover : hovered; - - const groupClassName = css( - styles.topologyPipelinesGroup, - className, - canDrop && 'pf-m-highlight', - canDrop && dropTarget && 'pf-m-drop-target', - dragging && 'pf-m-dragging', - selected && 'pf-m-selected' - ); - - return ( - - - - <> - - - - - - - {showLabel && ( - : undefined} - onActionIconClick={() => onCollapseChange(element, false)} - > - {label || element.getLabel()} - - )} - {children} - - ); -}; - -export default observer(PipelinesDefaultGroupCollapsed); diff --git a/packages/module/src/pipelines/components/groups/PipelinesDefaultGroupExpanded.tsx b/packages/module/src/pipelines/components/groups/PipelinesDefaultGroupExpanded.tsx deleted file mode 100644 index e71913ff..00000000 --- a/packages/module/src/pipelines/components/groups/PipelinesDefaultGroupExpanded.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import * as React from 'react'; -import { observer } from 'mobx-react'; -import { LabelPosition, Node } from '../../../types'; -import DefaultTaskGroup from './DefaultTaskGroup'; -import { CollapsibleGroupProps } from '../../../components'; - -type PipelinesDefaultGroupExpandedProps = { - className?: string; - element: Node; - labelPosition?: LabelPosition; - showLabel?: boolean; -} & CollapsibleGroupProps; - -const PipelinesDefaultGroupExpanded: React.FunctionComponent = ({ - element, - showLabel = true, - labelPosition = LabelPosition.top, - onCollapseChange, - ...rest -}) => { - return ( - - ); -}; - -export default observer(PipelinesDefaultGroupExpanded); diff --git a/packages/module/src/pipelines/components/groups/index.ts b/packages/module/src/pipelines/components/groups/index.ts index c4d5386b..5298db5f 100644 --- a/packages/module/src/pipelines/components/groups/index.ts +++ b/packages/module/src/pipelines/components/groups/index.ts @@ -1,4 +1,2 @@ export { default as DefaultTaskGroup } from './DefaultTaskGroup'; -export { default as PipelinesDefaultGroup } from './PipelinesDefaultGroup'; -export { default as PipelinesDefaultGroupCollapsed } from './PipelinesDefaultGroupCollapsed'; -export { default as PipelinesDefaultGroupExpanded } from './PipelinesDefaultGroupExpanded'; +export type { EdgeCreationTypes } from './DefaultTaskGroup'; diff --git a/packages/module/src/pipelines/components/nodes/TaskNode.tsx b/packages/module/src/pipelines/components/nodes/TaskNode.tsx index ce2c903d..4a8d1f23 100644 --- a/packages/module/src/pipelines/components/nodes/TaskNode.tsx +++ b/packages/module/src/pipelines/components/nodes/TaskNode.tsx @@ -109,6 +109,10 @@ export interface TaskNodeProps { onContextMenu?: (e: React.MouseEvent) => void; /** Flag indicating that the context menu for the node is currently open */ contextMenuOpen?: boolean; + /** Number of shadowed pills to show */ + shadowCount?: number; + /** Offset for each shadow */ + shadowOffset?: number; } type TaskNodeInnerProps = Omit & { element: Node }; @@ -153,6 +157,8 @@ const TaskNodeInner: React.FC = observer(({ actionIcon, actionIconClassName, onActionIconClick, + shadowCount = 0, + shadowOffset = 8, children }) => { const [hovered, hoverRef] = useHover(); @@ -417,6 +423,23 @@ const TaskNodeInner: React.FC = observer(({ ); } + + const shadows = []; + for (let i = shadowCount; i > 0; i--) { + shadows.push( + + + ) + } return ( = observer(({ ref={taskRef} > + {shadows} c.getId()) + }; + } + +}; + +export default BasePipelineNode; \ No newline at end of file diff --git a/packages/module/src/pipelines/elements/pipelineElementFactory.ts b/packages/module/src/pipelines/elements/pipelineElementFactory.ts new file mode 100644 index 00000000..9ab24530 --- /dev/null +++ b/packages/module/src/pipelines/elements/pipelineElementFactory.ts @@ -0,0 +1,13 @@ +import { ElementFactory, GraphElement, ModelKind } from '../../types'; +import BasePipelineNode from './BasePipelineNode'; + +const pipelineElementFactory: ElementFactory = (kind: ModelKind): GraphElement | undefined => { + switch (kind) { + case ModelKind.node: + return new BasePipelineNode(); + default: + return undefined; + } +}; + +export default pipelineElementFactory; diff --git a/packages/module/src/pipelines/utils/utils.ts b/packages/module/src/pipelines/utils/utils.ts index 4c60fb34..6aa340b4 100644 --- a/packages/module/src/pipelines/utils/utils.ts +++ b/packages/module/src/pipelines/utils/utils.ts @@ -59,6 +59,17 @@ const getSpacerId = (ids: string[]): string => return ref; }, ''); +const nodeVisible = (node: PipelineNodeModel, nodes: PipelineNodeModel[]): boolean => { + const parentNode = nodes.find((n) => n.children?.includes(node.id)); + if (!parentNode) { + return true; + } + if (parentNode.collapsed) { + return false; + } + return nodeVisible(parentNode, nodes); +}; + /** * parameters: * nodes: PipelineNodeModel[] - List of task and finally nodes in the model @@ -76,7 +87,7 @@ export const getSpacerNodes = ( interface ParallelNodeMap { [id: string]: PipelineNodeModel[]; } - const finallyNodes = nodes.filter(n => finallyNodeTypes.includes(n.type)); + const finallyNodes = nodes.filter(n => finallyNodeTypes.includes(n.type) && nodeVisible(n, nodes)); // Collect only multiple run-afters const multipleRunBeforeMap: ParallelNodeMap = nodes.reduce((acc, node) => { const { runAfterTasks } = node; @@ -132,19 +143,20 @@ export const getEdgesFromNodes = ( finallyEdgeType = DEFAULT_EDGE_TYPE ): EdgeModel[] => { const edges: EdgeModel[] = []; + const visibleNodes = nodes.filter(n => nodeVisible(n, nodes)); - const spacerNodes = nodes.filter(n => n.type === spacerNodeType); - const taskNodes = nodes.filter(n => n.type !== spacerNodeType); - const finallyNodes = nodes.filter(n => finallyNodeTypes.includes(n.type)); - const lastTasks = nodes + const spacerNodes = visibleNodes.filter(n => n.type === spacerNodeType); + const taskNodes = visibleNodes.filter(n => n.type !== spacerNodeType); + const finallyNodes = visibleNodes.filter(n => finallyNodeTypes.includes(n.type)); + const lastTasks = visibleNodes .filter(n => !finallyNodeTypes.includes(n.type)) .filter(n => spacerNodeType !== n.type) - .filter(t => !nodes.find(n => n.runAfterTasks?.includes(t.id))); + .filter(t => !visibleNodes.find(n => n.runAfterTasks?.includes(t.id))); spacerNodes.forEach(spacer => { const sourceIds = spacer.id.split('|'); sourceIds.forEach(sourceId => { - const node = nodes.find(n => n.id === sourceId); + const node = visibleNodes.find(n => n.id === sourceId); if (node && !finallyNodes.includes(node)) { edges.push({ id: `${sourceId}-${spacer.id}`, From 6ddbbac2993fa8a53009d0103962aa2a6fb83bc7 Mon Sep 17 00:00:00 2001 From: Jeffrey Phillips Date: Mon, 25 Mar 2024 12:22:37 -0400 Subject: [PATCH 4/9] Make recreateLayoutOnCollapseChange opt-in --- .../pipelineGroupsDemo/DemoTaskGroup.tsx | 1 + .../components/groups/DefaultTaskGroup.tsx | 44 +++++++++++-------- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/packages/demo-app-ts/src/demos/pipelineGroupsDemo/DemoTaskGroup.tsx b/packages/demo-app-ts/src/demos/pipelineGroupsDemo/DemoTaskGroup.tsx index 57e70f2d..86993844 100644 --- a/packages/demo-app-ts/src/demos/pipelineGroupsDemo/DemoTaskGroup.tsx +++ b/packages/demo-app-ts/src/demos/pipelineGroupsDemo/DemoTaskGroup.tsx @@ -60,6 +60,7 @@ const DemoTaskGroup: React.FunctionComponent = ({ element, . collapsedWidth={DEFAULT_TASK_WIDTH} collapsedHeight={DEFAULT_TASK_HEIGHT} element={element as Node} + recreateLayoutOnCollapseChange getEdgeCreationTypes={getEdgeCreationTypes} {...rest} /> diff --git a/packages/module/src/pipelines/components/groups/DefaultTaskGroup.tsx b/packages/module/src/pipelines/components/groups/DefaultTaskGroup.tsx index d5322666..695827f8 100644 --- a/packages/module/src/pipelines/components/groups/DefaultTaskGroup.tsx +++ b/packages/module/src/pipelines/components/groups/DefaultTaskGroup.tsx @@ -89,6 +89,8 @@ interface PipelinesDefaultGroupProps { onContextMenu?: (e: React.MouseEvent) => void; /** Flag indicating that the context menu for the node is currently open */ contextMenuOpen?: boolean; + /** Flag to recreate the layout when the group is expanded/collapsed. Be sure you are registering "pipelineElementFactory" when set to true. */ + recreateLayoutOnCollapseChange?: boolean; /** Function to return types used to re-create edges on a group collapse/expand (should be the same as calls to getEdgesFromNodes) */ getEdgeCreationTypes?: () => { spacerNodeType?: string, @@ -102,7 +104,7 @@ interface PipelinesDefaultGroupProps { type PipelinesDefaultGroupInnerProps = Omit & { element: Node } & WithSelectionProps; const DefaultTaskGroupInner: React.FunctionComponent = observer( - ({ className, element, onCollapseChange, getEdgeCreationTypes, ...rest }) => { + ({ className, element, onCollapseChange, recreateLayoutOnCollapseChange, getEdgeCreationTypes, ...rest }) => { const childCount = element.getAllNodeChildren().length; const handleCollapse = (group: Node, collapsed: boolean): void => { @@ -111,25 +113,31 @@ const DefaultTaskGroupInner: React.FunctionComponent n.type !== creationTypes.spacerNodeType).map((n) => ({ ...n, visible: true })); - const spacerNodes = getSpacerNodes(pipelineNodes, creationTypes.spacerNodeType, creationTypes.finallyNodeTypes); - const nodes = [...pipelineNodes, ...spacerNodes]; - const edges = getEdgesFromNodes( - pipelineNodes, - creationTypes.spacerNodeType, - creationTypes.edgeType, - creationTypes.edgeType, - creationTypes.finallyNodeTypes, - creationTypes.finallyEdgeType - ); - controller.fromModel({nodes, edges}, true); - controller.getGraph().layout(); + const pipelineNodes = model.nodes.filter((n) => n.type !== creationTypes.spacerNodeType).map((n) => ({ + ...n, + visible: true + })); + const spacerNodes = getSpacerNodes(pipelineNodes, creationTypes.spacerNodeType, creationTypes.finallyNodeTypes); + const nodes = [...pipelineNodes, ...spacerNodes]; + const edges = getEdgesFromNodes( + pipelineNodes, + creationTypes.spacerNodeType, + creationTypes.edgeType, + creationTypes.edgeType, + creationTypes.finallyNodeTypes, + creationTypes.finallyEdgeType + ); + controller.fromModel({nodes, edges}, true); + controller.getGraph().layout(); + } } + onCollapseChange && onCollapseChange(group, collapsed); }; From a84bd4d9ae7e11fec437e485d759fa642e5567e1 Mon Sep 17 00:00:00 2001 From: Jeffrey Phillips Date: Mon, 25 Mar 2024 12:27:21 -0400 Subject: [PATCH 5/9] Don't collapse groups in pipeline layout demo --- .../src/demos/pipelinesDemo/DemoPipelinesGroup.tsx | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/packages/demo-app-ts/src/demos/pipelinesDemo/DemoPipelinesGroup.tsx b/packages/demo-app-ts/src/demos/pipelinesDemo/DemoPipelinesGroup.tsx index ca0f6440..7adfb980 100644 --- a/packages/demo-app-ts/src/demos/pipelinesDemo/DemoPipelinesGroup.tsx +++ b/packages/demo-app-ts/src/demos/pipelinesDemo/DemoPipelinesGroup.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import { DefaultTaskGroup, - EdgeCreationTypes, GraphElement, LabelPosition, observer, @@ -10,7 +9,6 @@ import { WithDragNodeProps, WithSelectionProps, } from '@patternfly/react-topology'; -import { GROUPED_EDGE_TYPE } from './pipelineComponentFactory'; type DemoPipelinesGroupProps = { element: GraphElement; @@ -18,12 +16,6 @@ type DemoPipelinesGroupProps = { WithDragNodeProps & WithSelectionProps; -const getEdgeCreationTypes = (): EdgeCreationTypes => ({ - edgeType: GROUPED_EDGE_TYPE, - spacerEdgeType: GROUPED_EDGE_TYPE, - finallyEdgeType: GROUPED_EDGE_TYPE, -}); - const DemoPipelinesGroup: React.FunctionComponent = ({ element }) => { const data = element.getData(); const detailsLevel = element.getGraph().getDetailsLevel() @@ -31,13 +23,10 @@ const DemoPipelinesGroup: React.FunctionComponent = ({ return ( ); }; From 461f2ba361e0572c0a2e49c4724297b82e8a3794 Mon Sep 17 00:00:00 2001 From: Jenny <32821331+jenny-s51@users.noreply.github.com> Date: Mon, 25 Mar 2024 14:44:26 -0400 Subject: [PATCH 6/9] fix pill selection styling, change padding values to fix edge spacing --- .../demos/pipelineGroupsDemo/createDemoPipelineGroupsNodes.ts | 4 ++-- packages/module/src/css/topology-pipelines.css | 2 +- packages/module/src/pipelines/components/nodes/TaskNode.tsx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/demo-app-ts/src/demos/pipelineGroupsDemo/createDemoPipelineGroupsNodes.ts b/packages/demo-app-ts/src/demos/pipelineGroupsDemo/createDemoPipelineGroupsNodes.ts index ee0ef30a..db178242 100644 --- a/packages/demo-app-ts/src/demos/pipelineGroupsDemo/createDemoPipelineGroupsNodes.ts +++ b/packages/demo-app-ts/src/demos/pipelineGroupsDemo/createDemoPipelineGroupsNodes.ts @@ -1,10 +1,10 @@ /* eslint-disable camelcase */ import { PipelineNodeModel, RunStatus } from '@patternfly/react-topology'; -export const NODE_PADDING_VERTICAL = 15; +export const NODE_PADDING_VERTICAL = 0; export const NODE_PADDING_HORIZONTAL = 15; -export const GROUP_PADDING_VERTICAL = 50; +export const GROUP_PADDING_VERTICAL = 40; export const GROUP_PADDING_HORIZONTAL = 25; export const DEFAULT_TASK_WIDTH = 180; diff --git a/packages/module/src/css/topology-pipelines.css b/packages/module/src/css/topology-pipelines.css index e9b16d96..54374b78 100644 --- a/packages/module/src/css/topology-pipelines.css +++ b/packages/module/src/css/topology-pipelines.css @@ -156,7 +156,7 @@ fill: var(--pf-topology-pipelines__pill--Color); } -.pf-topology-pipelines__pill-background { +.pf-topology-pipelines__pill-background, .pf-topology-pipelines__pill-background-offset { fill: var(--pf-topology-pipelines__pill-background--Fill); stroke-width: var(--pf-topology-pipelines__pill-background--StrokeWidth); stroke: var(--pf-topology-pipelines__pill-background--Stroke); diff --git a/packages/module/src/pipelines/components/nodes/TaskNode.tsx b/packages/module/src/pipelines/components/nodes/TaskNode.tsx index 4a8d1f23..a85b3cd5 100644 --- a/packages/module/src/pipelines/components/nodes/TaskNode.tsx +++ b/packages/module/src/pipelines/components/nodes/TaskNode.tsx @@ -434,7 +434,7 @@ const TaskNodeInner: React.FC = observer(({ width={pillWidth} height={height} rx={height / 2} - className={css(styles.topologyPipelinesPillBackground)} + className={css(styles.topologyPipelinesPillBackgroundOffset)} filter={filter} /> From 1b62f1d61ee74876b3d1463c3f0a452ecf1c1d84 Mon Sep 17 00:00:00 2001 From: Jenny <32821331+jenny-s51@users.noreply.github.com> Date: Mon, 25 Mar 2024 16:46:12 -0400 Subject: [PATCH 7/9] feat(pipelines): add support for expandable/collapsible groups --- .../module/src/css/topology-pipelines.css | 82 +++++++++++++++++-- 1 file changed, 77 insertions(+), 5 deletions(-) diff --git a/packages/module/src/css/topology-pipelines.css b/packages/module/src/css/topology-pipelines.css index 54374b78..9d9f02b8 100644 --- a/packages/module/src/css/topology-pipelines.css +++ b/packages/module/src/css/topology-pipelines.css @@ -128,7 +128,7 @@ /* dark pill */ --pf-topology-pipelines__pill--Color: var(--pf-v5-global--Color--100); --pf-topology-pipelines__pill-background--Fill: var(--pf-v5-global--BackgroundColor--300); - --pf-topology-pipelines__pill-background--Stroke: var(--pf-v5-global--palette--black-300); + --pf-topology-pipelines__pill-background--Stroke: var(--pf-v5-global--palette--black-300); /* dark node labels */ --pf-topology-pipelines__pill-text--Color: var(--pf-v5-global--Color--100); @@ -156,7 +156,8 @@ fill: var(--pf-topology-pipelines__pill--Color); } -.pf-topology-pipelines__pill-background, .pf-topology-pipelines__pill-background-offset { +.pf-topology-pipelines__pill-background, +.pf-topology-pipelines__pill-background-offset { fill: var(--pf-topology-pipelines__pill-background--Fill); stroke-width: var(--pf-topology-pipelines__pill-background--StrokeWidth); stroke: var(--pf-topology-pipelines__pill-background--Stroke); @@ -242,12 +243,15 @@ .pf-topology-pipelines__pill.pf-m-hover.pf-m-danger .pf-topology-pipelines__pill-background { --pf-topology-pipelines__pill-background--StrokeWidth: var(--pf-v5-global--BorderWidth--md); } + .pf-topology-pipelines__pill.pf-m-hover.pf-m-success .pf-topology-pipelines__pill-background { --pf-topology-pipelines__pill-background--StrokeWidth: var(--pf-v5-global--BorderWidth--md); } + .pf-topology-pipelines__pill.pf-m-hover.pf-m-warning .pf-topology-pipelines__pill-background { --pf-topology-pipelines__pill-background--StrokeWidth: var(--pf-v5-global--BorderWidth--md); } + .pf-topology-pipelines__pill.pf-m-hover.pf-m-skipped .pf-topology-pipelines__pill-background { --pf-topology-pipelines__pill-background--StrokeWidth: var(--pf-v5-global--BorderWidth--md); } @@ -302,33 +306,43 @@ .pf-topology-pipelines__pill-status { color: var(--pf-topology-pipelines__pill-status--color); } + .pf-topology-pipelines__pill-status.pf-m-selected { --pf-topology-pipelines__pill-status--color: var(--pf-topology-pipelines__pill-status--m-selected--color); } + .pf-topology-pipelines__pill-status.pf-m-selected { --pf-topology-pipelines__pill-status--color: var(--pf-topology-pipelines__pill-status--m-selected--color); } + .pf-topology-pipelines__pill-status.pf-m-danger { --pf-topology-pipelines__pill-status--color: var(--pf-topology-pipelines__pill-status--m-danger--color); } + .pf-topology-pipelines__pill-status.pf-m-success { --pf-topology-pipelines__pill-status--color: var(--pf-topology-pipelines__pill-status--m-success--color); } + .pf-topology-pipelines__pill-status.pf-m-warning { --pf-topology-pipelines__pill-status--color: var(--pf-topology-pipelines__pill-status--m-warning--color); } + .pf-topology-pipelines__pill-status.pf-m-skipped { --pf-topology-pipelines__pill-status--color: var(--pf-topology-pipelines__pill-status--m-skipped--color); } + .pf-topology-pipelines__pill-status.pf-m-in-progress { --pf-topology-pipelines__pill-status--color: var(--pf-topology-pipelines__pill-status--m-in-progress--color); } + .pf-topology-pipelines__pill-status.pf-m-pending { --pf-topology-pipelines__pill-status--color: var(--pf-topology-pipelines__pill-status--m-pending--color); } + .pf-topology-pipelines__pill-status.pf-m-running { --pf-topology-pipelines__pill-status--color: var(--pf-topology-pipelines__pill-status--m-running--color); } + .pf-topology-pipelines__pill-status.pf-m-idle { --pf-topology-pipelines__pill-status--color: var(--pf-topology-pipelines__pill-status--m-idle--color); } @@ -336,27 +350,35 @@ .pf-topology-pipelines__pill-status.pf-m-selected.pf-m-danger { --pf-topology-pipelines__pill-status--color: var(--pf-topology-pipelines__pill-status--m-selected--m-danger--color); } + .pf-topology-pipelines__pill-status.pf-m-selected.pf-m-success { --pf-topology-pipelines__pill-status--color: var(--pf-topology-pipelines__pill-status--m-selected--m-success--color); } + .pf-topology-pipelines__pill-status.pf-m-selected.pf-m-warning { --pf-topology-pipelines__pill-status--color: var(--pf-topology-pipelines__pill-status--m-selected--m-warning--color); } + .pf-topology-pipelines__pill-status.pf-m-selected.pf-m-skipped { --pf-topology-pipelines__pill-status--color: var(--pf-topology-pipelines__pill-status--m-selected--m-skipped--color); } + .pf-topology-pipelines__pill-status.pf-m-selected.pf-m-in-progress { --pf-topology-pipelines__pill-status--color: var(--pf-topology-pipelines__pill-status--m-selected--m-in-progress--color); } + .pf-topology-pipelines__pill-status.pf-m-selected.pf-m-pending { --pf-topology-pipelines__pill-status--color: var(--pf-topology-pipelines__pill-status--m-selected--m-pending--color); } + .pf-topology-pipelines__pill-status.pf-m-selected.pf-m-running { --pf-topology-pipelines__pill-status--color: var(--pf-topology-pipelines__pill-status--m-selected--m-running--color); } + .pf-topology-pipelines__pill-status.pf-m-selected.pf-m-idle { --pf-topology-pipelines__pill-status--color: var(--pf-topology-pipelines__pill-status--m-selected--m-idle--color); } + .pf-topology-pipelines__pill-status.pf-m-spin { filter: blur(0); -webkit-filter: blur(0); @@ -364,11 +386,13 @@ transform-origin: center; animation: status-spin 2s infinite linear; } + @keyframes status-spin { 0% { -webkit-transform: rotate(0deg); transform: rotate(0deg); } + 100% { -webkit-transform: rotate(359deg); transform: rotate(359deg); @@ -379,37 +403,48 @@ fill: var(--pf-topology-pipelines__pill-badge--fill); stroke-width: 0; } + .pf-topology-pipelines__pill-badge text { fill: var(--pf-topology-pipelines__pill-badge--text--fill); color: var(--pf-topology__node--Color); } + .pf-topology-pipelines__pill.pf-m-selectable { cursor: pointer; } + .pf-topology-pipelines__pill.pf-m-selected .pf-topology__node__action-icon__icon { --pf-topology__node__action-icon__icon--Color: var(--pf-topology-pipelines__pill-text--m-selected--Color); } + .pf-topology-pipelines__pill.pf-m-selected.pf-m-danger .pf-topology__node__action-icon__icon { --pf-topology__node__action-icon__icon--Color: var(--pf-topology-pipelines__pill-text--m-selected--m-danger--Color); } + .pf-topology-pipelines__pill.pf-m-selected.pf-m-success .pf-topology__node__action-icon__icon { --pf-topology__node__action-icon__icon--Color: var(--pf-topology-pipelines__pill-text--m-selected--m-success--Color); } + .pf-topology-pipelines__pill.pf-m-selected.pf-m-warning .pf-topology__node__action-icon__icon { --pf-topology__node__action-icon__icon--Color: var(--pf-topology-pipelines__pill-text--m-selected--m-warning--Color); } + .pf-topology-pipelines__pill.pf-m-selected.pf-m-skipped .pf-topology__node__action-icon__icon { --pf-topology__node__action-icon__icon--Color: var(--pf-topology-pipelines__pill-text--m-selected--m-skipped--Color); } + .pf-topology-pipelines__pill.pf-m-selected.pf-m-in-progress .pf-topology__node__action-icon__icon { --pf-topology__node__action-icon__icon--Color: var(--pf-topology-pipelines__pill-text--m-selected--m-in-progress--Color); } + .pf-topology-pipelines__pill.pf-m-selected.pf-m-pending .pf-topology__node__action-icon__icon { --pf-topology__node__action-icon__icon--Color: var(--pf-topology-pipelines__pill-text--m-selected--m-pending--Color); } + .pf-topology-pipelines__pill.pf-m-selected.pf-m-running .pf-topology__node__action-icon__icon { --pf-topology__node__action-icon__icon--Color: var(--pf-topology-pipelines__pill-text--m-selected--m-running--Color); } + .pf-topology-pipelines__pill.pf-m-selected.pf-m-idle .pf-topology__node__action-icon__icon { --pf-topology__node__action-icon__icon--Color: var(--pf-topology-pipelines__pill-text--m-selected--m-idle--Color); } @@ -418,21 +453,26 @@ stroke: var(--pf-topology-pipelines__when-expression-background--Stroke); fill: var(--pf-topology-pipelines__when-expression-background--Fill); } + .pf-topology-pipelines__pill.pf-m-selected .pf-topology-pipelines__when-expression-background { --pf-topology-pipelines__when-expression-background--Stroke: var(--pf-topology-pipelines__when-expression--m-selected--color); } + .pf-topology-pipelines__when-expression-background.pf-m-success { --pf-topology-pipelines__when-expression-background--Stroke: var(--pf-topology-pipelines__when-expression--m-success--color); --pf-topology-pipelines__when-expression-background--Fill: var(--pf-topology-pipelines__when-expression--m-success--color); } + .pf-topology-pipelines__when-expression-background.pf-m-unmet { --pf-topology-pipelines__when-expression-background--Stroke: var(--pf-topology-pipelines__when-expression--m-warning--color); --pf-topology-pipelines__when-expression-background--Fill: var(--pf-topology-pipelines__when-expression--m-warning--color); - } +} + .pf-topology-pipelines__when-expression-background.pf-m-skipped { --pf-topology-pipelines__when-expression-background--Stroke: var(--pf-topology-pipelines__when-expression--m-skipped--color); --pf-topology-pipelines__when-expression-background--Fill: var(--pf-topology-pipelines__when-expression--m-skipped--color); } + .pf-topology-pipelines__when-expression-background.pf-m-in-progress { --pf-topology-pipelines__when-expression-background--Stroke: var(--pf-topology-pipelines__when-expression--m-in-progress--color); --pf-topology-pipelines__when-expression-background--Fill: var(--pf-topology-pipelines__when-expression--m-in-progress--color); @@ -445,33 +485,43 @@ .pf-topology-pipelines__status-icon { color: var(--pf-topology-pipelines__status-icon--color); } + .pf-topology-pipelines__status-icon.pf-m-selected { fill: var(--pf-topology-pipelines__status-icon--fill); } + .pf-topology-pipelines__status-icon.pf-m-selected { --pf-topology-pipelines__status-icon--color: var(--pf-topology-pipelines__status-icon--m-selected--color); } + .pf-topology-pipelines__status-icon.pf-m-danger { --pf-topology-pipelines__status-icon--color: var(--pf-topology-pipelines__status-icon--m-danger--color); } + .pf-topology-pipelines__status-icon.pf-m-success { --pf-topology-pipelines__status-icon--color: var(--pf-topology-pipelines__status-icon--m-success--color); } + .pf-topology-pipelines__status-icon.pf-m-warning { --pf-topology-pipelines__status-icon--color: var(--pf-topology-pipelines__status-icon--m-warning--color); } + .pf-topology-pipelines__status-icon.pf-m-skipped { --pf-topology-pipelines__status-icon--color: var(--pf-topology-pipelines__status-icon--m-skipped--color); } + .pf-topology-pipelines__status-icon.pf-m-in-progress { --pf-topology-pipelines__status-icon--color: var(--pf-topology-pipelines__status-icon--m-in-progress--color); } + .pf-topology-pipelines__status-icon.pf-m-pending { --pf-topology-pipelines__status-icon--color: var(--pf-topology-pipelines__status-icon--m-pending--color); } + .pf-topology-pipelines__status-icon.pf-m-running { --pf-topology-pipelines__status-icon--color: var(--pf-topology-pipelines__status-icon--m-running--color); } + .pf-topology-pipelines__status-icon.pf-m-idle { --pf-topology-pipelines__status-icon--color: var(--pf-topology-pipelines__status-icon--m-idle--color); } @@ -479,24 +529,31 @@ .pf-topology-pipelines__status-icon.pf-m-selected.pf-m-danger { --pf-topology-pipelines__status-icon--color: var(--pf-topology-pipelines__status-icon--m-selected--m-danger--color); } + .pf-topology-pipelines__status-icon.pf-m-selected.pf-m-success { --pf-topology-pipelines__status-icon--color: var(--pf-topology-pipelines__status-icon--m-selected--m-success--color); } + .pf-topology-pipelines__status-icon.pf-m-selected.pf-m-warning { --pf-topology-pipelines__status-icon--color: var(--pf-topology-pipelines__status-icon--m-selected--m-warning--color); } + .pf-topology-pipelines__status-icon.pf-m-selected.pf-m-skipped { --pf-topology-pipelines__status-icon--color: var(--pf-topology-pipelines__status-icon--m-selected--m-skipped--color); } + .pf-topology-pipelines__status-icon.pf-m-selected.pf-m-in-progress { --pf-topology-pipelines__status-icon--color: var(--pf-topology-pipelines__status-icon--m-selected--m-in-progress--color); } + .pf-topology-pipelines__status-icon.pf-m-selected.pf-m-pending { --pf-topology-pipelines__status-icon--color: var(--pf-topology-pipelines__status-icon--m-selected--m-pending--color); } + .pf-topology-pipelines__status-icon.pf-m-selected.pf-m-running { --pf-topology-pipelines__status-icon--color: var(--pf-topology-pipelines__status-icon--m-selected--m-running--color); } + .pf-topology-pipelines__status-icon.pf-m-selected.pf-m-idle { --pf-topology-pipelines__status-icon--color: var(--pf-topology-pipelines__status-icon--m-selected--m-idle--color); } @@ -509,24 +566,31 @@ .pf-topology-pipelines__status-icon-background.pf-m-danger { --pf-topology-pipelines__pill-background--Stroke: var(--pf-topology-pipelines__pill-background--m-danger--Stroke); } + .pf-topology-pipelines__status-icon-background.pf-m-success { --pf-topology-pipelines__pill-background--Stroke: var(--pf-topology-pipelines__pill-background--m-success--Stroke); } + .pf-topology-pipelines__status-icon-background.pf-m-warning { --pf-topology-pipelines__pill-background--Stroke: var(--pf-topology-pipelines__pill-background--m-warning--Stroke); } + .pf-topology-pipelines__status-icon-background.pf-m-skipped { --pf-topology-pipelines__pill-background--Stroke: var(--pf-topology-pipelines__pill-background--m-skipped--Stroke); } + .pf-topology-pipelines__status-icon-background.pf-m-in-progress { --pf-topology-pipelines__pill-background--Stroke: var(--pf-topology-pipelines__pill-background--m-in-progress--Stroke); } + .pf-topology-pipelines__status-icon-background.pf-m-pending { --pf-topology-pipelines__pill-background--Stroke: var(--pf-topology-pipelines__pill-background--m-pending--Stroke); } + .pf-topology-pipelines__status-icon-background.pf-m-running { --pf-topology-pipelines__pill-background--Stroke: var(--pf-topology-pipelines__pill-background--m-running--Stroke); } + .pf-topology-pipelines__status-icon-background.pf-m-idle { --pf-topology-pipelines__pill-background--Stroke: var(--pf-topology-pipelines__pill-background--m-idle--Stroke); } @@ -534,35 +598,43 @@ .pf-topology-pipelines__status-icon-background.pf-m-selected { --pf-topology-pipelines__pill-background--Fill: var(--pf-topology-pipelines__status-icon--m-selected--color); } + .pf-topology-pipelines__status-icon-background.pf-m-danger.pf-m-selected { --pf-topology-pipelines__pill-background--Fill: var(--pf-topology-pipelines__pill-background--m-danger--Stroke); } + .pf-topology-pipelines__status-icon-background.pf-m-success.pf-m-selected { --pf-topology-pipelines__pill-background--Fill: var(--pf-topology-pipelines__pill-background--m-success--Stroke); } + .pf-topology-pipelines__status-icon-background.pf-m-warning.pf-m-selected { --pf-topology-pipelines__pill-background--Fill: var(--pf-topology-pipelines__pill-background--m-warning--Stroke); } + .pf-topology-pipelines__status-icon-background.pf-m-skipped.pf-m-selected { --pf-topology-pipelines__pill-background--Fill: var(--pf-topology-pipelines__pill-background--m-skipped--Stroke); } + .pf-topology-pipelines__status-icon-background.pf-m-in-progress.pf-m-selected { --pf-topology-pipelines__pill-background--Fill: var(--pf-topology-pipelines__pill-background--m-in-progress--Stroke); } + .pf-topology-pipelines__status-icon-background.pf-m-pending.pf-m-selected { --pf-topology-pipelines__pill-background--Fill: var(--pf-topology-pipelines__pill-background--m-pending--Stroke); } + .pf-topology-pipelines__status-icon-background.pf-m-running.pf-m-selected { --pf-topology-pipelines__pill-background--Fill: var(--pf-topology-pipelines__pill-background--m-running--Stroke); } + .pf-topology-pipelines__status-icon-background.pf-m-idle.pf-m-selected { --pf-topology-pipelines__pill-background--Fill: var(--pf-topology-pipelines__pill-background--m-idle--Stroke); } .pf-topology-pipelines__group__collapsed .pf-topology__node__label__badge > rect, .pf-topology-pipelines__group__expanded .pf-topology__node__label__badge > rect { - stroke: var(--pf-v5-global--BorderColor--100); - fill: var(--pf-v5-global--palette--black-150); + stroke: var(--pf-v5-global--BorderColor--100); + fill: var(--pf-v5-global--palette--black-150); } .pf-topology__node__label__badge > text { From 4e25135ee4e8735331dd51e7c9a36c8da5d1d5c0 Mon Sep 17 00:00:00 2001 From: Jenny <32821331+jenny-s51@users.noreply.github.com> Date: Mon, 25 Mar 2024 21:10:23 -0400 Subject: [PATCH 8/9] feat(pipelines): add support for expandable/collapsible groups --- packages/module/src/css/topology-pipelines.css | 3 +-- packages/module/src/pipelines/components/nodes/TaskNode.tsx | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/module/src/css/topology-pipelines.css b/packages/module/src/css/topology-pipelines.css index 9d9f02b8..84d24e28 100644 --- a/packages/module/src/css/topology-pipelines.css +++ b/packages/module/src/css/topology-pipelines.css @@ -156,8 +156,7 @@ fill: var(--pf-topology-pipelines__pill--Color); } -.pf-topology-pipelines__pill-background, -.pf-topology-pipelines__pill-background-offset { +.pf-topology-pipelines__pill-background { fill: var(--pf-topology-pipelines__pill-background--Fill); stroke-width: var(--pf-topology-pipelines__pill-background--StrokeWidth); stroke: var(--pf-topology-pipelines__pill-background--Stroke); diff --git a/packages/module/src/pipelines/components/nodes/TaskNode.tsx b/packages/module/src/pipelines/components/nodes/TaskNode.tsx index a85b3cd5..c92599bc 100644 --- a/packages/module/src/pipelines/components/nodes/TaskNode.tsx +++ b/packages/module/src/pipelines/components/nodes/TaskNode.tsx @@ -434,8 +434,7 @@ const TaskNodeInner: React.FC = observer(({ width={pillWidth} height={height} rx={height / 2} - className={css(styles.topologyPipelinesPillBackgroundOffset)} - filter={filter} + className={css(topologyStyles.topologyNodeBackground, 'pf-m-disabled')} filter={filter} /> ) From 620de548acc943ac5d03bfa5b3f71aca1e5f5045 Mon Sep 17 00:00:00 2001 From: Jenny <32821331+jenny-s51@users.noreply.github.com> Date: Tue, 26 Mar 2024 11:03:40 -0400 Subject: [PATCH 9/9] feat(pipelines): add support for expandable/collapsible groups --- .../nodes/labels/PipelinesNodeLabel.tsx | 253 ------------------ .../src/components/nodes/labels/index.ts | 1 - 2 files changed, 254 deletions(-) delete mode 100644 packages/module/src/components/nodes/labels/PipelinesNodeLabel.tsx diff --git a/packages/module/src/components/nodes/labels/PipelinesNodeLabel.tsx b/packages/module/src/components/nodes/labels/PipelinesNodeLabel.tsx deleted file mode 100644 index 05f939d1..00000000 --- a/packages/module/src/components/nodes/labels/PipelinesNodeLabel.tsx +++ /dev/null @@ -1,253 +0,0 @@ -import * as React from 'react'; -import { css } from '@patternfly/react-styles'; -import styles from '../../../css/topology-components'; -import pipelineStyles from '../../../css/topology-pipelines'; -import { truncateMiddle } from '../../../utils/truncate-middle'; -import { createSvgIdUrl, useCombineRefs, useHover, useSize } from '../../../utils'; -import { WithContextMenuProps, WithDndDragProps } from '../../../behavior'; -import NodeShadows, { NODE_SHADOW_FILTER_ID_DANGER, NODE_SHADOW_FILTER_ID_HOVER } from '../NodeShadows'; -import LabelBadge from './LabelBadge'; -import LabelIcon from './LabelIcon'; -import LabelActionIcon from './LabelActionIcon'; -import { BadgeLocation, LabelPosition, NodeStatus } from '../../../types'; - -type PipelinesNodeLabelProps = { - children?: string; - className?: string; - paddingX?: number; - paddingY?: number; - x?: number; - y?: number; - position?: LabelPosition; - cornerRadius?: number; - status?: NodeStatus; - truncateLength?: number; // Defaults to 13 - labelIconClass?: string; // Icon to show in label - labelIcon?: React.ReactNode; - labelIconPadding?: number; - dragRef?: WithDndDragProps['dndDragRef']; - hover?: boolean; - dragging?: boolean; - edgeDragging?: boolean; - dropTarget?: boolean; - actionIcon?: React.ReactElement; - actionIconClassName?: string; - onActionIconClick?: (e: React.MouseEvent) => void; - badge?: string; - badgeColor?: string; - badgeTextColor?: string; - badgeBorderColor?: string; - badgeClassName?: string; - badgeLocation?: BadgeLocation; -} & Partial; - -/** - * Renders a `` component with a `` box behind. - */ -const PipelinesNodeLabel: React.FunctionComponent = ({ - children, - className, - paddingX = 0, - paddingY = 0, - cornerRadius = 22, - x = 0, - y = 0, - position = LabelPosition.bottom, - status, - badge, - badgeColor, - badgeTextColor, - badgeBorderColor, - badgeClassName, - badgeLocation = BadgeLocation.inner, - labelIconClass, - labelIcon, - labelIconPadding = 4, - truncateLength, - dragRef, - hover, - dragging, - edgeDragging, - dropTarget, - actionIcon, - actionIconClassName, - onActionIconClick, - ...other -}) => { - const [labelHover, labelHoverRef] = useHover(); - const refs = useCombineRefs(dragRef, typeof truncateLength === 'number' ? labelHoverRef : undefined); - - const [textSize, textRef] = useSize([children, truncateLength, className, labelHover]); - const [badgeSize, badgeRef] = useSize([badge]); - const [actionSize, actionRef] = useSize([actionIcon, paddingX]); - - const { - width, - height, - backgroundHeight, - startX, - startY, - badgeStartX, - badgeStartY, - actionStartX, - iconSpace, - badgeSpace - } = React.useMemo(() => { - if (!textSize) { - return { - width: 0, - height: 0, - backgroundHeight: 0, - startX: 0, - startY: 0, - badgeStartX: 0, - badgeStartY: 0, - actionStartX: 0, - contextStartX: 0, - iconSpace: 0, - badgeSpace: 0 - }; - } - const badgeSpace = badge && badgeSize && badgeLocation === BadgeLocation.inner ? badgeSize.width + paddingX : 0; - const height = Math.max(textSize.height, badgeSize?.height ?? 0) + paddingY * 2; - const iconSpace = labelIconClass || labelIcon ? (height + paddingY * 0.5) / 2 : 0; - const actionSpace = actionIcon && actionSize ? actionSize.width : 0; - const primaryWidth = iconSpace + badgeSpace + paddingX + textSize.width + actionSpace + paddingX; - const width = primaryWidth; - - let startX: number; - let startY: number; - if (position === LabelPosition.top) { - startX = x - width / 2; - startY = -y - height - paddingY; - } else if (position === LabelPosition.right) { - startX = x + iconSpace; - startY = y - height / 2; - } else if (position === LabelPosition.left) { - startX = - width - paddingX; - startY = y - height / 2 + paddingY; - } else { - startX = x - width / 2 + iconSpace / 2; - startY = y; - } - const actionStartX = iconSpace + badgeSpace + paddingX + textSize.width + paddingX; - const contextStartX = actionStartX + actionSpace; - const backgroundHeight = height; - let badgeStartX = 0; - let badgeStartY = 0; - if (badgeSize) { - if (badgeLocation === BadgeLocation.below) { - badgeStartX = (width - badgeSize.width) / 2; - badgeStartY = height + paddingY; - } else { - badgeStartX = iconSpace + paddingX; - badgeStartY = (height - badgeSize.height) / 2; - } - } - - return { - width, - height, - backgroundHeight, - startX, - startY, - actionStartX, - contextStartX, - badgeStartX, - badgeStartY, - iconSpace, - badgeSpace: badgeSize && badgeLocation === BadgeLocation.inner ? badgeSpace : 0 - }; - }, [ - textSize, - badge, - badgeSize, - badgeLocation, - paddingX, - paddingY, - labelIconClass, - labelIcon, - actionIcon, - actionSize, - position, - x, - y - ]); - - let filterId; - if (status === 'danger') { - filterId = NODE_SHADOW_FILTER_ID_DANGER; - } else if (hover || dragging || edgeDragging || dropTarget) { - filterId = NODE_SHADOW_FILTER_ID_HOVER; - } - - return ( - - - {textSize && ( - - )} - {textSize && badge && ( - - )} - {textSize && (labelIconClass || labelIcon) && ( - - )} - - {truncateLength > 0 && !labelHover ? truncateMiddle(children, { length: truncateLength }) : children} - - {textSize && actionIcon && ( - <> - - - - )} - - ); -}; - -export default PipelinesNodeLabel; diff --git a/packages/module/src/components/nodes/labels/index.ts b/packages/module/src/components/nodes/labels/index.ts index e16cc915..e47a24ad 100644 --- a/packages/module/src/components/nodes/labels/index.ts +++ b/packages/module/src/components/nodes/labels/index.ts @@ -3,5 +3,4 @@ export { default as LabelBadge } from './LabelBadge'; export { default as LabelContextMenu } from './LabelContextMenu'; export { default as LabelIcon } from './LabelIcon'; export { default as NodeLabel } from './NodeLabel'; -export { default as PipelinesNodeLabel } from './PipelinesNodeLabel';