Skip to content

Commit

Permalink
feat(pipelines): Add ability to scale collapsed pipeline groups (#173)
Browse files Browse the repository at this point in the history
* feat(pipelines): Add ability to scale collapsed pipeline groups

* Add optional chaining operator to condition

* Remove unused import

---------

Co-authored-by: Jenny <[email protected]>
  • Loading branch information
jeff-phillips-18 and jenny-s51 authored Apr 12, 2024
1 parent d2ac119 commit 3c689e9
Show file tree
Hide file tree
Showing 9 changed files with 139 additions and 131 deletions.
61 changes: 27 additions & 34 deletions packages/demo-app-ts/src/demos/pipelineGroupsDemo/DemoTaskGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,25 @@
import * as React from 'react';
import { observer } from 'mobx-react';
import {
AnchorEnd,
DagreLayoutOptions,
DefaultTaskGroup,
GraphElement,
isNode,
LabelPosition,
Node,
TOP_TO_BOTTOM,
useAnchor,
WithContextMenuProps,
WithSelectionProps,
ShapeProps,
WithDragNodeProps,
EdgeCreationTypes,
useHover,
ScaleDetailsLevel,
DEFAULT_LAYER,
Layer,
TOP_LAYER, GROUPS_LAYER
} from '@patternfly/react-topology';
import TaskGroupSourceAnchor from './TaskGroupSourceAnchor';
import TaskGroupTargetAnchor from './TaskGroupTargetAnchor';

type DemoTaskGroupProps = {
element: GraphElement;
collapsible?: boolean;
collapsedWidth?: number;
collapsedHeight?: number;
onCollapseChange?: (group: Node, collapsed: boolean) => void;
getCollapsedShape?: (node: Node) => React.FunctionComponent<ShapeProps>;
collapsedShadowOffset?: number; // defaults to 10
} & WithContextMenuProps &
WithDragNodeProps &
WithSelectionProps;
} & WithSelectionProps;

export const DEFAULT_TASK_WIDTH = 180;
export const DEFAULT_TASK_HEIGHT = 32;
Expand All @@ -41,29 +31,32 @@ const getEdgeCreationTypes = (): EdgeCreationTypes => ({

const DemoTaskGroup: React.FunctionComponent<DemoTaskGroupProps> = ({ element, ...rest }) => {
const verticalLayout = (element.getGraph().getLayoutOptions?.() as DagreLayoutOptions)?.rankdir === TOP_TO_BOTTOM;
const [hover, hoverRef] = useHover();
const detailsLevel = element.getGraph().getDetailsLevel();

useAnchor(
React.useCallback((node: Node) => new TaskGroupSourceAnchor(node, verticalLayout), [verticalLayout]),
AnchorEnd.source
);
useAnchor(
React.useCallback((node: Node) => new TaskGroupTargetAnchor(node, verticalLayout), [verticalLayout]),
AnchorEnd.target
);
if (!isNode(element)) {
return null;
}
const groupLayer = element.isCollapsed() ? DEFAULT_LAYER : GROUPS_LAYER;

return (
<DefaultTaskGroup
labelPosition={verticalLayout ? LabelPosition.top : LabelPosition.bottom}
collapsible
collapsedWidth={DEFAULT_TASK_WIDTH}
collapsedHeight={DEFAULT_TASK_HEIGHT}
element={element as Node}
recreateLayoutOnCollapseChange
getEdgeCreationTypes={getEdgeCreationTypes}
{...rest}
/>
<Layer id={detailsLevel !== ScaleDetailsLevel.high && hover ? TOP_LAYER : groupLayer}>
<g ref={hoverRef}>
<DefaultTaskGroup
labelPosition={verticalLayout ? LabelPosition.top : LabelPosition.bottom}
collapsible
collapsedWidth={DEFAULT_TASK_WIDTH}
collapsedHeight={DEFAULT_TASK_HEIGHT}
element={element as Node}
recreateLayoutOnCollapseChange
getEdgeCreationTypes={getEdgeCreationTypes}
scaleNode={hover && detailsLevel !== ScaleDetailsLevel.high}
showLabel={detailsLevel === ScaleDetailsLevel.high}
hideDetailsAtMedium
{...rest}
/>
</g>
</Layer>
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { AbstractAnchor, Point, Node } from '@patternfly/react-topology';
import { AbstractAnchor } from '../../../anchors';
import { Point } from '../../../geom';
import { Node } from '../../../types';

export default class TaskGroupSourceAnchor<E extends Node = Node> extends AbstractAnchor {
export default class TaskGroupSourceAnchor extends AbstractAnchor {
private vertical = false;

constructor(owner: E, vertical: boolean = true) {
constructor(owner: Node, vertical: boolean = true) {
super(owner);
this.vertical = vertical;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { AbstractAnchor, Point, Node } from '@patternfly/react-topology';
import { AbstractAnchor } from '../../../anchors';
import { Node } from '../../../types';
import { Point } from '../../../geom';

export default class TaskGroupTargetAnchor<E extends Node = Node> extends AbstractAnchor {
export default class TaskGroupTargetAnchor extends AbstractAnchor {
private vertical = false;

constructor(owner: E, vertical = false) {
constructor(owner: Node, vertical = false) {
super(owner);
this.vertical = vertical;
}
Expand Down
2 changes: 2 additions & 0 deletions packages/module/src/pipelines/components/anchors/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export { default as TaskNodeSourceAnchor } from './TaskNodeSourceAnchor';
export { default as TaskNodeTargetAnchor } from './TaskNodeTargetAnchor';
export { default as TaskGroupSourceAnchor } from './TaskGroupSourceAnchor';
export { default as TaskGroupTargetAnchor } from './TaskGroupTargetAnchor';
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
import * as React from 'react';
import { observer } from 'mobx-react';
import { OnSelect, WithDndDragProps, ConnectDragSource, ConnectDropTarget, WithSelectionProps } from '../../../behavior';
import {
OnSelect,
WithDndDragProps,
ConnectDragSource,
ConnectDropTarget,
} from '../../../behavior';
import { ShapeProps } from '../../../components';
import { Dimensions } from '../../../geom';
import { GraphElement, LabelPosition, BadgeLocation, isNode, Node } from '../../../types';
import { action } from '../../../mobx-exports';
import { getEdgesFromNodes, getSpacerNodes } from '../../utils';
import DefaultTaskGroupCollapsed from './DefaultTaskGroupCollapsed';
import DefaultTaskGroupExpanded from './DefaultTaskGroupExpanded';

export interface EdgeCreationTypes {
spacerNodeType?: string,
spacerNodeType?: string;
edgeType?: string;
spacerEdgeType?: string;
finallyNodeTypes?: string[];
finallyEdgeType?: string;
}

interface PipelinesDefaultGroupProps {
export interface DefaultTaskGroupProps {
/** Additional content added to the node */
children?: React.ReactNode;
/** Additional classes added to the group */
Expand All @@ -33,6 +39,10 @@ interface PipelinesDefaultGroupProps {
dragging?: boolean;
/** Flag if drag operation is a regroup operation */
dragRegroupable?: boolean;
/** Flag indicating the node should be scaled, best on hover of the node at lowest scale level */
scaleNode?: boolean;
/** Flag to hide details at medium scale */
hideDetailsAtMedium?: boolean;
/** Flag if the user is hovering on the node */
hover?: boolean;
/** Label for the node. Defaults to element.getLabel() */
Expand All @@ -45,6 +55,8 @@ interface PipelinesDefaultGroupProps {
labelPosition?: LabelPosition;
/** The maximum length of the label before truncation */
truncateLength?: number;
/** Space between the label and the group. Defaults to 17 */
labelOffset?: 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 */
Expand Down Expand Up @@ -73,6 +85,8 @@ interface PipelinesDefaultGroupProps {
onCollapseChange?: (group: Node, collapsed: boolean) => void;
/** Shape of the collapsed group */
getCollapsedShape?: (node: Node) => React.FunctionComponent<ShapeProps>;
/** Number of shadows to shop for collapse groups. Defaults to 2 */
collapsedShadowCount?: number;
/** Shadow offset for the collapsed group */
collapsedShadowOffset?: number;
/** Flag if the element selected. Part of WithSelectionProps */
Expand All @@ -93,21 +107,29 @@ interface PipelinesDefaultGroupProps {
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,
spacerNodeType?: string;
edgeType?: string;
spacerEdgeType?: string;
finallyNodeTypes?: string[];
finallyEdgeType?: string;
};
}

type PipelinesDefaultGroupInnerProps = Omit<PipelinesDefaultGroupProps, 'element'> & { element: Node } & WithSelectionProps;
type PipelinesDefaultGroupInnerProps = Omit<DefaultTaskGroupProps, 'element'> & { element: Node };

const DefaultTaskGroupInner: React.FunctionComponent<PipelinesDefaultGroupInnerProps> = observer(
({ className, element, onCollapseChange, recreateLayoutOnCollapseChange, getEdgeCreationTypes, ...rest }) => {
const DefaultTaskGroupInner: React.FunctionComponent<PipelinesDefaultGroupInnerProps> = observer(({
className,
element,
badge,
onCollapseChange,
collapsedShadowCount,
recreateLayoutOnCollapseChange,
getEdgeCreationTypes,
...rest
}) => {
const childCount = element.getAllNodeChildren().length;

const handleCollapse = (group: Node, collapsed: boolean): void => {
const handleCollapse = action((group: Node, collapsed: boolean): void => {
if (collapsed && rest.collapsedWidth !== undefined && rest.collapsedHeight !== undefined) {
group.setDimensions(new Dimensions(rest.collapsedWidth, rest.collapsedHeight));
}
Expand All @@ -120,9 +142,9 @@ const DefaultTaskGroupInner: React.FunctionComponent<PipelinesDefaultGroupInnerP
const creationTypes: EdgeCreationTypes = getEdgeCreationTypes ? getEdgeCreationTypes() : {};

const pipelineNodes = model.nodes.filter((n) => n.type !== creationTypes.spacerNodeType).map((n) => ({
...n,
visible: true
}));
...n,
visible: true
}));
const spacerNodes = getSpacerNodes(pipelineNodes, creationTypes.spacerNodeType, creationTypes.finallyNodeTypes);
const nodes = [...pipelineNodes, ...spacerNodes];
const edges = getEdgesFromNodes(
Expand All @@ -133,24 +155,22 @@ const DefaultTaskGroupInner: React.FunctionComponent<PipelinesDefaultGroupInnerP
creationTypes.finallyNodeTypes,
creationTypes.finallyEdgeType
);
controller.fromModel({nodes, edges}, true);
controller.fromModel({ nodes, edges }, true);
controller.getGraph().layout();
}
}

onCollapseChange && onCollapseChange(group, collapsed);
};
});

if (element.isCollapsed()) {
return (
<DefaultTaskGroupCollapsed
className={className}
element={element}
shadowCount={collapsedShadowCount}
onCollapseChange={handleCollapse}
badge={`${childCount}`}
badgeColor="#f5f5f5"
badgeBorderColor="#d2d2d2"
badgeTextColor="#000000"
badge={badge || `${childCount}`}
{...rest}
/>
);
Expand All @@ -161,24 +181,30 @@ const DefaultTaskGroupInner: React.FunctionComponent<PipelinesDefaultGroupInnerP
labelPosition={LabelPosition.top}
element={element}
onCollapseChange={handleCollapse}
badgeColor="#f5f5f5"
badgeBorderColor="#d2d2d2"
badgeTextColor="#000000"
{...rest}
/>
);
}
);

const DefaultTaskGroup: React.FunctionComponent<PipelinesDefaultGroupProps> = ({
const DefaultTaskGroup: React.FunctionComponent<DefaultTaskGroupProps> = ({
element,
badgeColor = '#f5f5f5',
badgeBorderColor = '#d2d2d2',
badgeTextColor = '#000000',
...rest
}: PipelinesDefaultGroupProps) => {
}: DefaultTaskGroupProps) => {
if (!isNode(element)) {
throw new Error('DefaultTaskGroup must be used only on Node elements');
}

return <DefaultTaskGroupInner element={element} {...rest} />;
return <DefaultTaskGroupInner
element={element}
badgeColor={badgeColor}
badgeBorderColor={badgeBorderColor}
badgeTextColor={badgeTextColor}
{...rest}
/>;
};

export default DefaultTaskGroup;
Original file line number Diff line number Diff line change
@@ -1,38 +1,19 @@
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 { Node } from '../../../types';
import { TaskNode } from '../nodes';
import { TaskNodeProps } from '../nodes/TaskNode';

type DefaultTaskGroupCollapsedProps = {
children?: React.ReactNode;
className?: string;
export type DefaultTaskGroupCollapsedProps = {
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;
shadowCount?: number;
} & Omit<TaskNodeProps, 'element'> & CollapsibleGroupProps;

const DefaultTaskGroupCollapsed: React.FunctionComponent<DefaultTaskGroupCollapsedProps> = ({
element,
shadowCount = 2,
collapsible,
onCollapseChange,
...rest
Expand All @@ -43,7 +24,8 @@ const DefaultTaskGroupCollapsed: React.FunctionComponent<DefaultTaskGroupCollaps
element={element} {...rest}
actionIcon={collapsible ? <ExpandIcon /> : undefined}
onActionIconClick={() => onCollapseChange(element, false)}
shadowCount={2}
shadowCount={shadowCount}
{...rest}
/>
);
};
Expand Down
Loading

0 comments on commit 3c689e9

Please sign in to comment.