[JENKINS-49050] Add support for sequential parallel stages (#1784)
* add support for sequential parallel stages * update sequential stages unit test * fix bugs * fix pipeline graph while pipeline is running
This commit is contained in:
parent
9cd5d47167
commit
5e1fed1828
|
@ -91,6 +91,19 @@ function convertJenkinsNodeDetails(jenkinsNode, isCompleted, skewMillis = 0) {
|
|||
return converted;
|
||||
}
|
||||
|
||||
function buildSequentialStages(originalNodes, convertedNodes, sequentialNodeKey, currentNode) {
|
||||
const nextSequentialNodeId = originalNodes[sequentialNodeKey].edges[0] ? originalNodes[sequentialNodeKey].edges[0].id : '';
|
||||
|
||||
currentNode.isSequential = true;
|
||||
if (nextSequentialNodeId) {
|
||||
if (originalNodes[sequentialNodeKey].edges.length && originalNodes[nextSequentialNodeId].firstParent == currentNode.id) {
|
||||
currentNode.nextSibling = convertedNodes[nextSequentialNodeId];
|
||||
|
||||
buildSequentialStages(originalNodes, convertedNodes, currentNode.nextSibling.id, currentNode.nextSibling);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the graph results of a run as reported by Jenkins into the model required by the PipelineGraph component
|
||||
*
|
||||
|
@ -109,7 +122,6 @@ export function convertJenkinsNodeGraph(jenkinsGraph, isCompleted, skewMillis) {
|
|||
const edgeCountToNode = {}; // id => int
|
||||
// const edgeCountFromNode = {}; // id => int
|
||||
let firstNode = undefined;
|
||||
|
||||
// Convert the basic details of nodes, and index them by id
|
||||
jenkinsGraph.forEach(jenkinsNode => {
|
||||
const convertedNode = convertJenkinsNodeDetails(jenkinsNode, isCompleted, skewMillis);
|
||||
|
@ -138,7 +150,9 @@ export function convertJenkinsNodeGraph(jenkinsGraph, isCompleted, skewMillis) {
|
|||
// Follow the graph and build our results
|
||||
let currentNode = firstNode;
|
||||
while (currentNode) {
|
||||
results.push(currentNode);
|
||||
if (!currentNode.isSequential) {
|
||||
results.push(currentNode);
|
||||
}
|
||||
|
||||
let nextNode = null;
|
||||
const originalNode = originalNodeForId[currentNode.id];
|
||||
|
@ -150,6 +164,7 @@ export function convertJenkinsNodeGraph(jenkinsGraph, isCompleted, skewMillis) {
|
|||
|
||||
if (edges.length === 1 && parallelNodes.length === 0) {
|
||||
// Single following (sibling) node
|
||||
|
||||
nextNode = convertedNodeForId[edges[0].id];
|
||||
} else if (parallelNodes.length > 0) {
|
||||
// Multiple following nodes are child nodes (parallel branch) not siblings
|
||||
|
@ -165,6 +180,18 @@ export function convertJenkinsNodeGraph(jenkinsGraph, isCompleted, skewMillis) {
|
|||
|
||||
for (const branchNode of branchNodes) {
|
||||
const branchNodeEdges = originalNodeForId[branchNode.id].edges || [];
|
||||
|
||||
Object.keys(convertedNodeForId).map((key, index) => {
|
||||
//Check if this stage contains sequential stages and if so, replace it with the first one in the sequence
|
||||
if (originalNodeForId[key].firstParent === branchNode.id) {
|
||||
currentNode.children[0] = convertedNodeForId[key];
|
||||
|
||||
if (originalNodeForId[key].edges[0]) {
|
||||
buildSequentialStages(originalNodeForId, convertedNodeForId, key, currentNode.children[0]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (branchNodeEdges.length > 0) {
|
||||
// Should only be 0 at end of pipeline or bad input data
|
||||
const followingNode = convertedNodeForId[branchNodeEdges[0].id];
|
||||
|
@ -246,7 +273,6 @@ export default class PipelineRunGraph extends Component {
|
|||
}
|
||||
|
||||
const id = this.props.selectedStage.id;
|
||||
|
||||
let selectedStage = null;
|
||||
|
||||
// Find selected stage by id
|
||||
|
@ -255,9 +281,15 @@ export default class PipelineRunGraph extends Component {
|
|||
selectedStage = topStage;
|
||||
} else {
|
||||
for (const child of topStage.children) {
|
||||
if (child.id === id) {
|
||||
selectedStage = child;
|
||||
break;
|
||||
let currentStage = child;
|
||||
|
||||
while (currentStage) {
|
||||
if (currentStage.id === id) {
|
||||
selectedStage = currentStage;
|
||||
break;
|
||||
}
|
||||
|
||||
currentStage = currentStage.nextSibling;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -323,12 +323,16 @@ export default class Pipeline extends Component {
|
|||
let nodeRestartId = this.pager.currentNode.restartable ? this.pager.currentNode.id : '';
|
||||
let nodeRestartTitle = this.pager.currentNode.restartable ? title : '';
|
||||
|
||||
if (this.pager.currentNode.restartable == false && this.pager.currentNode.type == 'PARALLEL') {
|
||||
const currentNodeParent = this.pager.nodes.data.model.filter(node => node.id == this.pager.currentNode.parent)[0];
|
||||
if (this.pager.currentNode.restartable == false) {
|
||||
let currentNodeParent = this.pager.nodes.data.model.filter(node => node.id == this.pager.currentNode.firstParent)[0];
|
||||
|
||||
if (currentNodeParent.restartable) {
|
||||
nodeRestartId = currentNodeParent.id;
|
||||
nodeRestartTitle = currentNodeParent.title;
|
||||
while (currentNodeParent) {
|
||||
if (currentNodeParent && currentNodeParent.restartable) {
|
||||
nodeRestartId = currentNodeParent.id;
|
||||
nodeRestartTitle = currentNodeParent.title;
|
||||
break;
|
||||
}
|
||||
currentNodeParent = this.pager.nodes.data.model.filter(node => node.id == currentNodeParent.firstParent)[0];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,8 @@ import { logging } from '@jenkins-cd/blueocean-core-js';
|
|||
import { prefixIfNeeded } from '../../urls/prefixIfNeeded';
|
||||
import { KaraokeApi } from '../../index';
|
||||
|
||||
import { convertJenkinsNodeGraph } from '../../../PipelineRunGraph';
|
||||
|
||||
const logger = logging.logger('io.jenkins.blueocean.dashboard.karaoke.Pager.Pipeline');
|
||||
|
||||
/**
|
||||
|
@ -111,7 +113,9 @@ export class PipelinePager {
|
|||
} else {
|
||||
// Actually we should only come here on a not running job
|
||||
logger.debug('Actually we should only come here on a not running job');
|
||||
const lastNode = logData.data.model[logData.data.model.length - 1];
|
||||
const convertedNodes = convertJenkinsNodeGraph(logData.data.model, result.isFinished, 0);
|
||||
const lastNode = logData.data.model.filter(node => node.id === convertedNodes[convertedNodes.length - 1].id)[0];
|
||||
|
||||
this.currentNode = lastNode;
|
||||
}
|
||||
this.currentStepsUrl = prefixIfNeeded(this.currentNode._links.steps.href);
|
||||
|
|
|
@ -66,6 +66,7 @@ export const getNodesInformation = nodes => {
|
|||
logUrl: hasLogs ? logActions[0]._links.self.href : undefined,
|
||||
isParallel,
|
||||
parent,
|
||||
firstParent: item.firstParent || undefined,
|
||||
isRunning,
|
||||
isCompleted,
|
||||
computedResult,
|
||||
|
|
|
@ -572,7 +572,7 @@ export class PipelineGraph extends Component {
|
|||
*/
|
||||
stageIsSelected(stage?: StageInfo) {
|
||||
const { selectedStage } = this.state;
|
||||
return selectedStage && selectedStage === stage;
|
||||
return selectedStage && stage && selectedStage.id === stage.id;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -585,16 +585,18 @@ export class PipelineGraph extends Component {
|
|||
|
||||
if (children && selectedStage) {
|
||||
for (const childStage of children) {
|
||||
let testee = childStage;
|
||||
while (testee) {
|
||||
if (testee === selectedStage) {
|
||||
let currentStage = childStage;
|
||||
|
||||
while (currentStage) {
|
||||
if (currentStage.id === selectedStage.id) {
|
||||
return true;
|
||||
}
|
||||
testee = testee.nextSibling;
|
||||
currentStage = currentStage.nextSibling;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -219,7 +219,7 @@ function createSmallLabels(columns: Array<NodeColumn>) {
|
|||
for (const row of column.rows) {
|
||||
for (const node of row) {
|
||||
// We add small labels to parallel nodes only so skip others
|
||||
if (!node.stage || node.stage.type !== 'PARALLEL') {
|
||||
if (!node.stage || (node.stage.type !== 'PARALLEL' && node.stage.isSequential !== true)) {
|
||||
continue;
|
||||
}
|
||||
const label: LabelInfo = {
|
||||
|
|
|
@ -11,17 +11,21 @@ const validResultValues = StatusIndicator.validResultValues;
|
|||
// Data creation helpers Lifted from stories
|
||||
let __id = 1111;
|
||||
|
||||
function makeNode(name, children = [], state = validResultValues.not_built, type='STAGE', completePercent) {
|
||||
completePercent = completePercent || ((state == validResultValues.running) ? Math.floor(Math.random() * 60 + 20) : 50);
|
||||
function makeNode(name, children = [], state = validResultValues.not_built, type = 'STAGE', completePercent) {
|
||||
completePercent = completePercent || (state == validResultValues.running ? Math.floor(Math.random() * 60 + 20) : 50);
|
||||
const id = __id++;
|
||||
return {name, children, state, completePercent, id, type};
|
||||
return { name, children, state, completePercent, id, type };
|
||||
}
|
||||
|
||||
function makeSequence(...stages) {
|
||||
for (let i = 0; i < stages.length - 1; i++) {
|
||||
stages[i].nextSibling = stages[i + 1];
|
||||
stages[i].isSequential = true;
|
||||
}
|
||||
|
||||
//also mark the last node in the sequence as sequential
|
||||
stages[stages.length - 1].isSequential = true;
|
||||
|
||||
return stages[0]; // The model only needs the first in a sequence
|
||||
}
|
||||
|
||||
|
@ -50,7 +54,7 @@ function assertRow(row, ...nodesParams) {
|
|||
}
|
||||
}
|
||||
|
||||
function assertLabel(labels, text, x,y) {
|
||||
function assertLabel(labels, text, x, y) {
|
||||
const label = labels.find(label => label.text === text);
|
||||
assert.ok(label, `label ${text} exists`);
|
||||
assert.equal(label.x, x, `label ${text} x`);
|
||||
|
@ -67,12 +71,11 @@ function assertConnection(connections, sourceName, destinationName) {
|
|||
}
|
||||
}
|
||||
|
||||
assert.fail(0,1,`could not find ${sourceName} --> ${destinationName} connection`);
|
||||
assert.fail(0, 1, `could not find ${sourceName} --> ${destinationName} connection`);
|
||||
}
|
||||
|
||||
describe('PipelineGraph', () => {
|
||||
describe('layoutGraph', () => {
|
||||
|
||||
it('gracefully handles a Stage with null children', () => {
|
||||
const stagesNullChildren = require('../data/pipeline-graph/stages-with-null-children.json');
|
||||
const { nodeColumns } = layoutGraph(stagesNullChildren, defaultLayout);
|
||||
|
@ -96,23 +99,20 @@ describe('PipelineGraph', () => {
|
|||
]),
|
||||
makeNode('Skizzled', [], validResultValues.skipped),
|
||||
makeNode('Foshizzle', [], validResultValues.skipped),
|
||||
makeNode('Dev', [
|
||||
makeNode('US-East', [], validResultValues.success, 'PARALLEL'),
|
||||
makeNode('US-West', [], validResultValues.success, 'PARALLEL'),
|
||||
makeNode('APAC', [], validResultValues.success, 'PARALLEL'),
|
||||
], validResultValues.success),
|
||||
makeNode(
|
||||
'Dev',
|
||||
[
|
||||
makeNode('US-East', [], validResultValues.success, 'PARALLEL'),
|
||||
makeNode('US-West', [], validResultValues.success, 'PARALLEL'),
|
||||
makeNode('APAC', [], validResultValues.success, 'PARALLEL'),
|
||||
],
|
||||
validResultValues.success
|
||||
),
|
||||
makeNode('Staging', [], validResultValues.skipped),
|
||||
makeNode('Production'),
|
||||
];
|
||||
|
||||
const {
|
||||
nodeColumns,
|
||||
connections,
|
||||
bigLabels,
|
||||
smallLabels,
|
||||
measuredWidth,
|
||||
measuredHeight,
|
||||
} = layoutGraph(stages, defaultLayout);
|
||||
const { nodeColumns, connections, bigLabels, smallLabels, measuredWidth, measuredHeight } = layoutGraph(stages, defaultLayout);
|
||||
|
||||
// Basic stuff
|
||||
|
||||
|
@ -269,30 +269,20 @@ describe('PipelineGraph', () => {
|
|||
|
||||
it('lays out a multi-stage parallel graph', () => {
|
||||
const stages = [
|
||||
makeNode("Alpha"),
|
||||
makeNode("Bravo", [
|
||||
makeNode("Echo", [], validResultValues.not_built, 'PARALLEL'),
|
||||
makeNode('Alpha'),
|
||||
makeNode('Bravo', [
|
||||
makeNode('Echo', [], validResultValues.not_built, 'PARALLEL'),
|
||||
makeSequence(
|
||||
makeNode("Foxtrot", [], validResultValues.not_built, 'PARALLEL'),
|
||||
makeNode("Golf", [], validResultValues.not_built, 'PARALLEL'),
|
||||
makeNode("Hotel", [], validResultValues.not_built, 'PARALLEL'),
|
||||
makeNode('Foxtrot', [], validResultValues.not_built, 'STAGE'),
|
||||
makeNode('Golf', [], validResultValues.not_built, 'STAGE'),
|
||||
makeNode('Hotel', [], validResultValues.not_built, 'STAGE')
|
||||
),
|
||||
makeSequence(
|
||||
makeNode("India", [], validResultValues.not_built, 'PARALLEL'),
|
||||
makeNode("Juliet", [], validResultValues.not_built, 'PARALLEL'),
|
||||
)
|
||||
makeSequence(makeNode('India', [], validResultValues.not_built, 'STAGE'), makeNode('Juliet', [], validResultValues.not_built, 'STAGE')),
|
||||
]),
|
||||
makeNode("Charlie"),
|
||||
makeNode('Charlie'),
|
||||
];
|
||||
|
||||
const {
|
||||
nodeColumns,
|
||||
connections,
|
||||
bigLabels,
|
||||
smallLabels,
|
||||
measuredWidth,
|
||||
measuredHeight,
|
||||
} = layoutGraph(stages, defaultLayout);
|
||||
const { nodeColumns, connections, bigLabels, smallLabels, measuredWidth, measuredHeight } = layoutGraph(stages, defaultLayout);
|
||||
|
||||
// Basic stuff
|
||||
|
||||
|
@ -327,15 +317,8 @@ describe('PipelineGraph', () => {
|
|||
assert.equal(col.topStage.name, 'Bravo', 'top stage name');
|
||||
assert.equal(3, col.rows.length);
|
||||
assertSingleNodeRow(col.rows[0], 'Echo', 384, 55);
|
||||
assertRow(col.rows[1],
|
||||
['Foxtrot', 264, 125],
|
||||
['Golf', 384, 125],
|
||||
['Hotel', 504, 125],
|
||||
);
|
||||
assertRow(col.rows[2],
|
||||
['India', 324, 195],
|
||||
['Juliet', 444, 195],
|
||||
);
|
||||
assertRow(col.rows[1], ['Foxtrot', 264, 125], ['Golf', 384, 125], ['Hotel', 504, 125]);
|
||||
assertRow(col.rows[2], ['India', 324, 195], ['Juliet', 444, 195]);
|
||||
|
||||
// Col 3
|
||||
col = nodeColumns[3];
|
||||
|
@ -381,20 +364,11 @@ describe('PipelineGraph', () => {
|
|||
it('lays out a single node parallel graph', () => {
|
||||
const stages = [
|
||||
makeNode('Build', [], validResultValues.success),
|
||||
makeNode('Test', [
|
||||
makeNode('JUnit', [], validResultValues.success, 'PARALLEL'),
|
||||
]),
|
||||
makeNode('Test', [makeNode('JUnit', [], validResultValues.success, 'PARALLEL')]),
|
||||
makeNode('Deploy'),
|
||||
];
|
||||
|
||||
const {
|
||||
nodeColumns,
|
||||
connections,
|
||||
bigLabels,
|
||||
smallLabels,
|
||||
measuredWidth,
|
||||
measuredHeight,
|
||||
} = layoutGraph(stages, defaultLayout);
|
||||
const { nodeColumns, connections, bigLabels, smallLabels, measuredWidth, measuredHeight } = layoutGraph(stages, defaultLayout);
|
||||
|
||||
// Basic stuff
|
||||
|
||||
|
|
Loading…
Reference in New Issue