Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(pipelines): Add ability to scale collapsed pipeline groups #173

Merged
merged 3 commits into from
Apr 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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