Compare commits

...

20 Commits

Author SHA1 Message Date
tfennelly 0ac084d048 Added pointer to JENKINS-36198 2016-06-23 17:10:04 +01:00
tfennelly 9cf4feff74 Fixed a lint error 2016-06-23 17:04:47 +01:00
tfennelly 07d6279b67 Use UrlUtils as suggested by @cliffmeyers 2016-06-23 16:46:43 +01:00
tfennelly 28765eb4ab Use stopPropogation as suggested by @cliffmeyers 2016-06-23 16:28:58 +01:00
tfennelly 5e691381f6 Use isomorphic fetch for running the build 2016-06-23 10:19:19 +01:00
tfennelly 4e40e1d9d4 Use real REST API for starting a run 2016-06-23 10:19:19 +01:00
tfennelly c7cffe1af8 Fix LESS mystery 2016-06-23 10:19:19 +01:00
tfennelly 180eef07bf SVG icons for run 2016-06-23 10:19:19 +01:00
tfennelly 4df5d98136 Added Branch.onJobChannelEvent 2016-06-23 10:19:19 +01:00
tfennelly a079428a19 Added branch name in toast 2016-06-23 10:19:19 +01:00
tfennelly 82592448b4 run details routing from toast action 2016-06-23 10:19:18 +01:00
tfennelly 92c82f7790 Branch instance from SSE event 2016-06-23 10:19:18 +01:00
tfennelly a4ad73ffaa Toast working via SSE event 2016-06-23 10:19:18 +01:00
tfennelly adb9fa2e4d Toast - not right though 2016-06-23 10:19:18 +01:00
tfennelly 73c988e9a0 Code comment 2016-06-23 10:19:18 +01:00
tfennelly 3fecbbe658 Start build button on the PRs tab 2016-06-23 10:19:18 +01:00
tfennelly 12c885afdb removed redundant <head> data attributes 2016-06-23 10:19:18 +01:00
tfennelly a445bf4021 Fix lint errors 2016-06-23 10:19:18 +01:00
tfennelly 83321d13f9 Fix appurl (broken during last pre-commit checks) 2016-06-23 10:19:18 +01:00
tfennelly c5efa05948 RunPipeline component and supporting bits 2016-06-23 10:19:18 +01:00
20 changed files with 461 additions and 120 deletions

View File

@ -1,7 +1,16 @@
import * as sse from '@jenkins-cd/sse-gateway';
import React, { Component, PropTypes } from 'react';
import appConfig from './config';
const { object, node } = PropTypes;
// Connect to the SSE Gateway and allocate a
// dispatcher for blueocean.
// TODO: We might want to move this code to a local SSE util module.
sse.connect('jenkins_blueocean');
appConfig.loadConfig();
class Dashboard extends Component {
getChildContext() {

View File

@ -10,11 +10,6 @@ import * as pushEventUtil from './util/push-event-util';
const { object, array, func, node, string } = PropTypes;
// Connect to the SSE Gateway and allocate a
// dispatcher for blueocean.
// TODO: We might want to move this code to a local SSE util module.
sse.connect('jenkins_blueocean');
class OrganizationPipelines extends Component {
// FIXME: IMO the following should be dropped

View File

@ -0,0 +1,98 @@
/**
* Simple pipeline branch API component.
* <p>
* Non-react component that contains general API methods for
* interacting with pipeline branches, encapsulating REST API calls etc.
*/
import fetch from 'isomorphic-fetch';
import config from '../config';
import Pipeline from './Pipeline';
import * as urlUtils from '../util/UrlUtils';
import * as sse from '@jenkins-cd/sse-gateway';
import * as pushEventUtil from '../util/push-event-util';
export default class Branch {
constructor(pipeline, name) {
this.pipeline = pipeline;
this.name = name;
this.sseListeners = [];
}
runDetailsRouteUrl(runId) {
if (runId === undefined) {
throw new Error('Branch.runDetailsRouteUrl must be supplied with a "runId" parameter.');
}
return urlUtils.buildRunDetailsUrl(
this.pipeline.organization,
this.pipeline.name,
this.name, runId, 'pipeline');
}
restUrl() {
return `${config.blueoceanAppURL}/rest/organizations/${this.pipeline.organization}/pipelines/${this.pipeline.name}/branches/${this.name}`;
}
onJobChannelEvent(callback) {
const _this = this;
const jobListener = sse.subscribe('job', (event) => {
const eventBranch = exports.fromSSEEvent(event);
if (_this.equals(eventBranch)) {
callback(event);
}
});
this.sseListeners.push(jobListener);
}
clearEventListeners() {
for (let i = 0; i < this.sseListeners.length; i++) {
try {
sse.unsubscribe(this.sseListeners[i]);
} catch (e) {
console.error('Unexpected error clearing SSE event listeners from Branch object');
console.error(e);
}
}
this.sseListeners = [];
}
run(onFail) {
const url = `${this.restUrl()}/runs/`;
fetch(url, {
method: 'post',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
}).then((response) => {
if (onFail && (response.status < 200 || response.status > 299)) {
onFail(response);
}
});
}
equals(branch) {
if (branch && branch.name === this.name) {
// and it's the same pipeline...
return (
branch.pipeline.organization === this.pipeline.organization &&
branch.pipeline.name === this.pipeline.name
);
}
return false;
}
}
exports.fromSSEEvent = function (event) {
const eventCopy = pushEventUtil.enrichJobEvent(event);
if (!eventCopy.blueocean_is_multi_branch) {
return undefined;
}
return new Branch(
new Pipeline('jenkins', eventCopy.blueocean_job_name),
eventCopy.blueocean_branch_name
);
};

View File

@ -0,0 +1,13 @@
/**
* Simple pipeline API component.
* <p>
* Non-react component that contains general API methods for
* interacting with pipelines, encapsulating REST API calls etc.
*/
export default class Pipeline {
constructor(organization, pipelineName) {
this.organization = organization;
this.name = pipelineName;
}
}

View File

@ -1,6 +1,7 @@
import React, { Component, PropTypes } from 'react';
import { CommitHash, ReadableDate } from '@jenkins-cd/design-language';
import { LiveStatusIndicator, WeatherIcon } from '@jenkins-cd/design-language';
import RunPipeline from './RunPipeline.jsx';
const { object } = PropTypes;
@ -39,7 +40,7 @@ export default class Branches extends Component {
return (<tr key={name} onClick={open} id={`${name}-${id}`} >
<td><WeatherIcon score={weatherScore} /></td>
<td>
<td onClick={open}>
<LiveStatusIndicator result={result === 'UNKNOWN' ? state : result}
startTime={startTime} estimatedDuration={estimatedDurationInMillis}
/>
@ -48,6 +49,7 @@ export default class Branches extends Component {
<td><CommitHash commitId={commitId} /></td>
<td>{msg || '-'}</td>
<td><ReadableDate date={endTime || ''} /></td>
<td><RunPipeline organization={organization} pipeline={pipelineName} branch={name} /></td>
</tr>);
}
}

View File

@ -101,6 +101,7 @@ export class MultiBranch extends Component {
{ label: 'Last commit', className: 'lastcommit' },
{ label: 'Latest message', className: 'message' },
{ label: 'Completed', className: 'completed' },
{ label: '', className: 'run' },
];
return (

View File

@ -1,5 +1,6 @@
import React, { Component, PropTypes } from 'react';
import { LiveStatusIndicator, ReadableDate } from '@jenkins-cd/design-language';
import RunPipeline from './RunPipeline.jsx';
const { object } = PropTypes;
@ -51,6 +52,7 @@ export default class PullRequest extends Component {
<td>{title || '-'}</td>
<td>{author || '-'}</td>
<td><ReadableDate date={endTime} /></td>
<td><RunPipeline organization={organization} pipeline={pipelineName} branch={name} /></td>
</tr>);
}
}

View File

@ -99,6 +99,7 @@ export class PullRequests extends Component {
{ label: 'Summary', className: 'summary' },
'Author',
{ label: 'Completed', className: 'completed' },
{ label: '', className: 'run' },
];
return (

View File

@ -0,0 +1,101 @@
/**
* Simple widget for triggering the running of a pipeline.
*/
import React, { Component, PropTypes } from 'react';
import Pipeline from '../api/Pipeline';
import Branch from '../api/Branch';
import { Toast } from '@jenkins-cd/design-language';
export default class RunPipeline extends Component {
constructor(props) {
super(props);
const pipeline = new Pipeline(props.organization, props.pipeline);
this.branch = new Branch(pipeline, props.branch);
this.state = {
toast: undefined,
};
}
componentDidMount() {
const _this = this;
const reactContext = this.context;
const theBranch = this.branch;
this.branch.onJobChannelEvent((event) => {
if (event.jenkins_event === 'job_run_queue_enter') {
_this.setState({
toast: { text: `Queued "${theBranch.name}"` },
});
} else if (event.jenkins_event === 'job_run_started') {
_this.setState({
toast: {
text: `Started "${theBranch.name}" #${event.jenkins_object_id}`,
action: {
label: 'Open',
callback: () => {
const runDetailsUrl = theBranch.runDetailsRouteUrl(event.jenkins_object_id);
reactContext.location.pathname = runDetailsUrl;
reactContext.router.push(runDetailsUrl);
},
},
},
});
} else {
_this.setState({ toast: undefined });
}
});
}
componentWillUnmount() {
this.branch.clearEventListeners();
}
run(event) {
const _this = this;
const theBranch = this.branch;
this.branch.run((response) => {
console.error(`Unexpected error queuing a run of "${theBranch.name}". Response:`);
console.error(response);
_this.setState({
toast: { text: `Failed to queue "${theBranch.name}". Try reloading the page.` },
});
});
event.stopPropagation();
}
render() {
const toast = this.state.toast;
if (toast) {
if (toast.action) {
return (<div>
<div className="run-pipeline" onClick={(event) => this.run(event)}></div>
<div className="run-pipeline-toast">
<Toast text={toast.text} action={toast.action.label} onActionClick={() => toast.action.callback()} />
</div>
</div>);
}
return (<div>
<div className="run-pipeline" onClick={(event) => this.run(event)}></div>
<div className="run-pipeline-toast">
<Toast text={toast.text} />
</div>
</div>);
}
return (<div className="run-pipeline" onClick={(event) => this.run(event)}></div>);
}
}
RunPipeline.propTypes = {
organization: PropTypes.string,
pipeline: PropTypes.string,
branch: PropTypes.string,
};
RunPipeline.contextTypes = {
router: PropTypes.object.isRequired, // From react-router
location: PropTypes.object,
};

View File

@ -1 +1,19 @@
// Shared consts.urlPrefix
//
// General "system" config information.
//
// TODO: This should be in a general sharable component.
// Passing it around in the react context is silly.
//
exports.loadConfig = function () {
const headElement = document.getElementsByTagName('head')[0];
// Look up where the Blue Ocean app is hosted
exports.blueoceanAppURL = headElement.getAttribute('data-appurl');
if (typeof exports.blueoceanAppURL !== 'string') {
exports.blueoceanAppURL = '/';
}
exports.jenkinsRootURL = headElement.getAttribute('data-rooturl');
};

View File

@ -39,7 +39,7 @@ exports.enrichJobEvent = function (event, activePipelineName) {
eventCopy.blueocean_is_multi_branch = false;
}
// Is this even associated with the currently active pipeline job?
// Is this event associated with the currently active pipeline job?
eventCopy.blueocean_is_for_current_job =
(eventCopy.blueocean_job_name === activePipelineName);

View File

@ -0,0 +1,111 @@
.pipelines-table {
th {
width: 10%;
min-width: 100px;
}
.name {
width: auto;
}
.favorite {
width: 30px;
}
}
.activity-table {
th {
width: 75px;
}
.branch {
width: 175px;
}
.message {
width: 50%;
}
.duration, .completed {
width: 125px;
}
.status-link {
cursor: pointer;
}
}
.multibranch-table {
th {
width: 75px;
}
.branch {
width: 200px;
}
.message {
width: 50%;
}
.lastcommit, .completed {
width: 125px;
}
}
.pr-table {
th {
width: 75px;
}
.summary {
width: 100%;
}
.build, .completed {
width: 125px;
}
}
.changeset-table {
th {
width: 100px;
}
.author {
width: 150px;
}
.message {
width: 100%;
}
.date {
width: 125px;
}
}
.artifacts-table {
th {
width: 100px;
}
.name {
width: 100%;
}
.download {
text-align: right;
}
}
.nodes {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px;
}
.nodes__section {
display: flex;
align-items: center;
}

View File

@ -1,111 +1,4 @@
.pipelines-table {
th {
width: 10%;
min-width: 100px;
}
@import "variables";
.name {
width: auto;
}
.favorite {
width: 30px;
}
}
.activity-table {
th {
width: 75px;
}
.branch {
width: 175px;
}
.message {
width: 50%;
}
.duration, .completed {
width: 125px;
}
.status-link {
cursor: pointer;
}
}
.multibranch-table {
th {
width: 75px;
}
.branch {
width: 200px;
}
.message {
width: 50%;
}
.lastcommit, .completed {
width: 125px;
}
}
.pr-table {
th {
width: 75px;
}
.summary {
width: 100%;
}
.build, .completed {
width: 125px;
}
}
.changeset-table {
th {
width: 100px;
}
.author {
width: 150px;
}
.message {
width: 100%;
}
.date {
width: 125px;
}
}
.artifacts-table {
th {
width: 100px;
}
.name {
width: 100%;
}
.download {
text-align: right;
}
}
.nodes {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px;
}
.nodes__section {
display: flex;
align-items: center;
}
@import "core";
@import "run-pipeline";

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="314.068px" height="314.068px" viewBox="0 0 314.068 314.068" style="enable-background:new 0 0 314.068 314.068;"
xml:space="preserve">
<g>
<g id="_x33_56._Play">
<g>
<path fill="#1C436A" d="M293.002,78.53C249.646,3.435,153.618-22.296,78.529,21.068C3.434,64.418-22.298,160.442,21.066,235.534
c43.35,75.095,139.375,100.83,214.465,57.47C310.627,249.639,336.371,153.62,293.002,78.53z M219.834,265.801
c-60.067,34.692-136.894,14.106-171.576-45.973C13.568,159.761,34.161,82.935,94.23,48.26
c60.071-34.69,136.894-14.106,171.578,45.971C300.493,154.307,279.906,231.117,219.834,265.801z M213.555,150.652l-82.214-47.949
c-7.492-4.374-13.535-0.877-13.493,7.789l0.421,95.174c0.038,8.664,6.155,12.191,13.669,7.851l81.585-47.103
C221.029,162.082,221.045,155.026,213.555,150.652z"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="314.068px" height="314.068px" viewBox="0 0 314.068 314.068" style="enable-background:new 0 0 314.068 314.068;"
xml:space="preserve">
<g>
<g id="_x33_56._Play">
<g>
<path fill="#4A90E2" d="M293.002,78.53C249.646,3.435,153.618-22.296,78.529,21.068C3.434,64.418-22.298,160.442,21.066,235.534
c43.35,75.095,139.375,100.83,214.465,57.47C310.627,249.639,336.371,153.62,293.002,78.53z M219.834,265.801
c-60.067,34.692-136.894,14.106-171.576-45.973C13.568,159.761,34.161,82.935,94.23,48.26
c60.071-34.69,136.894-14.106,171.578,45.971C300.493,154.307,279.906,231.117,219.834,265.801z M213.555,150.652l-82.214-47.949
c-7.492-4.374-13.535-0.877-13.493,7.789l0.421,95.174c0.038,8.664,6.155,12.191,13.669,7.851l81.585-47.103
C221.029,162.082,221.045,155.026,213.555,150.652z"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,36 @@
.jdl-table {
.run-pipeline {
z-index: 2000;
font-size: 10px;
padding-left: 5px;
padding-top: 2px;
border-radius: 50%;
width: 20px;
height: 20px;
background-image: url('./icons/run.svg');
background-size: cover;
}
.run-pipeline:hover {
background-image: url('./icons/run-darkened.svg'); // TODO: nicer if it could be done via a data-uri transform
}
.run-pipeline-toast {
position: fixed;
left: 15px;
bottom: 15px;
// Need to set higher specificity rules for some of the
// Toast styles because they are being overridden by other
// styles in JDL (table styles).
// TODO: remove post https://issues.jenkins-ci.org/browse/JENKINS-36198
.toast {
a.action {
color: @blueocean-blue;
}
.text {
white-space: normal;
}
}
}
}

View File

@ -0,0 +1,2 @@
@blueocean-blue: #4A90E2;
@blueocean-blue-darkened: darken(@blueocean-blue, 20%);

View File

@ -0,0 +1,23 @@
import { assert } from 'chai';
import Pipeline from '../../main/js/api/Pipeline';
import Branch, { fromSSEEvent } from '../../main/js/api/Branch';
describe('Branch', () => {
it('equals', () => {
const pipeline_1 = new Pipeline('jenkins', 'pipeline1');
const pipeline_2 = new Pipeline('jenkins', 'pipeline2');
const branch_1_1 = new Branch(pipeline_1, 'branch_1');
const branch_1_2 = new Branch(pipeline_1, 'branch_2');
const branch_2_1 = new Branch(pipeline_2, 'branch_1');
// same branch, different pipeline
assert.equal(false, branch_1_1.equals(branch_1_2));
// same pipeline, different branch
assert.equal(false, branch_1_1.equals(branch_2_1));
// same pipeline, same branch
assert.equal(true, branch_1_1.equals(new Branch(pipeline_1, 'branch_1')));
});
});

View File

@ -35,7 +35,7 @@ describe("Branches should render", () => {
const hashComp = row[3].getRenderOutput().props.children;
const hashRendered = sd.shallowRender(hashComp).getRenderOutput();
assert.equal(hashRendered.props.children, commitHash);
assert.equal(row.length, 6);
assert.equal(row.length, 7);
});
});

View File

@ -22,7 +22,7 @@ describe('PullRequest should render', () => {
it('does renders the PullRequest with data', () => {
const result = tree.everySubTree('td');
assert.equal(result.length, 5);
assert.equal(result.length, 6);
assert.equal(data.length, 2);
assert.equal(pr.length, 1);
const im = new RunsRecord(pr[0]);