Merge pull request #174 from cloudbees/feature/UX-74-PipelineGraph-integration

* Integrate PipelineGraph into the blueocean-admin run results modal
This commit is contained in:
Josh McDonald 2016-05-10 13:58:35 +10:00
commit 0794862dfb
11 changed files with 735 additions and 35 deletions

View File

@ -0,0 +1,182 @@
import React, { Component, PropTypes } from 'react';
import { fetch, PipelineGraph } from '@jenkins-cd/design-language';
function badNode(jenkinsNode) {
// eslint-disable-next-line
console.error('Malformed / missing Jenkins run node:', jenkinsNode);
return new Error('convertJenkinsNodeDetails: malformed / missing Jenkins run node.');
}
function convertJenkinsNodeDetails(jenkinsNode) {
if (!jenkinsNode
|| !jenkinsNode.displayName
|| !jenkinsNode.id) {
throw badNode(jenkinsNode);
}
let completePercent = 0;
let state = 'unknown';
if (jenkinsNode.result === 'SUCCESS') {
state = 'success';
completePercent = 100;
} else if (jenkinsNode.result === 'FAILURE') {
state = 'failure';
completePercent = 100;
} else if (jenkinsNode.state === 'RUNNING') {
state = 'running';
completePercent = 50;
} else if (jenkinsNode.state === 'QUEUED'
|| jenkinsNode.state === null) {
state = 'queued';
completePercent = 0;
} else if (jenkinsNode.state === 'NOT_BUILT'
|| jenkinsNode.state === 'ABORTED') {
state = 'not_built';
completePercent = 0;
}
return {
name: jenkinsNode.displayName,
children: [],
state,
completePercent,
id: jenkinsNode.id,
};
}
/**
* Convert the graph results of a run as reported by Jenkins into the
* model required by the PipelineGraph component
*/
export function convertJenkinsNodeGraph(jenkinsGraph) {
if (!jenkinsGraph || !jenkinsGraph.length) {
return [];
}
const results = [];
const originalNodeForId = {};
const convertedNodeForId = {};
let firstNode = undefined;
// Convert the basic details of nodes, and index them by id
jenkinsGraph.forEach(jenkinsNode => {
const convertedNode = convertJenkinsNodeDetails(jenkinsNode);
const { id } = convertedNode;
firstNode = firstNode || convertedNode;
convertedNodeForId[id] = convertedNode;
originalNodeForId[id] = jenkinsNode;
});
// Follow the graph and build our results
let currentNode = firstNode;
while (currentNode) {
results.push(currentNode);
let nextNode = null;
const originalNode = originalNodeForId[currentNode.id];
const edges = originalNode.edges || [];
if (edges.length === 1) {
// Single following (sibling) node
nextNode = convertedNodeForId[edges[0].id];
} else if (edges.length > 1) {
// Multiple following nodes are child nodes not siblings
currentNode.children = edges.map(edge => convertedNodeForId[edge.id]);
// We need to look at the child node's edges to figure out what the next sibling node is
const childEdges = originalNodeForId[edges[0].id].edges || [];
if (childEdges.length) {
nextNode = convertedNodeForId[childEdges[0].id];
}
}
currentNode = nextNode;
}
return results;
}
export class PipelineRunGraph extends Component {
constructor(props) {
super(props);
this.lastData = null;
this.state = { graphNodes: null };
}
componentWillMount() {
this.processData(this.props.data);
}
componentWillReceiveProps(nextProps) {
if (nextProps.data !== this.lastData) {
this.processData(nextProps.data);
}
}
processData(newData) {
this.lastData = newData;
this.setState({
graphNodes: convertJenkinsNodeGraph(newData),
});
}
render() {
const { graphNodes } = this.state;
if (!graphNodes) {
// FIXME: Make a placeholder empty state when data is null (loading)
return <div>Loading...</div>;
} else if (graphNodes.length === 0) {
// Do nothing when there's no node data
return null;
}
const outerDivStyle = {
display: 'flex',
justifyContent: 'center',
};
return (
<div style={outerDivStyle}>
<PipelineGraph stages={graphNodes} />
</div>
);
}
}
PipelineRunGraph.propTypes = {
pipelineName: PropTypes.string,
branchName: PropTypes.string,
runId: PropTypes.string,
data: PropTypes.array,
};
export default fetch(PipelineRunGraph, (props, config) => {
const { pipelineName, branchName, runId } = props;
if (!pipelineName || !runId) {
return null; // Nothing to load yet
}
let id;
if (branchName) {
// Multibranch
// eslint-disable-next-line
id = encodeURIComponent(pipelineName) + '/branches/' +
encodeURIComponent(branchName);
} else {
// No multibranch
id = encodeURIComponent(pipelineName);
}
// eslint-disable-next-line
return config.getAppURLBase() +
'/rest/organizations/jenkins' +
`/pipelines/${id}/runs/${runId}/nodes/`;
}) ;

View File

@ -6,6 +6,7 @@ import {
LogConsole,
PipelineResult,
} from '@jenkins-cd/design-language';
import { ExtensionPoint } from '@jenkins-cd/js-extensions';
import LogToolbar from './LogToolbar';
import {
@ -28,43 +29,40 @@ class RunDetails extends Component {
const {
params: {
pipeline,
},
},
config = {},
} = this.context;
} = this.context;
config.pipeline = pipeline;
this.props.fetchRunsIfNeeded(config);
this.props.setPipeline(config);
}
}
render() {
// early out
if (!this.context.params
|| !this.props.runs
|| this.props.isMultiBranch === null
) {
|| this.props.isMultiBranch === null) {
return null;
}
const {
context: {
router,
params: {
branch,
runId,
pipeline: name,
},
},
} = this;
router,
params,
} = this.context;
const { pipeline: name, branch, runId } = params; // From route
// multibranch special treatment - get url of the log
const isMultiBranch = this.props.isMultiBranch;
const baseUrl = '/rest/organizations/jenkins' +
`/pipelines/${name}`;
`/pipelines/${uriString(name)}/`;
let url;
let fileName = name;
if (this.props.isMultiBranch) {
url = `${baseUrl}/branches/${uriString(branch)}/runs/${runId}/log`;
let fileName;
if (isMultiBranch) {
url = `${baseUrl}/branches/${uriString(branch)}/runs/${runId}/log/`;
fileName = `${branch}-${runId}.txt`;
} else {
url = `${baseUrl}/runs/${runId}/log`;
url = `${baseUrl}/runs/${runId}/log/`;
fileName = `${runId}.txt`;
}
const result = this.props.runs.filter(
@ -76,21 +74,28 @@ class RunDetails extends Component {
router.goBack();
};
return (<ModalView
isVisible
result={result.result}
{...{ afterClose }}
>
<ModalHeader>
<PipelineResult data={result} />
</ModalHeader>
<ModalBody>
<div>
<LogToolbar {...{ fileName, url }} />
<LogConsole {...{ url }} />
</div>
</ModalBody>
</ModalView>);
return (
<ModalView
isVisible
result={result.result}
{...{ afterClose }}
>
<ModalHeader>
<PipelineResult data={result} />
</ModalHeader>
<ModalBody>
<div>
<ExtensionPoint name="jenkins.pipeline.run.result"
pipelineName={name}
branchName={isMultiBranch ? branch : undefined}
runId={runId}
/>
<LogToolbar {...{ fileName, url }} />
<LogConsole {...{ url }} />
</div>
</ModalBody>
</ModalView>
);
}
}

View File

@ -1,5 +1,6 @@
# Extensions in this plugin
# NB: "component" currently maps to modules, not "symbols" so make sure to "export default"
# WARNING: If you change this you'll have to change io.jenkins.blueocean.jsextensions.JenkinsJSExtensionsTest as well :(
extensions:
- component: AdminNavLink
extensionPoint: jenkins.logo.top
@ -7,3 +8,5 @@ extensions:
extensionPoint: jenkins.main.routes
- component: PipelineStore
extensionPoint: jenkins.main.stores
- component: components/PipelineRunGraph
extensionPoint: jenkins.pipeline.run.result

View File

@ -0,0 +1,281 @@
import {assert} from 'chai';
import fs from 'fs';
import path from 'path';
import {convertJenkinsNodeGraph} from '../../main/js/components/PipelineRunGraph.jsx';
import { pipelineStageState } from '@jenkins-cd/design-language/dist/js/components/PipelineGraph';
describe("pipeline graph data converter", () => {
let jsonDir = null;
before(() => {
jsonDir = path.resolve(__dirname, "../json/pipeline-graph-converter/");
});
describe("for empty input of", () => {
function expectEmptyArrayFor(label, input) {
describe(label, () => {
it("returns an empty array", () => {
let result = convertJenkinsNodeGraph(input);
assert(Array.isArray(result), "result should be array");
assert.equal(result.length, 0, "result should be empty");
});
});
}
expectEmptyArrayFor("null", null);
expectEmptyArrayFor("undefined", undefined);
expectEmptyArrayFor("[]", []);
});
describe("for single-node.json", () => {
let testDataJSON = null;
let testData = null;
before(() => {
testDataJSON = fs.readFileSync(path.resolve(jsonDir, "single-node.json"));
});
beforeEach(()=> {
testData = JSON.parse(testDataJSON);
assert(Array.isArray(testData), "testData should be array");
assert.isAtLeast(testData.length, 1, "testData should not be empty");
});
it("produces the correct result", () => {
let result = convertJenkinsNodeGraph(testData);
assert(Array.isArray(result), "result should be array");
assert.equal(result.length, 1, "result.length");
assert.equal(result[0].name, "Deploy", "result[0].name");
assert.equal(result[0].id, "27", "result[0].id");
assert.equal(result[0].state, pipelineStageState.success, "result[0].state");
assert.equal(result[0].completePercent, 100, "result[0].completePercent");
assert(Array.isArray(result[0].children), "result[0].children should be array");
assert.equal(result[0].children.length, 0, "result[0] should have no children");
});
});
describe("for three-nodes.json", () => {
let testDataJSON = null;
let testData = null;
before(() => {
testDataJSON = fs.readFileSync(path.resolve(jsonDir, "three-nodes.json"));
});
beforeEach(()=> {
testData = JSON.parse(testDataJSON);
assert(Array.isArray(testData), "testData should be array");
assert.isAtLeast(testData.length, 1, "testData should not be empty");
});
it("produces the correct result", () => {
let result = convertJenkinsNodeGraph(testData);
assert(Array.isArray(result), "result should be array");
assert.equal(result.length, 3, "result.length");
assert.equal(result[0].name, "First", "result[0].name");
assert.equal(result[0].id, "3", "result[0].id");
assert.equal(result[0].state, pipelineStageState.success, "result[0].state");
assert.equal(result[0].completePercent, 100, "result[0].completePercent");
assert(Array.isArray(result[0].children), "result[0].children should be array");
assert.equal(result[0].children.length, 0, "result[0] should have no children");
assert.equal(result[1].name, "Second", "result[1].name");
assert.equal(result[1].id, "13", "result[1].id");
assert.equal(result[1].state, pipelineStageState.running, "result[1].state");
assert.equal(result[1].completePercent, 50, "result[1].completePercent");
assert(Array.isArray(result[1].children), "result[1].children should be array");
assert.equal(result[1].children.length, 0, "result[1] should have no children");
assert.equal(result[2].name, "Third", "result[2].name");
assert.equal(result[2].id, "27", "result[2].id");
assert.equal(result[2].state, pipelineStageState.queued, "result[2].state");
assert.equal(result[2].completePercent, 0, "result[2].completePercent");
assert(Array.isArray(result[1].children), "result[2].children should be array");
assert.equal(result[2].children.length, 0, "result[2] should have no children");
});
});
describe("for pipeline-nodes-example.json", () => {
let testDataJSON = null;
let testData = null;
before(() => {
testDataJSON = fs.readFileSync(path.resolve(jsonDir, "pipeline-nodes-example.json"));
});
beforeEach(()=> {
testData = JSON.parse(testDataJSON);
assert(Array.isArray(testData), "testData should be array");
assert.isAtLeast(testData.length, 1, "testData should not be empty");
});
it("produces the correct result", () => {
// Or it gets the hose again
let result = convertJenkinsNodeGraph(testData);
assert(Array.isArray(result), "result should be array");
assert.equal(result.length, 3, "result.length");
assert.equal(result[0].name, "Build", "result[0].name");
assert.equal(result[0].id, "3", "result[0].id");
assert.equal(result[0].state, pipelineStageState.success, "result[0].state");
assert.equal(result[0].completePercent, 100, "result[0].completePercent");
assert(Array.isArray(result[0].children), "result[0].children should be array");
assert.equal(result[0].children.length, 0, "result[0] should have no children");
assert.equal(result[1].name, "Test", "result[1].name");
assert.equal(result[1].id, "9", "result[1].id");
assert.equal(result[1].state, pipelineStageState.success, "result[1].state");
assert.equal(result[1].completePercent, 100, "result[1].completePercent");
assert(Array.isArray(result[1].children), "result[1].children should be array");
assert.equal(result[1].children.length, 2, "result[1] should have 2 children");
assert.equal(result[2].name, "Deploy", "result[2].name");
assert.equal(result[2].id, "27", "result[2].id");
assert.equal(result[2].state, pipelineStageState.success, "result[2].state");
assert.equal(result[2].completePercent, 100, "result[2].completePercent");
assert(Array.isArray(result[2].children), "result[2].children should be array");
assert.equal(result[2].children.length, 0, "result[2] should have no children");
let children = result[1].children;
assert.equal(children[0].name, "Firefox", "children[0].name");
assert.equal(children[0].id, "12", "children[0].id");
assert.equal(children[0].state, pipelineStageState.success, "children[0].state");
assert.equal(children[0].completePercent, 100, "children[0].completePercent");
assert(Array.isArray(children[0].children), "children[0].children should be array");
assert.equal(children[0].children.length, 0, "children[0] should have no children");
assert.equal(children[1].name, "Chrome", "children[1].name");
assert.equal(children[1].id, "13", "children[1].id");
assert.equal(children[1].state, pipelineStageState.success, "children[1].state");
assert.equal(children[1].completePercent, 100, "children[1].completePercent");
assert(Array.isArray(children[1].children), "children[1].children should be array");
assert.equal(children[1].children.length, 0, "children[1] should have no children");
});
});
describe("for ends-with-parallel.json", () => {
let testDataJSON = null;
let testData = null;
before(() => {
testDataJSON = fs.readFileSync(path.resolve(jsonDir, "ends-with-parallel.json"));
});
beforeEach(()=> {
testData = JSON.parse(testDataJSON);
assert(Array.isArray(testData), "testData should be array");
assert.isAtLeast(testData.length, 1, "testData should not be empty");
});
it("produces the correct result", () => {
let result = convertJenkinsNodeGraph(testData);
assert(Array.isArray(result), "result should be array");
assert.equal(result.length, 2, "result.length");
assert.equal(result[0].name, "Build", "result[0].name");
assert.equal(result[0].id, "3", "result[0].id");
assert.equal(result[0].state, pipelineStageState.success, "result[0].state");
assert.equal(result[0].completePercent, 100, "result[0].completePercent");
assert(Array.isArray(result[0].children), "result[0].children should be array");
assert.equal(result[0].children.length, 0, "result[0] should have no children");
assert.equal(result[1].name, "Test", "result[1].name");
assert.equal(result[1].id, "9", "result[1].id");
assert.equal(result[1].state, pipelineStageState.success, "result[1].state");
assert.equal(result[1].completePercent, 100, "result[1].completePercent");
assert(Array.isArray(result[1].children), "result[1].children should be array");
assert.equal(result[1].children.length, 2, "result[1] should have 2 children");
let children = result[1].children;
assert.equal(children[0].name, "Firefox", "children[0].name");
assert.equal(children[0].id, "12", "children[0].id");
assert.equal(children[0].state, pipelineStageState.success, "children[0].state");
assert.equal(children[0].completePercent, 100, "children[0].completePercent");
assert(Array.isArray(children[0].children), "children[0].children should be array");
assert.equal(children[0].children.length, 0, "children[0] should have no children");
assert.equal(children[1].name, "Chrome", "children[1].name");
assert.equal(children[1].id, "13", "children[1].id");
assert.equal(children[1].state, pipelineStageState.success, "children[1].state");
assert.equal(children[1].completePercent, 100, "children[1].completePercent");
assert(Array.isArray(children[1].children), "children[1].children should be array");
assert.equal(children[1].children.length, 0, "children[1] should have no children");
});
});
describe("for every-result.json", () => {
let testDataJSON = null;
let testData = null;
before(() => {
testDataJSON = fs.readFileSync(path.resolve(jsonDir, "every-result.json"));
});
beforeEach(()=> {
testData = JSON.parse(testDataJSON);
assert(Array.isArray(testData), "testData should be array");
assert.isAtLeast(testData.length, 1, "testData should not be empty");
});
it("produces the correct result", () => {
let result = convertJenkinsNodeGraph(testData);
assert(Array.isArray(result), "result should be array");
assert.equal(result.length, 6, "result.length");
assert.equal(result[0].name, "First", "result[0].name");
assert.equal(result[0].id, "3", "result[0].id");
assert.equal(result[0].state, pipelineStageState.success, "result[0].state");
assert.equal(result[0].completePercent, 100, "result[0].completePercent");
assert(Array.isArray(result[0].children), "result[0].children should be array");
assert.equal(result[0].children.length, 0, "result[0] should have no children");
assert.equal(result[1].name, "Second", "result[1].name");
assert.equal(result[1].id, "13", "result[1].id");
assert.equal(result[1].state, pipelineStageState.running, "result[1].state");
assert.equal(result[1].completePercent, 50, "result[1].completePercent");
assert(Array.isArray(result[1].children), "result[1].children should be array");
assert.equal(result[1].children.length, 0, "result[1] should have no children");
assert.equal(result[2].name, "Third", "result[2].name");
assert.equal(result[2].id, "27", "result[2].id");
assert.equal(result[2].state, pipelineStageState.queued, "result[2].state");
assert.equal(result[2].completePercent, 0, "result[2].completePercent");
assert(Array.isArray(result[2].children), "result[2].children should be array");
assert.equal(result[2].children.length, 0, "result[2] should have no children");
assert.equal(result[3].name, "Fourth", "result[3].name");
assert.equal(result[3].id, "28", "result[3].id");
assert.equal(result[3].state, pipelineStageState.failure, "result[3].state");
assert.equal(result[3].completePercent, 100, "result[3].completePercent");
assert(Array.isArray(result[3].children), "result[3].children should be array");
assert.equal(result[3].children.length, 0, "result[3] should have no children");
assert.equal(result[4].name, "Steve", "result[4].name");
assert.equal(result[4].id, "29", "result[4].id");
assert.equal(result[4].state, pipelineStageState.notBuilt, "result[4].state");
assert.equal(result[4].completePercent, 0, "result[4].completePercent");
assert(Array.isArray(result[4].children), "result[4].children should be array");
assert.equal(result[4].children.length, 0, "result[4] should have no children");
assert.equal(result[5].name, "Unknown-Null", "result[5].name");
assert.equal(result[5].id, "33", "result[5].id");
assert.equal(result[5].state, pipelineStageState.queued, "result[5].state");
assert.equal(result[5].completePercent, 0, "result[5].completePercent");
assert(Array.isArray(result[5].children), "result[5].children should be array");
assert.equal(result[5].children.length, 0, "result[5] should have no children");
});
});
});

View File

@ -0,0 +1,48 @@
[
{
"displayName": "Build",
"edges": [
{
"durationInMillis": 694,
"id": "9"
}
],
"id": "3",
"result": "SUCCESS",
"startTime": "2016-05-02T16:06:28.639+1000",
"state": "FINISHED"
},
{
"displayName": "Test",
"edges": [
{
"durationInMillis": 2,
"id": "12"
},
{
"durationInMillis": 3,
"id": "13"
}
],
"id": "9",
"result": "SUCCESS",
"startTime": "2016-05-02T16:06:29.333+1000",
"state": "FINISHED"
},
{
"displayName": "Firefox",
"edges": [],
"id": "12",
"result": "SUCCESS",
"startTime": "2016-05-02T16:06:29.335+1000",
"state": "FINISHED"
},
{
"displayName": "Chrome",
"edges": [],
"id": "13",
"result": "SUCCESS",
"startTime": "2016-05-02T16:06:29.336+1000",
"state": "FINISHED"
}
]

View File

@ -0,0 +1,70 @@
[
{
"displayName": "First",
"edges": [
{
"id": "13"
}
],
"id": "3",
"result": "SUCCESS",
"startTime": "2016-05-02T16:06:28.639+1000",
"state": "FINISHED"
},
{
"displayName": "Second",
"edges": [
{
"id": "27"
}
],
"id": "13",
"result": "UNKNOWN",
"startTime": "2016-05-02T16:06:29.336+1000",
"state": "RUNNING"
},
{
"displayName": "Third",
"edges": [
{
"id": "28"
}
],
"id": "27",
"result": "UNKNOWN",
"startTime": "2016-05-02T16:06:29.942+1000",
"state": "QUEUED"
},
{
"displayName": "Fourth",
"edges": [
{
"id": "29"
}
],
"id": "28",
"result": "FAILURE",
"startTime": "2016-05-02T16:06:29.942+1000",
"state": "FINISHED"
},
{
"displayName": "Steve",
"edges": [
{
"id": "33"
}
],
"id": "29",
"result": "UNKNOWN",
"startTime": "2016-05-02T16:06:29.942+1000",
"state": "NOT_BUILT"
},
{
"displayName": "Unknown-Null",
"edges": [],
"id": "33",
"result": null,
"startTime": "2016-05-02T16:06:29.942+1000",
"state": null
}
]

View File

@ -0,0 +1,66 @@
[
{
"displayName": "Build",
"edges": [
{
"durationInMillis": 694,
"id": "9"
}
],
"id": "3",
"result": "SUCCESS",
"startTime": "2016-05-02T16:06:28.639+1000",
"state": "FINISHED"
},
{
"displayName": "Test",
"edges": [
{
"durationInMillis": 2,
"id": "12"
},
{
"durationInMillis": 3,
"id": "13"
}
],
"id": "9",
"result": "SUCCESS",
"startTime": "2016-05-02T16:06:29.333+1000",
"state": "FINISHED"
},
{
"displayName": "Firefox",
"edges": [
{
"durationInMillis": 607,
"id": "27"
}
],
"id": "12",
"result": "SUCCESS",
"startTime": "2016-05-02T16:06:29.335+1000",
"state": "FINISHED"
},
{
"displayName": "Chrome",
"edges": [
{
"durationInMillis": 606,
"id": "27"
}
],
"id": "13",
"result": "SUCCESS",
"startTime": "2016-05-02T16:06:29.336+1000",
"state": "FINISHED"
},
{
"displayName": "Deploy",
"edges": [],
"id": "27",
"result": "SUCCESS",
"startTime": "2016-05-02T16:06:29.942+1000",
"state": "FINISHED"
}
]

View File

@ -0,0 +1,11 @@
[
{
"displayName": "Deploy",
"edges": [],
"id": "27",
"result": "SUCCESS",
"extraneousFields":"better not break anything",
"startTime": "2016-05-02T16:06:29.942+1000",
"state": "FINISHED"
}
]

View File

@ -0,0 +1,34 @@
[
{
"displayName": "First",
"edges": [
{
"id": "13"
}
],
"id": "3",
"result": "SUCCESS",
"startTime": "2016-05-02T16:06:28.639+1000",
"state": "FINISHED"
},
{
"displayName": "Second",
"edges": [
{
"id": "27"
}
],
"id": "13",
"result": "UNKNOWN",
"startTime": "2016-05-02T16:06:29.336+1000",
"state": "RUNNING"
},
{
"displayName": "Third",
"edges": [],
"id": "27",
"result": "UNKNOWN",
"startTime": "2016-05-02T16:06:29.942+1000",
"state": "QUEUED"
}
]

View File

@ -37,6 +37,7 @@ public class JenkinsJSExtensionsTest extends BaseTest{
@Test
public void test() {
// FIXME: This test relies on configuration in a separate project
// Simple test of the rest endpoint. It should find the "blueocean-admin"
// plugin ExtensionPoint contributions.
List<Map> extensions = get("/javaScriptExtensionInfo", List.class);
@ -46,7 +47,7 @@ public class JenkinsJSExtensionsTest extends BaseTest{
List<Map> ext = (List<Map>) extensions.get(0).get("extensions");
Assert.assertEquals(3, ext.size());
Assert.assertEquals(4, ext.size());
Assert.assertEquals("AdminNavLink", ext.get(0).get("component"));
Assert.assertEquals("jenkins.logo.top", ext.get(0).get("extensionPoint"));

View File

@ -39,7 +39,6 @@ var packageFiles = [];
packageFiles.push(require("./blueocean-admin/package.json"));
packageFiles.push(require("./blueocean-web/package.json"));
packageFiles.push(require("./jenkins-design-language/package.json"));
packageFiles.forEach(packageFile => {