diff --git a/packages/demo-app-ts/src/demos/pipelineGroupsDemo/DemoTaskGroup.tsx b/packages/demo-app-ts/src/demos/pipelineGroupsDemo/DemoTaskGroup.tsx index 2880632..8699384 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,14 @@ 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 65c13f9..4387104 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 9f4f687..7adfb98 100644 --- a/packages/demo-app-ts/src/demos/pipelinesDemo/DemoPipelinesGroup.tsx +++ b/packages/demo-app-ts/src/demos/pipelinesDemo/DemoPipelinesGroup.tsx @@ -1,78 +1,34 @@ import * as React from 'react'; import { + DefaultTaskGroup, 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 -} - 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 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/DemoTaskNode.tsx b/packages/demo-app-ts/src/demos/pipelinesDemo/DemoTaskNode.tsx index b65815d..4feeeb8 100644 --- a/packages/demo-app-ts/src/demos/pipelinesDemo/DemoTaskNode.tsx +++ b/packages/demo-app-ts/src/demos/pipelinesDemo/DemoTaskNode.tsx @@ -82,7 +82,6 @@ const DemoTaskNode: React.FunctionComponent = ({ badge={pipelineOptions.showBadges ? data.taskProgress : undefined} badgePopoverParams={pipelineOptions.showBadgeTooltips ? undefined : badgePopoverParams} badgeTooltip={pipelineOptions.showBadgeTooltips ? DEMO_TIP_TEXT : undefined} - hasWhenExpression={!!data.whenStatus} whenOffset={DEFAULT_WHEN_OFFSET} whenSize={DEFAULT_WHEN_SIZE} {...rest} diff --git a/packages/demo-app-ts/src/demos/pipelinesDemo/PipelineLayoutDemo.tsx b/packages/demo-app-ts/src/demos/pipelinesDemo/PipelineLayoutDemo.tsx index f27670c..6cb95af 100644 --- a/packages/demo-app-ts/src/demos/pipelinesDemo/PipelineLayoutDemo.tsx +++ b/packages/demo-app-ts/src/demos/pipelinesDemo/PipelineLayoutDemo.tsx @@ -30,8 +30,6 @@ import PipelineOptionsBar from './PipelineOptionsBar'; export const PIPELINE_NODE_SEPARATION_VERTICAL = 65; -export const LAYOUT_TITLE = 'Layout'; - const GROUP_PREFIX = 'Grouped_'; const VERTICAL_SUFFIX = '_Vertical'; const PIPELINE_LAYOUT = 'PipelineLayout'; diff --git a/packages/demo-app-ts/src/demos/pipelinesDemo/pipelineComponentFactory.tsx b/packages/demo-app-ts/src/demos/pipelinesDemo/pipelineComponentFactory.tsx index 1d1917b..b1302d3 100644 --- a/packages/demo-app-ts/src/demos/pipelinesDemo/pipelineComponentFactory.tsx +++ b/packages/demo-app-ts/src/demos/pipelinesDemo/pipelineComponentFactory.tsx @@ -20,7 +20,7 @@ import { import DemoTaskNode from './DemoTaskNode'; import DemoFinallyNode from './DemoFinallyNode'; import DemoTaskGroupEdge from './DemoTaskGroupEdge'; -import { DemoPipelinesGroup } from "./DemoPipelinesGroup"; +import DemoPipelinesGroup from './DemoPipelinesGroup'; export const GROUPED_EDGE_TYPE = 'GROUPED_EDGE'; diff --git a/packages/module/src/elements/BaseNode.ts b/packages/module/src/elements/BaseNode.ts index 2c8fd07..035a032 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 6673038..695827f 100644 --- a/packages/module/src/pipelines/components/groups/DefaultTaskGroup.tsx +++ b/packages/module/src/pipelines/components/groups/DefaultTaskGroup.tsx @@ -1,209 +1,184 @@ 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; + /** 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 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, + edgeType?: string; + spacerEdgeType?: string; + finallyNodeTypes?: string[]; + finallyEdgeType?: string; + }; +} -type DefaultTaskGroupInnerProps = Omit & { element: Node }; +type PipelinesDefaultGroupInnerProps = Omit & { element: Node } & WithSelectionProps; -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, onCollapseChange, recreateLayoutOnCollapseChange, getEdgeCreationTypes, ...rest }) => { + const childCount = element.getAllNodeChildren().length; - let parent = element.getParent(); - let altGroup = false; - while (!isGraph(parent)) { - altGroup = !altGroup; - parent = parent.getParent(); - } - - const children = element.getNodes().filter((c) => c.isVisible()); + 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); - // cast to number and coerce - const padding = maxPadding(element.getStyle().padding ?? 17); + if (recreateLayoutOnCollapseChange) { + const controller = group.hasController() && group.getController(); + if (controller) { + const model = controller.toModel(); + const creationTypes: EdgeCreationTypes = getEdgeCreationTypes ? getEdgeCreationTypes() : {}; - 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 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(); + } } - }, [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' - ); + onCollapseChange && onCollapseChange(group, collapsed); + }; + if (element.isCollapsed()) { + return ( + + ); + } 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 0000000..54cb275 --- /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 0000000..0249a48 --- /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 1f67f5d..0000000 --- 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 bc4a992..0000000 --- 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 e71913f..0000000 --- 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 c4d5386..5298db5 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 ce2c903..4a8d1f2 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 0000000..9ab2453 --- /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 d7a96ff..f40d8f4 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}`,