From 997c80d5327dbb89fbd24810e1d68b19dd0075d4 Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 12 Mar 2024 09:50:29 +0000 Subject: [PATCH] WIP: sketchy start at supporting nester parallel branches in graph --- .../pipeline-graph/main/PipelineGraph.tsx | 2 +- .../main/PipelineGraphLayout.ts | 241 +++++++++++------- .../main/PipelineGraphModel.tsx | 9 +- .../pipeline-graph/main/support/nodes.tsx | 2 +- 4 files changed, 159 insertions(+), 95 deletions(-) diff --git a/src/main/frontend/pipeline-graph-view/pipeline-graph/main/PipelineGraph.tsx b/src/main/frontend/pipeline-graph-view/pipeline-graph/main/PipelineGraph.tsx index 3fff59330..b4923dcba 100644 --- a/src/main/frontend/pipeline-graph-view/pipeline-graph/main/PipelineGraph.tsx +++ b/src/main/frontend/pipeline-graph-view/pipeline-graph/main/PipelineGraph.tsx @@ -177,7 +177,7 @@ export class PipelineGraph extends React.Component { let nodes = []; for (const column of nodeColumns) { for (const row of column.rows) { - for (const node of row) { + for (const node of row.stages) { nodes.push(node); } } diff --git a/src/main/frontend/pipeline-graph-view/pipeline-graph/main/PipelineGraphLayout.ts b/src/main/frontend/pipeline-graph-view/pipeline-graph/main/PipelineGraphLayout.ts index df4737557..5f8ca046c 100644 --- a/src/main/frontend/pipeline-graph-view/pipeline-graph/main/PipelineGraphLayout.ts +++ b/src/main/frontend/pipeline-graph-view/pipeline-graph/main/PipelineGraphLayout.ts @@ -1,11 +1,14 @@ +import { BeachAccess } from "@mui/icons-material"; import { CompositeConnection, PositionedGraph } from "./PipelineGraphModel"; import { + PlaceholderNodeInfo, NodeColumn, NodeLabelInfo, LayoutInfo, StageInfo, NodeInfo, + NodeRow, } from "./PipelineGraphModel"; export const sequentialStagesLabelOffset = 70; @@ -26,7 +29,7 @@ export function layoutGraph( ): PositionedGraph { const stageNodeColumns = createNodeColumns(newStages, collasped); const { nodeSpacingH, ypStart } = layout; - + const startNode: NodeInfo = { x: 0, y: 0, @@ -48,9 +51,9 @@ export function layoutGraph( }; const allNodeColumns: Array = [ - { rows: [[startNode]], centerX: 0, hasBranchLabels: false, startX: 0 }, // Column X positions calculated later + { rows: [{beforeId: undefined, stages: [startNode], nestedRows: [], after: undefined}], centerX: 0, hasBranchLabels: false, startX: 0 }, // Column X positions calculated later ...stageNodeColumns, - { rows: [[endNode]], centerX: 0, hasBranchLabels: false, startX: 0 }, + { rows: [{beforeId: undefined, stages: [endNode], nestedRows: [], after: undefined}], centerX: 0, hasBranchLabels: false, startX: 0 }, ]; positionNodes(allNodeColumns, layout); @@ -66,7 +69,7 @@ export function layoutGraph( for (const column of allNodeColumns) { for (const row of column.rows) { - for (const node of row) { + for (const node of row.stages) { measuredWidth = Math.max(measuredWidth, node.x + nodeSpacingH / 2); measuredHeight = Math.max(measuredHeight, node.y + ypStart); } @@ -84,6 +87,7 @@ export function layoutGraph( }; } + /** * Generate an array of columns, based on the top-level stages */ @@ -109,48 +113,69 @@ export function createNodeColumns( }; }; - const processTopStage = (topStage: StageInfo, willRecurse: boolean) => { + const createColumn = (topStage: StageInfo, rows:Array=[], childBranches:number=0): NodeColumn => { + console.log(`Creating column for: ${topStage.name}`); + return { + topStage, + rows: rows, + centerX: 0, // Layout is done later + startX: 0, + childBranches: 0, + hasBranchLabels: false, // set below + } as NodeColumn; + } + + const processTopStage = (topStage: StageInfo, before: number) => { // If stage has children, we don't draw a node for it, just its children const stagesForColumn = - !willRecurse && stageHasChildren(topStage) + stageHasChildren(topStage) ? topStage.children : [topStage]; - const column: NodeColumn = { - topStage, - rows: [], - centerX: 0, // Layout is done later - startX: 0, - hasBranchLabels: false, // set below - }; + const column = createColumn(topStage); + nodeColumns.push(column); + processChildStages(stagesForColumn, column, before); + }; + + const processChildStages = (stagesForColumn: StageInfo[], column: NodeColumn, beforeId?: number)=> { for (const nodeStage of stagesForColumn) { - const rowNodes: Array = []; - if (!collasped && !willRecurse && stageHasChildren(nodeStage)) { - column.hasBranchLabels = true; - forEachChildStage(nodeStage, (parentStage, childStage, _) => - rowNodes.push(makeNodeForStage(childStage, parentStage.name)) - ); + const nodeRow = {beforeId: beforeId, stages: [], nestedRows: [], after: null} as NodeRow; + column.rows.push(nodeRow); + if (stageHasChildren(nodeStage) && nodeStage.children[0].type != "PARALLEL") { + console.log(`Found parallel branch: ${nodeStage.name}`); + column.hasBranchLabels = true; + processBranchStage(nodeStage, column, nodeRow) } else { - rowNodes.push(makeNodeForStage(nodeStage)); + nodeRow.stages.push(makeNodeForStage(nodeStage)); + console.log(`Found parallel stage: ${nodeStage.name}`); } - column.rows.push(rowNodes); if (collasped) { - break; + return; } } + } + + const processBranchStage = (nodeStage: StageInfo, column: NodeColumn, nodeRow: NodeRow, before?: number): void => { + for (const childStage of nodeStage.children) { + console.log(`Found parallel branch -: ${nodeStage.name}`); + nodeRow.stages.push(makeNodeForStage(childStage, `${nodeStage.name}`)); + processChildStages(childStage.children, column, before); + } + + } - nodeColumns.push(column); - }; for (const protoTopStage of topLevelStages) { const selfParentTopStage = { ...protoTopStage, children: [protoTopStage] }; - - forEachChildStage(selfParentTopStage, (_, topStage, willRecurse) => - processTopStage(topStage, willRecurse) + forEachChildStage(selfParentTopStage, (parent, topStage, willRecurse) => + processTopStage(topStage, parent.id) ); } + // TODO@: REMOVE Useful for debugging structure: + console.log(nodeColumns); + return nodeColumns; } @@ -169,7 +194,8 @@ function stageHasChildren(stage: StageInfo): boolean { */ function forEachChildStage( topStage: StageInfo, - callback: (parent: StageInfo, child: StageInfo, willRecurse: boolean) => void + callback: (parent: StageInfo, child: StageInfo, willRecurse: boolean) => void, + allowRecursion: boolean = false ) { if (!stageHasChildren(topStage)) { return; @@ -179,8 +205,7 @@ function forEachChildStage( if (stage.type == "PIPELINE_START") { continue; } - const needToRecurse = - stageHasChildren(stage) && stage.children[0].type != "PARALLEL"; + const needToRecurse = allowRecursion && stageHasChildren(stage); callback(topStage, stage, needToRecurse); if (needToRecurse) { forEachChildStage(stage, callback); @@ -199,13 +224,13 @@ function positionNodes( let previousTopNode: NodeInfo | undefined; for (const column of nodeColumns) { - const topNode = column.rows[0][0]; + const topNode = column.rows[0].stages[0]; let yp = ypStart; // Reset Y to top for each column if (previousTopNode) { // Advance X position - if (previousTopNode.isPlaceholder || topNode.isPlaceholder) { + if (previousTopNode.isPlaceholder && topNode && !topNode.isPlaceholder) { // Don't space placeholder nodes (start/end) as wide as normal. xp += Math.floor(nodeSpacingH * 0.7); } else { @@ -213,44 +238,57 @@ function positionNodes( } } - let widestRow = 0; - for (const row of column.rows) { - widestRow = Math.max(widestRow, row.length); - } - const xpStart = xp; // Remember the left-most position in this column - // Make room for row labels if (column.hasBranchLabels) { xp += sequentialStagesLabelOffset; } let maxX = xp; + positionChildNodes(column.rows, xp, yp, maxX, parallelSpacingH, nodeSpacingV) - for (const row of column.rows) { - let nodeX = xp; // Start nodes at current column xp (not xpstart as that includes branch label) + column.centerX = Math.round((xpStart + maxX) / 2); + column.startX = xpStart; // Record on column for use later to position branch labels + xp = maxX; // Make sure we're at the end of the widest row for this column before next loop + + previousTopNode = topNode; + } +} + +const getWidestRow = (rows: NodeRow[], offset:number=0): number => { + let widestRow = 0; + for (const row of rows) { + widestRow = Math.max(widestRow, row.stages.length); + for (const nestedRow of row.nestedRows) { + widestRow = Math.max(widestRow, getWidestRow(row.nestedRows, row.stages.length)); + } + } + return widestRow +} + +const positionChildNodes = (rows: NodeRow[], xOffset: number, yOffset: number, maxX: number, parallelSpacingH: number, nodeSpacingV: number ) => { + let widestRow = getWidestRow(rows, 0); + + for (const row of rows) { + let nodeX = xOffset; // Start nodes at current column xp (not xpstart as that includes branch label) // Offset the beginning of narrower rows towards column center - nodeX += Math.round((widestRow - row.length) * parallelSpacingH * 0.5); + nodeX += Math.round((widestRow - row.stages.length) * parallelSpacingH * 0.5); - for (const node of row) { + for (const node of row.stages) { maxX = Math.max(maxX, nodeX); node.x = nodeX; - node.y = yp; + node.y = yOffset; nodeX += parallelSpacingH; // Space out nodes in each row } - yp += nodeSpacingV; // LF - } - - column.centerX = Math.round((xpStart + maxX) / 2); - column.startX = xpStart; // Record on column for use later to position branch labels - xp = maxX; // Make sure we're at the end of the widest row for this column before next loop + positionChildNodes(row.nestedRows, xOffset, yOffset + nodeSpacingV, maxX, parallelSpacingH, nodeSpacingV); - previousTopNode = topNode; - } + yOffset += nodeSpacingV; // LF + } } + /** * Generate label descriptions for big labels at the top of each column */ @@ -258,25 +296,27 @@ function createBigLabels(columns: Array): Array { const labels: Array = []; for (const column of columns) { - const node = column.rows[0][0]; - const stage = column.topStage; - const text = stage ? stage.name : node.name; - const key = "l_b_" + node.key; + const node = column.rows[0].stages[0]; + if (node) { + const stage = column.topStage; + const text = stage ? stage.name : node.name; + const key = "l_b_" + node.key; + + // bigLabel is located above center of column, but offset if there's branch labels + let x = column.centerX; + if (column.hasBranchLabels) { + x += Math.floor(sequentialStagesLabelOffset / 2); + } - // bigLabel is located above center of column, but offset if there's branch labels - let x = column.centerX; - if (column.hasBranchLabels) { - x += Math.floor(sequentialStagesLabelOffset / 2); + labels.push({ + x, + y: node.y, + node, + stage, + text, + key, + }); } - - labels.push({ - x, - y: node.y, - node, - stage, - text, - key, - }); } return labels; @@ -295,7 +335,7 @@ function createSmallLabels( } for (const column of columns) { for (const row of column.rows) { - for (const node of row) { + for (const node of row.stages) { // We add small labels to parallel nodes only so skip others if (node.isPlaceholder || node.stage === column.topStage) { continue; @@ -336,7 +376,7 @@ function createBranchLabels( for (const column of columns) { if (column.hasBranchLabels) { for (const row of column.rows) { - const firstNode = row[0]; + const firstNode = row.stages[0]; if (!firstNode.isPlaceholder && firstNode.seqContainerName) { labels.push({ x: column.startX, @@ -367,35 +407,52 @@ function createConnections( for (const column of columns) { if (column.topStage && column.topStage.state === "skipped") { - skippedNodes.push(column.rows[0][0]); + skippedNodes.push(column.rows[0].stages[0]); continue; } - // Connections to each row in this column - if (sourceNodes.length) { + connections.push(...createRowConnections(column.rows, sourceNodes, skippedNodes, column.hasBranchLabels, false)); + + sourceNodes = column.rows.filter((row) => row.stages.length > 0).map((row) => row.stages[row.stages.length - 1]); // Last node of each row + skippedNodes = []; + } + + return connections; +} + +function createRowConnections( + rows: Array, + sourceNodes: Array, + skippedNodes: Array, + hasBranchLabels: boolean, + collasped: boolean +): Array { + const connections: Array = []; + // Connections to each row in this column + if (sourceNodes.length && rows.filter((row) => row.stages.length > 0).length > 0) { + connections.push({ + sourceNodes, + destinationNodes: rows.filter((row) => row.stages.length > 0).map((row) => row.stages[0]), // First node of each row + skippedNodes: skippedNodes, + hasBranchLabels: hasBranchLabels, + }); + } + + // Simple horizontal connections between nodes within each row + for (const row of rows) { + for (let i = 0; i < row.stages.length - 1; i++) { connections.push({ - sourceNodes, - destinationNodes: column.rows.map((row) => row[0]), // First node of each row - skippedNodes: skippedNodes, - hasBranchLabels: column.hasBranchLabels, + sourceNodes: [row.stages[i]], + destinationNodes: [row.stages[i + 1]], + skippedNodes: [], + hasBranchLabels: false, }); } - - // Simple horizontal connections between nodes within each row - for (const row of column.rows) { - for (let i = 0; i < row.length - 1; i++) { - connections.push({ - sourceNodes: [row[i]], - destinationNodes: [row[i + 1]], - skippedNodes: [], - hasBranchLabels: false, - }); - } + let rowSourceNodes = rows.filter((row) => row.stages.length > 0).map((row) => row.stages[row.stages.length - 1]); // Last node of each row + for (const nestedRow of row.nestedRows) { + connections.push(...createRowConnections(row.nestedRows, rowSourceNodes, [], hasBranchLabels, collasped)); } - - sourceNodes = column.rows.map((row) => row[row.length - 1]); // Last node of each row - skippedNodes = []; } return connections; -} +} \ No newline at end of file diff --git a/src/main/frontend/pipeline-graph-view/pipeline-graph/main/PipelineGraphModel.tsx b/src/main/frontend/pipeline-graph-view/pipeline-graph/main/PipelineGraphModel.tsx index 3140846fa..16af6a346 100644 --- a/src/main/frontend/pipeline-graph-view/pipeline-graph/main/PipelineGraphModel.tsx +++ b/src/main/frontend/pipeline-graph-view/pipeline-graph/main/PipelineGraphModel.tsx @@ -94,9 +94,16 @@ export interface PlaceholderNodeInfo extends BaseNodeInfo { export type NodeInfo = StageNodeInfo | PlaceholderNodeInfo; +export interface NodeRow { + beforeId?: number, + stages: NodeInfo[], + nestedRows: NodeRow[], + afterId?: number, +} + export interface NodeColumn { topStage?: StageInfo; // Top-most stage for this column, which will have no rendered nodes if it's parallel - rows: Array>; + rows: Array; centerX: number; // Center X position, for positioning top bigLabel hasBranchLabels: boolean; startX: number; // Where to put the branch labels, or if none, the center of the left-most node(s) diff --git a/src/main/frontend/pipeline-graph-view/pipeline-graph/main/support/nodes.tsx b/src/main/frontend/pipeline-graph-view/pipeline-graph/main/support/nodes.tsx index 037cb9cef..d4d646bbb 100644 --- a/src/main/frontend/pipeline-graph-view/pipeline-graph/main/support/nodes.tsx +++ b/src/main/frontend/pipeline-graph-view/pipeline-graph/main/support/nodes.tsx @@ -99,7 +99,7 @@ export function SelectionHighlight({ columnLoop: for (const column of nodeColumns) { for (const row of column.rows) { - for (const node of row) { + for (const node of row.stages) { if (node.isPlaceholder === false && isStageSelected(node.stage)) { selectedNode = node; break columnLoop;