Skip to content

Commit

Permalink
Merge pull request #148 from jeff-phillips-18/vertical-pipelines
Browse files Browse the repository at this point in the history
feat(pipelines): Support top to bottom pipelines
  • Loading branch information
jeff-phillips-18 authored Feb 28, 2024
2 parents 445b709 + 6072423 commit cad0a7a
Show file tree
Hide file tree
Showing 14 changed files with 232 additions and 74 deletions.
18 changes: 11 additions & 7 deletions packages/demo-app-ts/src/demos/PipelineLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ import {
getEdgesFromNodes,
DEFAULT_EDGE_TYPE,
DEFAULT_SPACER_NODE_TYPE,
DEFAULT_FINALLY_NODE_TYPE
DEFAULT_FINALLY_NODE_TYPE,
TOP_TO_BOTTOM,
LEFT_TO_RIGHT
} from '@patternfly/react-topology';
import pipelineComponentFactory, { GROUPED_EDGE_TYPE } from '../components/pipelineComponentFactory';
import { usePipelineOptions } from '../utils/usePipelineOptions';
Expand All @@ -28,22 +30,23 @@ export const PIPELINE_NODE_SEPARATION_VERTICAL = 65;

export const LAYOUT_TITLE = 'Layout';

const GROUP_PREFIX = 'Grouped_';
const VERTICAL_SUFFIX = '_Vertical';
const PIPELINE_LAYOUT = 'PipelineLayout';
const GROUPED_PIPELINE_LAYOUT = 'GroupedPipelineLayout';

const TopologyPipelineLayout: React.FC = () => {
const [selectedIds, setSelectedIds] = React.useState<string[]>();

const controller = useVisualizationController();
const { contextToolbar, showContextMenu, showBadges, showIcons, showGroups, badgeTooltips } = usePipelineOptions(
const { contextToolbar, showContextMenu, showBadges, showIcons, showGroups, badgeTooltips, verticalLayout } = usePipelineOptions(
true
);
const pipelineNodes = useDemoPipelineNodes(
showContextMenu,
showBadges,
showIcons,
badgeTooltips,
'PipelineDagreLayout',
controller.getGraph().getLayout(),
showGroups
);

Expand All @@ -67,15 +70,15 @@ const TopologyPipelineLayout: React.FC = () => {
type: 'graph',
x: 25,
y: 25,
layout: showGroups ? GROUPED_PIPELINE_LAYOUT : PIPELINE_LAYOUT
layout: `${showGroups ? GROUP_PREFIX : ''}${PIPELINE_LAYOUT}${verticalLayout ? VERTICAL_SUFFIX : ''}`
},
nodes,
edges
},
true
);
controller.getGraph().layout();
}, [controller, pipelineNodes, showGroups]);
}, [controller, pipelineNodes, showGroups, verticalLayout]);

useEventListener<SelectionEventListener>(SELECTION_EVENT, ids => {
setSelectedIds(ids);
Expand All @@ -98,8 +101,9 @@ export const PipelineLayout = React.memo(() => {
(type: string, graph: Graph): Layout | undefined =>
new PipelineDagreLayout(graph, {
nodesep: PIPELINE_NODE_SEPARATION_VERTICAL,
rankdir: type.endsWith(VERTICAL_SUFFIX) ? TOP_TO_BOTTOM : LEFT_TO_RIGHT,
ranksep:
type === GROUPED_PIPELINE_LAYOUT ? GROUPED_PIPELINE_NODE_SEPARATION_HORIZONTAL : NODE_SEPARATION_HORIZONTAL,
type.startsWith(GROUP_PREFIX) ? GROUPED_PIPELINE_NODE_SEPARATION_HORIZONTAL : NODE_SEPARATION_HORIZONTAL,
ignoreGroups: true
})
);
Expand Down
3 changes: 2 additions & 1 deletion packages/demo-app-ts/src/demos/StatusConnectors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
Graph,
Layout,
LayoutFactory,
LEFT_TO_RIGHT,
NODE_SEPARATION_HORIZONTAL,
NodeShape,
SELECTION_EVENT,
Expand Down Expand Up @@ -52,7 +53,7 @@ const defaultLayoutFactory: LayoutFactory = (type: string, graph: Graph): Layout
ranksep: NODE_SEPARATION_HORIZONTAL,
edgesep: 100,
ranker: 'longest-path',
rankdir: 'LR',
rankdir: LEFT_TO_RIGHT,
marginx: 20,
marginy: 20,
});
Expand Down
19 changes: 13 additions & 6 deletions packages/demo-app-ts/src/utils/usePipelineOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,21 @@ import React from 'react';
import { Checkbox, ToolbarItem } from '@patternfly/react-core';

export const usePipelineOptions = (
allowGroups = false
isLayout = false,
): {
contextToolbar: React.ReactNode;
showContextMenu: boolean;
showBadges: boolean;
showIcons: boolean;
showGroups: boolean;
badgeTooltips: boolean;
verticalLayout: boolean;
} => {
const [showContextMenu, setShowContextMenu] = React.useState<boolean>(false);
const [showBadges, setShowBadges] = React.useState<boolean>(false);
const [showIcons, setShowIcons] = React.useState<boolean>(false);
const [showGroups, setShowGroups] = React.useState<boolean>(false);
const [verticalLayout, setVerticalLayout] = React.useState<boolean>(false);
const [badgeTooltips, setBadgeTooltips] = React.useState<boolean>(false);

const contextToolbar = (
Expand All @@ -31,13 +33,18 @@ export const usePipelineOptions = (
<ToolbarItem>
<Checkbox id="menus-switch" isChecked={showContextMenu} onChange={(_event, checked) => setShowContextMenu(checked)} label="Context menus" />
</ToolbarItem>
{allowGroups ? (
<ToolbarItem>
<Checkbox id="groups-switch" isChecked={showGroups} onChange={(_event, checked) => setShowGroups(checked)} label="Show groups" />
</ToolbarItem>
{isLayout ? (
<>
<ToolbarItem>
<Checkbox id="groups-switch" isChecked={showGroups} onChange={(_event, checked) => setShowGroups(checked)} label="Show groups" />
</ToolbarItem>
<ToolbarItem>
<Checkbox id="vertical-switch" isChecked={verticalLayout} onChange={(_event, checked) => setVerticalLayout(checked)} label="Vertical layout" />
</ToolbarItem>
</>
) : null}
</>
);

return { contextToolbar, showContextMenu, showBadges, showIcons, showGroups, badgeTooltips };
return { contextToolbar, showContextMenu, showBadges, showIcons, showGroups, badgeTooltips, verticalLayout };
};
11 changes: 11 additions & 0 deletions packages/module/src/elements/BaseGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
ScaleDetailsThresholds
} from '../types';
import BaseElement from './BaseElement';
import { LayoutOptions } from '../layouts';

export default class BaseGraph<E extends GraphModel = GraphModel, D = any> extends BaseElement<E, D>
implements Graph<E, D> {
Expand All @@ -34,6 +35,8 @@ export default class BaseGraph<E extends GraphModel = GraphModel, D = any> exten

private currentLayout?: Layout = undefined;

private layoutOptions?: LayoutOptions = undefined;

private scaleExtent: ScaleExtent = [0.25, 4];

constructor() {
Expand All @@ -44,6 +47,7 @@ export default class BaseGraph<E extends GraphModel = GraphModel, D = any> exten
| 'layers'
| 'scale'
| 'layoutType'
| 'layoutOptions'
| 'dimensions'
| 'position'
| 'scaleExtent'
Expand All @@ -55,6 +59,7 @@ export default class BaseGraph<E extends GraphModel = GraphModel, D = any> exten
layers: observable.ref,
scale: observable,
layoutType: observable,
layoutOptions: observable.deep,
dimensions: observable.ref,
position: observable.ref,
scaleExtent: observable.ref,
Expand Down Expand Up @@ -175,17 +180,23 @@ export default class BaseGraph<E extends GraphModel = GraphModel, D = any> exten
return this.layoutType;
}

getLayoutOptions(): LayoutOptions | undefined {
return this.layoutOptions;
}

setLayout(layout: string | undefined): void {
if (layout === this.layoutType) {
return;
}

if (this.currentLayout) {
this.currentLayout.destroy();
this.layoutOptions = undefined;
}

this.layoutType = layout;
this.currentLayout = layout ? this.getController().getLayout(layout) : undefined;
this.layoutOptions = this.currentLayout?.getLayoutOptions?.();
}

layout(): void {
Expand Down
4 changes: 4 additions & 0 deletions packages/module/src/layouts/BaseLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ export class BaseLayout implements Layout {
this.startListening();
}

getLayoutOptions(): LayoutOptions {
return this.options;
}

protected onSimulationEnd = () => {};

destroy(): void {
Expand Down
5 changes: 4 additions & 1 deletion packages/module/src/layouts/DagreLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import { DagreNode } from './DagreNode';
import { DagreGroup } from './DagreGroup';
import { DagreLink } from './DagreLink';

export const TOP_TO_BOTTOM = 'TB';
export const LEFT_TO_RIGHT = 'LR';

export type DagreLayoutOptions = LayoutOptions & dagre.GraphLabel & { ignoreGroups?: boolean };

export class DagreLayout extends BaseLayout implements Layout {
Expand All @@ -23,7 +26,7 @@ export class DagreLayout extends BaseLayout implements Layout {
nodesep: this.options.nodeDistance,
edgesep: this.options.linkDistance,
ranker: 'tight-tree',
rankdir: 'TB',
rankdir: TOP_TO_BOTTOM,
...options
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import { Node, ScaleDetailsLevel } from '../../../types';

export default class TaskNodeSourceAnchor<E extends Node = Node> extends AbstractAnchor {
private detailsLevel: ScaleDetailsLevel;
private lowDetailsStatusIconWidth = 0;
private lowDetailsStatusIconSize = 0;
private vertical = false;

constructor(owner: E, detailsLevel: ScaleDetailsLevel, lowDetailsStatusIconWidth: number) {
constructor(owner: E, detailsLevel: ScaleDetailsLevel, lowDetailsStatusIconSize: number, vertical: boolean = false) {
super(owner);
this.detailsLevel = detailsLevel;
this.lowDetailsStatusIconWidth = lowDetailsStatusIconWidth;
this.lowDetailsStatusIconSize = lowDetailsStatusIconSize;
this.vertical = vertical;
}

getLocation(): Point {
Expand All @@ -20,7 +22,13 @@ export default class TaskNodeSourceAnchor<E extends Node = Node> extends Abstrac
const bounds = this.owner.getBounds();
if (this.detailsLevel !== ScaleDetailsLevel.high) {
const scale = this.owner.getGraph().getScale();
return new Point(bounds.x + this.lowDetailsStatusIconWidth * (1 / scale), bounds.y + bounds.height / 2);
if (this.vertical) {
return new Point(bounds.x + (this.lowDetailsStatusIconSize / 2 + 2) * (1 / scale), bounds.bottom());
}
return new Point(bounds.x + this.lowDetailsStatusIconSize * (1 / scale), bounds.y + bounds.height / 2);
}
if (this.vertical) {
return new Point(bounds.x + bounds.width / 2, bounds.bottom());
}
return new Point(bounds.right(), bounds.y + bounds.height / 2);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { Point } from '../../../geom';
import { AbstractAnchor } from '../../../anchors';
import { Node } from '../../../types';
import { Node, ScaleDetailsLevel } from '../../../types';

export default class TaskNodeTargetAnchor<E extends Node = Node> extends AbstractAnchor {
private whenOffset = 0;
private detailsLevel: ScaleDetailsLevel;
private lowDetailsStatusIconSize = 0;
private vertical = false;

constructor(owner: E, whenOffset: number) {
constructor(owner: E, whenOffset: number, detailsLevel = ScaleDetailsLevel.high, lowDetailsStatusIconSize = 0, vertical = false) {
super(owner);
this.whenOffset = whenOffset;
this.detailsLevel = detailsLevel;
this.lowDetailsStatusIconSize = lowDetailsStatusIconSize;
this.vertical = vertical;
}

getLocation(): Point {
Expand All @@ -16,6 +22,14 @@ export default class TaskNodeTargetAnchor<E extends Node = Node> extends Abstrac

getReferencePoint(): Point {
const bounds = this.owner.getBounds();

if (this.vertical) {
if (this.detailsLevel !== ScaleDetailsLevel.high) {
const scale = this.owner.getGraph().getScale();
return new Point(bounds.x + (this.lowDetailsStatusIconSize / 2 + 2) * (1 / scale), bounds.y);
}
return new Point(bounds.x + bounds.width / 2, bounds.y);
}
return new Point(bounds.x + this.whenOffset, bounds.y + bounds.height / 2);
}
}
4 changes: 3 additions & 1 deletion packages/module/src/pipelines/components/edges/TaskEdge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { css } from '@patternfly/react-styles';
import styles from '../../../css/topology-components';
import { Edge, GraphElement, isEdge } from '../../../types';
import { integralShapePath } from '../../utils';
import { DagreLayoutOptions, TOP_TO_BOTTOM } from '../../../layouts';

interface TaskEdgeProps {
/** The graph edge element to represent */
Expand All @@ -24,11 +25,12 @@ const TaskEdgeInner: React.FunctionComponent<TaskEdgeInnerProps> = observer(({
const endPoint = element.getEndPoint();
const groupClassName = css(styles.topologyEdge, className);
const startIndent: number = element.getData()?.indent || 0;
const verticalLayout = (element.getGraph().getLayoutOptions?.() as DagreLayoutOptions)?.rankdir === TOP_TO_BOTTOM;

return (
<g data-test-id="task-handler" className={groupClassName} fillOpacity={0}>
<path
d={integralShapePath(startPoint, endPoint, startIndent, nodeSeparation)}
d={integralShapePath(startPoint, endPoint, startIndent, nodeSeparation, verticalLayout)}
transform="translate(0.5,0.5)"
shapeRendering="geometricPrecision"
/>
Expand Down
Loading

0 comments on commit cad0a7a

Please sign in to comment.