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): Support top to bottom pipelines #148

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