Compare commits

...

59 Commits

Author SHA1 Message Date
Ivan Meredith 574aed8ce3 [JENKINS-36209] Queued activity items (#389)
* [JENKINS-36209] Queued items now show in activties

* Remove unneeded changes from routes

* Fix whitespace issues

* Fix linting

* Style changes

* Fixed pipeline steps

* Fix linting

* Add tests and make queues work for multibranch projects

* Remove destructering from RunDetailsHeader

* Extract common function for mapping runs

* Optimize imports

* Remove whitespace

* Remove inline CSS

* Fix typo

* Fix isCompleted Function

* Add timer icon to emtpy state

* Bump JDL version

* Try waiting for start of anything that is in the queue first

* Fix NPE

* Disable Multibranch test for now
2016-08-08 14:27:54 +12:00
Michael Neale db31a290f7 [maven-release-plugin] prepare for next development iteration 2016-08-05 19:55:29 +10:00
Michael Neale 5ee06d03c7 [maven-release-plugin] prepare release blueocean-parent-1.0-alpha-6 2016-08-05 19:55:20 +10:00
Tom Fennelly 29dbde63eb Ath test help changes (#391)
* step-scroll-area class

* Added step-[id] on each of the logConsoles in run details
2016-08-04 09:14:41 +01:00
Tom Fennelly 0dfe60a6a8 Updated SSE Gateway to 1.8 to fix ATH (v2 grrrrr) (#390) 2016-08-03 11:54:02 +01:00
Tom Fennelly f98037d136 Updated SSE Gateway to 1.7 to fix ATH (#388) 2016-08-02 13:12:00 +01:00
Tom Fennelly 34eff2a236 Submitter checklist item for running the ATH 2016-08-02 11:41:04 +01:00
Tom Fennelly f5f0c0ecf9 Update PULL_REQUEST_TEMPLATE (#380)
Fixed some typos that were annoying me :)
2016-08-01 10:37:11 +10:00
Cliff Meyers eea4b1697d Merge pull request #387 from jenkinsci/bug/JENKINS-36904-running-queued-anim
Bug/jenkins 36904 running queued anim
2016-07-30 09:40:42 -04:00
Cliff Meyers e52c309a83 [JENKINS-36904] use a lighter color for the outer circle when "queued" 2016-07-29 14:35:29 -04:00
Cliff Meyers c8197db439 [JENKINS-36904] use solid white pulse for "running" indicator 2016-07-29 14:35:04 -04:00
Vivek Pandey b8f4cf9699 [maven-release-plugin] prepare for next development iteration 2016-07-29 09:57:07 -07:00
Vivek Pandey ad0bfecd72 [maven-release-plugin] prepare release blueocean-parent-1.0-alpha-5 2016-07-29 09:56:59 -07:00
Thorsten Scherler 693c48fd3f Jenkins 36131 (#385)
* [JENKINS-36131] WIP testing with content length

* [JENKINS-36131] WIP add parameter fetchAll and refetch the logs when present. Broken for me ATM unknown reason. will merge master now in here

* [JENKINS-36131] WIP first working version with steps.

* [JENKINS-36131] WIP first working version as well for freestyle, but I need to refactor the thing. using the hash is not optimal, will switch to the backend hook start=0

* [JENKINS-36131] Switch to trigger ?start=0 which enables us to link in full extended logs. Now writing AT for it

* [JENKINS-36131] Make the link to the full log more visible

* [JENKINS-36131] easier matching for AT

* [JENKINS-36131] in follow along the Full Log button should not be shown, since you see everything already

* eslint - formating changes and fix offences

* [JENKINS-36131] Fix button to use the correct classes. Thanks James for the headsup!
2016-07-29 17:27:52 +02:00
Cliff Meyers 203df4b7d8 Merge pull request #381 from jenkinsci/feature/JENKINS-37007-favorites-animations
Feature/jenkins 37007 favorites animations
2016-07-29 09:18:57 -04:00
Tom Fennelly a12ae4e452 [JENKINS-36238] New SSE client gateway with store and forward support (#379)
* New SSE client gateway with store and forward support

* Updated SSE deps to release version
2016-07-29 10:38:40 +01:00
Cliff Meyers 36966d4793 [JENKINS-37020] fix bug where branch name was not decoded (#382) 2016-07-29 12:07:58 +10:00
Cliff Meyers 39b4ffa0de Merge pull request #383 from jenkinsci/bug/JENKINS-37022-branches-actions-spacing
[JENKINS-37022] add a little spacing to actions on Branches tab
2016-07-28 14:50:17 -04:00
Cliff Meyers d65165f6ab [JENKINS-37022] add a little spacing ot actions on Branches tab 2016-07-28 14:07:59 -04:00
Cliff Meyers 33f643ea37 [JENKINS-37007] tick js-extensions and packages depending on it to 0.0.20 2016-07-28 13:29:21 -04:00
vivek e181cf5a91 JENKINS-36884# Analytics-tools plugin to integrated rollbar (#374)
* JENKINS-36884# Analytics-tools plugin to integrated rollbar

Also includes infrastrucutre to inject HTML head tags and HTTP header by any plugin that implements BluePageDecorator
and appropriate jelly files.

* Doc update

* Wrapped rollbar JS code inside IIFE.

* Use rollbar-browser package and build browser bundle

* Fixes and doc update

- Stapler includei jelly tag needs escaping $
- Added missing dependency in gulpfile.js
- require should load browser-rollbar
2016-07-28 08:50:24 -07:00
Cliff Meyers 789882e4ea [JENKINS-37007] fix a bug where expand/collapse animation did not work by using max-height instead of height; adjust timing 2016-07-28 11:42:27 -04:00
tfennelly 759e920c5b remove addons from web bundle extDeps 2016-07-28 16:20:17 +01:00
tfennelly c5011d8cc1 0.0.20-beta2 of js-extensions 2016-07-28 16:19:03 +01:00
Cliff Meyers 8a636726fa Merge branch 'master' into feature/JENKINS-37007-favorites-animations 2016-07-28 08:16:37 -04:00
Thorsten Scherler 3958b8aaa3 Jenkins 36131 "Show all" link required anywhere a log is displayed (#378)
* [JENKINS-36131] WIP testing with content length

* [JENKINS-36131] WIP add parameter fetchAll and refetch the logs when present. Broken for me ATM unknown reason. will merge master now in here

* [JENKINS-36131] WIP first working version with steps.

* [JENKINS-36131] WIP first working version as well for freestyle, but I need to refactor the thing. using the hash is not optimal, will switch to the backend hook start=0

* [JENKINS-36131] Switch to trigger ?start=0 which enables us to link in full extended logs. Now writing AT for it

* [JENKINS-36131] Make the link to the full log more visible

* [JENKINS-36131] easier matching for AT

* [JENKINS-36131] in follow along the Full Log button should not be shown, since you see everything already
2016-07-28 13:01:40 +02:00
Cliff Meyers e84f3204e0 Merge pull request #367 from jenkinsci/feature/JENKINS-35840-35781-more-favoriting
Feature/jenkins 35840 35781 more favoriting
2016-07-27 21:18:20 -04:00
Cliff Meyers b8789358b1 [JENKINS-35837] fix test regression 2016-07-27 15:30:46 -04:00
Cliff Meyers 19bdafa271 [JENKINS-37007] animations for favorites cards; wip while working around "duplicate Reacts" error (tracked in JENKINS-37006) 2016-07-27 15:17:29 -04:00
Cliff Meyers cbb7fc9687 [JENKINS-35837] linkify the name in the favorites card to open the "run details" screen, pipeline tab 2016-07-27 15:08:13 -04:00
Thorsten Scherler 4b96cb3af6 [JENKINS-36169] Override resultItem css to remove border and padding (#371)
* [JENKINS-36169] Override resultItem css to remove border and padding

* [JENKINS-36169] remove override since it is not needed anymore

* [JENKINS-36169] tick version so changes from jdl are visible

* [JENKINS-36169] pump version

* [JENKINS-36169] The border is needed in the test view

* [JENKINS-36169] using now published version
2016-07-27 20:10:29 +02:00
Cliff Meyers 7f74eeb7ec [JENKINS-35840] freshen js-extensions 2016-07-27 13:14:34 -04:00
Cliff Meyers 29c877055b [JENKINS-35781] fix a bug where clicking on a favorite on the Branches tab instead triggered the modal to open. React eventing is fun... 2016-07-27 12:01:57 -04:00
Keith Zantow 91c4188539 [FIXED JENKINS-36336] Always show fixed tests if there are some (#373)
* JENKINS-36336 Always show fixed tests if there are some
* JENKINS-36930 Test results display not handling REGRESSION case
2016-07-27 11:59:16 -04:00
vivek b8c038abac JENKINS-36967# Favorite object self link fix (#377) 2016-07-27 07:47:39 -07:00
Cliff Meyers 0624b7d416 [JENKINS-36968] fix a bug where multibranch pipelines that were favorited could not be unfavorited by toggling the "favorite" button in the favorites card to "off" 2016-07-27 10:29:48 -04:00
Cliff Meyers 3227570ee9 Merge branch 'bug/JENKINS-36967' into feature/JENKINS-35840-35781-more-favoriting 2016-07-27 09:30:53 -04:00
Vivek Pandey 619618213c JENKINS-36967# Favorite object self link fix 2016-07-26 15:40:55 -07:00
Cliff Meyers 387f470af8 Merge branch 'master' into feature/JENKINS-35840-35781-more-favoriting
Conflicts:
	blueocean-personalization/src/test/js/redux/FavoritesStore-spec.js
2016-07-26 15:26:47 -04:00
Thorsten Scherler 6de55426ef Jenkins 36903 (#376)
* [JENKINS-36903] test for things that we later use and if not present would throw an exception

* [JENKINS-36903] Fix name not generated on build slave
2016-07-26 20:53:03 +02:00
Keith Zantow 501e74db31 Get rid of React warning about duplicate/missing keys on test page (#372) 2016-07-26 10:02:17 -04:00
Thorsten Scherler 3b3cd1764e [JENKINS-36903] test for things that we later use and if not present would throw an exception (#375) 2016-07-26 11:23:16 +02:00
Thorsten Scherler 64f4ce5ea6 [JENKINS-36231] less timeout for scrollBottom and scroll on every processLines (#370) 2016-07-25 12:15:19 +02:00
vivek b71a48c31d JENKINS-36481# Rollbar and it.pluginVersion() removed (#369)
* JENKINS-36481# Rollbar and it.pluginVersion() removed

BlueOceanUI is a bootstrap and core infrastructure class, we do want to
keep it fiarly abstract and indepedent of direct Jenkins dependency.

Rollbar or any such analytics tool integration should be done out of blueocean-web
possibly inside a separate plugin or some place else that makes sense.

Footer version to display the commitId/branch

* Handle the case when revisionInfo is not there, returning null for now
2016-07-22 22:02:41 -07:00
Michael Neale 2a7baee0d9 [maven-release-plugin] prepare for next development iteration 2016-07-22 11:08:17 -07:00
Michael Neale f344c261c6 [maven-release-plugin] prepare release blueocean-parent-1.0-alpha-4 2016-07-22 11:08:12 -07:00
Keith Zantow d78a16e21a JENKINS-36829 - fix context path passed to blue ocean - don't end with / (#368)
* JENKINS-36829 - fix context path passed to blue ocean - don't end with /
* [JENKINS-36829-bad-test-url] Fix path for creating new pipelines
2016-07-22 13:08:45 -04:00
Thorsten Scherler 6c1a0c0543 [master] tick version of jdl to newest 2016-07-22 18:59:51 +02:00
Cliff Meyers 18038c83f0 Merge branch 'master' into feature/JENKINS-35840-35781-more-favoriting 2016-07-21 15:57:04 -07:00
Cliff Meyers 7ec7d89157 [JENKINS-35840] test fix 2016-07-21 15:53:45 -07:00
Cliff Meyers d2f6cc00f6 [JENKINS-35840] delint 2016-07-21 15:31:26 -07:00
Cliff Meyers 429eb8e898 [JENKINS-35840] delint 2016-07-21 15:20:40 -07:00
Cliff Meyers 9beb47a37f [JENKINS-35840] convert from JSON to properly linted string 2016-07-21 15:20:28 -07:00
Cliff Meyers 072c6883f8 [JENKINS-35840] jsdoc 2016-07-21 15:14:43 -07:00
Cliff Meyers e759913ed1 [JENKINS-35781] enable favoriting of branches on Branches tab 2016-07-21 14:22:27 -07:00
Cliff Meyers e3b616ad45 Merge branch 'master' into feature/JENKINS-35840-35781-more-favoriting 2016-07-21 14:01:09 -07:00
Cliff Meyers b804b8cf38 [JENKINS-35781] whitespace 2016-07-20 15:49:14 -07:00
Cliff Meyers 32978e73a5 [JENKINS-35840] wire up favoriting of pipeline in Pipeline Detail header 2016-07-20 15:14:13 -07:00
Cliff Meyers 3299527fae [JENKINS-35840] refactor user/favorites fetching logic out of DashboardCards and into new "FavoritesProvider" that can ensure the data is loaded for any components that might need it 2016-07-20 15:04:14 -07:00
77 changed files with 1661 additions and 640 deletions

View File

@ -1,13 +1,17 @@
**Decription**
# Description
**Submitter checklist**
See [JENKINS-XXXXX](https://issues.jenkins-ci.org/browse/JENKINS-XXXXX).
# Submitter checklist
- [ ] Link to JIRA ticket in description, if appropriate.
- [ ] Change is code complete and matches issue description
- [ ] Apppropriate unit or acceptance tests or explaination to why this change has no tests
- [ ] Reviewer's manual test instructions provided in PR description. See Reviewer's first task below.
- [ ] Ran Acceptance Test Harness against PR changes.
**Reviewer checklist**
# Reviewer checklist
- [ ] Run the changes and verified the change matches the issue description
- [ ] Reviewed the code
- [ ] Verified that the appropriate tests have been written or valid explaination given
@reviewbybees
@jenkinsci/code-reviewers @reviewbybees

View File

@ -39,8 +39,11 @@ Provides implementation of Pipeline apis for Jenkins pipeline and multi-branch j
## blueocean-web
Web infrastructure that glues Jenkins and Blue Ocean plugin together on the /blue endpoint.
Core Web infrastructure that bootstraps BlueOcean UI and integrates REST API core blueocean-rest.
## blueocean-analytics-tools
Plugin to inject analytics tools as HTML header in blueocean UI.
# Building and running

View File

@ -0,0 +1,21 @@
The MIT License
Copyright (c) 2016 CloudBees Inc and a number of other of contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,21 @@
> Analytics Tools to be injected in to BlueOcean UI
# RollBar
* Enable RollBar
RollBar is disabled by default. Use BLUEOCEAN_ROLLBAR_ENABLED JVM property to enable.
````
mvn hpi:run -DBLUEOCEAN_ROLLBAR_ENABLED=true
````
## Usage ...
try {
foo();
$blueocean_Rollbar.debug('foo() called');
} catch (e) {
$blueocean_Rollbar.error('Problem calling foo()', e);
}

View File

@ -0,0 +1,7 @@
var builder = require('@jenkins-cd/js-builder');
//
// Create the rollbar bundle.
// See https://github.com/jenkinsci/js-builder
//
builder.bundle('src/main/js/rollbar.js');

View File

@ -0,0 +1,20 @@
{
"name": "blueocean-analytics-tools",
"version": "0.0.1",
"description": "Analytics tools that gets injected in BlueOcean UI",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Vivek Pandey <vivek.pandey@gmail.com> (https://github.com/vivek)",
"license": "MIT",
"dependencies": {
"@jenkins-cd/js-modules": "0.0.5",
"rollbar-browser": "1.9.1"
},
"devDependencies": {
"@jenkins-cd/js-builder": "0.0.35",
"babel-eslint": "^6.1.2",
"gulp": "3.9.1",
"eslint-plugin-react": "^5.0.1"
}
}

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.jenkins.blueocean</groupId>
<artifactId>blueocean-parent</artifactId>
<version>1.0-alpha-7-SNAPSHOT</version>
</parent>
<artifactId>blueocean-analytics-tools</artifactId>
<packaging>hpi</packaging>
<name>BlueOcean :: Analytics Tools</name>
<url>https://wiki.jenkins-ci.org/display/JENKINS/Blue+Ocean+Plugin</url>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>blueocean-web</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,22 @@
package io.jenkins.blueocean.analyticstools;
import hudson.Extension;
import io.jenkins.blueocean.BluePageDecorator;
import jenkins.model.Jenkins;
/**
* @author Vivek Pandey
*/
@Extension(ordinal = 10)
public class AnalyticsTools extends BluePageDecorator {
public boolean isRollBarEnabled(){
return Boolean.getBoolean("BLUEOCEAN_ROLLBAR_ENABLED");
}
/** gives Blueocean plugin version. blueocean-web being core module is looked at to determine the version */
public String getBlueOceanPluginVersion(){
return Jenkins.getInstance().getPlugin("blueocean-web").getWrapper().getVersion();
}
}

View File

@ -0,0 +1,59 @@
function getConfigAttribute(name) {
var headElements = document.getElementsByTagName('head');
if (headElements.length === 1) {
return headElements[0].getAttribute(name);
}
return undefined;
}
function normalizeURL(location) {
var normalizedUrl = 'http://anon.blueocean.io' + location;
var rootUrl = getConfigAttribute('data-rooturl');
if (rootUrl && location.startsWith(rootUrl)) {
normalizedUrl = 'http://anon.blueocean.io' + location.substring(rootUrl.length - 1, location.length);
}
return normalizedUrl;
}
var transformer = function (payload) {
payload.data.request.user_ip = '0.0.0.0';
payload.data.request.url = normalizeURL(window.location.pathname);
};
//
// Configure rollbar ...
// See https://github.com/rollbar/rollbar.js/tree/master/examples/browserify
//
var _rollbarConfig = {
accessToken: '81f3134dedf44871b9cc0a347b1313df',
captureUncaught: true,
code_version: window.$blueocean_pluginVersion, // see header.jelly
source_map_enabled: true,
guess_uncaught_frames: true,
transform: transformer
};
var rollbarBrowser = require('rollbar-browser');
var Rollbar = rollbarBrowser.init(_rollbarConfig);
// Looking at docs (https://github.com/rollbar/rollbar.js/tree/master/examples/browserify)
// it seems like they stuff it into a global. We are trying hard not to do that
// under any circumstances, but maybe this is an exception if it's only going to be
// used in a closed/controlled env.
//
// Soooo .... lets export it to global for now, but as $blueocean_Rollbar ...
//
window.$blueocean_Rollbar = Rollbar;
//
// Usage ...
//
//try {
// foo();
// $blueocean_Rollbar.debug('foo() called');
//} catch (e) {
// $blueocean_Rollbar.error('Problem calling foo()', e);
//}

View File

@ -0,0 +1,4 @@
<?jelly escape-by-default='true'?>
<div>
BlueOcean Analytics Tools plugin
</div>

View File

@ -0,0 +1,25 @@
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler">
<j:if test="${it.rollBarEnabled}">
<!--
Still use a small bit of JS to inject the Rollbar access token.
I think this is ok for now because we're setting a global anyway - see
comment at the end of src/main/js/rollbar.js
-->
<script>
(function () {
window.$$blueocean_pluginVersion = '${it.blueOceanPluginVersion}';
})();
</script>
<!--
Running the plugin build (or just "gulp" from the command line) will
generate a browser bundle of what's in src/main/js/rollbar.js and we
can load that using an adjunct as follows.
See gulpfile.js and see the output from running the "gulp" command.
-->
<st:adjunct includes="org.jenkins.ui.jsmodules.blueocean_analytics_tools.rollbar"/>
</j:if>
</j:jelly>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>io.jenkins.blueocean</groupId>
<artifactId>blueocean-parent</artifactId>
<version>1.0-alpha-4-SNAPSHOT</version>
<version>1.0-alpha-7-SNAPSHOT</version>
</parent>
<artifactId>blueocean-commons</artifactId>

View File

@ -35,10 +35,10 @@
"skin-deep": "^0.16.0"
},
"dependencies": {
"@jenkins-cd/design-language": "0.0.63",
"@jenkins-cd/js-extensions": "0.0.19",
"@jenkins-cd/design-language": "0.0.67",
"@jenkins-cd/js-extensions": "0.0.20",
"@jenkins-cd/js-modules": "0.0.5",
"@jenkins-cd/sse-gateway": "0.0.6",
"@jenkins-cd/sse-gateway": "0.0.7",
"immutable": "3.8.1",
"isomorphic-fetch": "2.2.1",
"keymirror": "0.1.1",

View File

@ -3,7 +3,7 @@
<parent>
<artifactId>blueocean-parent</artifactId>
<groupId>io.jenkins.blueocean</groupId>
<version>1.0-alpha-4-SNAPSHOT</version>
<version>1.0-alpha-7-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -4,12 +4,16 @@ import appConfig from './config';
const { object, node } = PropTypes;
appConfig.loadConfig();
// 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();
sse.connect({
clientId: 'jenkins_blueocean',
onConnect: undefined,
jenkinsUrl: `${appConfig.getJenkinsRootURL()}/`, // FIXME sse should not require this to end with a /
});
class Dashboard extends Component {

View File

@ -2,7 +2,7 @@ import React, { Component, PropTypes } from 'react';
import { EmptyStateView, Table } from '@jenkins-cd/design-language';
import Runs from './Runs';
import Pipeline from '../api/Pipeline';
import { ActivityRecord, ChangeSetRecord } from './records';
import { RunRecord, ChangeSetRecord } from './records';
import RunPipeline from './RunPipeline.jsx';
import {
actions,
@ -88,26 +88,27 @@ export class Activity extends Component {
{ label: '', className: 'actions' },
];
return (<main>
<article className="activity">
{showRunButton && <RunNonMultiBranchPipeline pipeline={pipeline} buttonText="Run" />}
<Table className="activity-table fixed" headers={headers}>
{ runs.map((run, index) => {
const changeset = run.changeSet;
let latestRecord = {};
if (changeset && changeset.length > 0) {
latestRecord = new ChangeSetRecord(changeset[
Object.keys(changeset)[0]
]);
}
const props = {
key: index,
changeset: latestRecord,
result: new ActivityRecord(run),
};
return (<Runs {...props} />);
})}
{
runs.map((run, index) => {
const changeset = run.changeSet;
let latestRecord = {};
if (changeset && changeset.length > 0) {
latestRecord = new ChangeSetRecord(changeset[
Object.keys(changeset)[0]
]);
}
return (<Runs {...{
key: index,
changeset: latestRecord,
result: new RunRecord(run) }} />);
})
}
</Table>
</article>
</main>);

View File

@ -3,6 +3,7 @@ import { CommitHash, ReadableDate } from '@jenkins-cd/design-language';
import { LiveStatusIndicator, WeatherIcon } from '@jenkins-cd/design-language';
import Extensions from '@jenkins-cd/js-extensions';
import RunPipeline from './RunPipeline.jsx';
import { StopPropagation } from './StopPropagation';
import { buildRunDetailsUrl } from '../util/UrlUtils';
const { object } = PropTypes;
@ -43,22 +44,30 @@ export default class Branches extends Component {
};
const { msg } = changeSet[0] || {};
return (<tr key={cleanBranchName} onClick={open} id={`${cleanBranchName}-${id}`} >
<td><WeatherIcon score={weatherScore} /></td>
<td onClick={open}>
<LiveStatusIndicator result={result === 'UNKNOWN' ? state : result}
startTime={startTime} estimatedDuration={estimatedDurationInMillis}
/>
</td>
<td>{cleanBranchName}</td>
<td><CommitHash commitId={commitId} /></td>
<td>{msg || '-'}</td>
<td><ReadableDate date={endTime} liveUpdate /></td>
<td>
<RunPipeline organization={organization} pipeline={fullName} branch={encodeURIComponent(branchName)} />
<Extensions.Renderer extensionPoint="jenkins.pipeline.branches.list.action" />
</td>
</tr>);
return (
<tr key={cleanBranchName} onClick={open} id={`${cleanBranchName}-${id}`} >
<td><WeatherIcon score={weatherScore} /></td>
<td onClick={open}>
<LiveStatusIndicator result={result === 'UNKNOWN' ? state : result}
startTime={startTime} estimatedDuration={estimatedDurationInMillis}
/>
</td>
<td>{cleanBranchName}</td>
<td><CommitHash commitId={commitId} /></td>
<td>{msg || '-'}</td>
<td><ReadableDate date={endTime} liveUpdate /></td>
<td>
<StopPropagation className="actions">
<RunPipeline organization={organization} pipeline={fullName} branch={encodeURIComponent(branchName)} />
<Extensions.Renderer
extensionPoint="jenkins.pipeline.branches.list.action"
pipeline={data}
store={this.context.store}
/>
</StopPropagation>
</td>
</tr>
);
}
}
@ -66,8 +75,8 @@ Branches.propTypes = {
data: object.isRequired,
};
Branches.contextTypes = {
store: object,
pipeline: object,
router: object.isRequired, // From react-router
location: object,

View File

@ -86,9 +86,8 @@ export class LogConsole extends Component {
this.timeouts.render = setTimeout(() => {
this._processNextLines();
}, RERENDER_DELAY);
} else {
this.scroll();
}
this.scroll();
}
scroll() {
@ -100,7 +99,7 @@ export class LogConsole extends Component {
* React needs the timeout to have the dom ready
*/
if (this.props.scrollToBottom && !match) {
this.timeouts.scroll = setTimeout(() => this.props.scrollBottom(), INITIAL_RENDER_DELAY + 1);
this.timeouts.scroll = setTimeout(() => this.props.scrollBottom(), RERENDER_DELAY + 1);
} else if (match) {
// we need to scroll to a certain line now
this.timeouts.scroll = this.props.scrollToAnchorTimeOut(RERENDER_DELAY + 1);
@ -109,7 +108,7 @@ export class LogConsole extends Component {
render() {
const lines = this.state.lines;
const { prefix = '' } = this.props;
const { prefix = '', hasMore = false } = this.props; // if hasMore true then show link to full log
if (!lines) {
return null;
}
@ -117,8 +116,22 @@ export class LogConsole extends Component {
return (<code
className="block"
>
{ hasMore && <div key={0} id={`${prefix}log-${0}`} className="fullLog">
<a
className="btn-secondary inverse"
key={0}
href={`?start=0#${prefix || ''}log-${1}`}
>
Show complete log
</a>
</div>}
{ lines.map((line, index) => <p key={index + 1} id={`${prefix}log-${index + 1}`}>
<a key={index + 1} href={`#${prefix || ''}log-${index + 1}`} name={`${prefix}log-${index + 1}`}>{line}</a>
<a
key={index + 1}
href={`#${prefix || ''}log-${index + 1}`}
name={`${prefix}log-${index + 1}`}
>{line}
</a>
</p>)}</code>);
}
}
@ -130,6 +143,7 @@ LogConsole.propTypes = {
scrollToAnchorTimeOut: func,
scrollBottom: func,
prefix: string,
hasMore: bool,
};
export default scrollHelper(LogConsole);

View File

@ -1,6 +1,7 @@
import React, { Component, PropTypes } from 'react';
import { Icon } from 'react-material-icons-blue';
import { fetchAllSuffix as suffix } from '../util/UrlUtils';
const { string } = PropTypes;
@ -12,6 +13,7 @@ export default class LogToolbar extends Component {
if (!url) {
return null;
}
const logUrl = url.includes(suffix) ? url : `${url}${suffix}`;
const style = { fill: '#4a4a4a' };
return (<div className="log-header">
<div className="log-header__section">
@ -21,13 +23,13 @@ export default class LogToolbar extends Component {
<a {...{
title: 'Display the log in new window',
target: '_blank',
href: `${url}?start=0`,
href: logUrl,
}}>
<Icon size={24} {...{ style, icon: 'launch' }} />
</a>
<a {...{
title: 'Download the log file',
href: `${url}?start=0&download=true`,
href: `${logUrl}&download=true`,
}}>
<Icon size={24} {...{ style, icon: 'file_download' }} />
</a>

View File

@ -1,5 +1,6 @@
import React, { Component, PropTypes } from 'react';
import { Link } from 'react-router';
import Extensions from '@jenkins-cd/js-extensions';
import { isFailure, isPending } from '../util/FetchStatus';
import NotFound from './NotFound';
import {
@ -9,7 +10,6 @@ import {
PageTabs,
TabLink,
WeatherIcon,
Favorite,
} from '@jenkins-cd/design-language';
import { buildOrganizationUrl, buildPipelineUrl } from '../util/UrlUtils';
@ -27,7 +27,7 @@ export default class PipelinePage extends Component {
if (isPending(pipeline)) {
return null;
}
if (isFailure(pipeline)) {
return <NotFound />;
}
@ -44,7 +44,11 @@ export default class PipelinePage extends Component {
<span> / </span>
<Link to={activityUrl}>{name}</Link>
</h1>
<Favorite className="dark-yellow" />
<Extensions.Renderer
extensionPoint="jenkins.pipeline.detail.header.action"
store={this.context.store}
pipeline={this.context.pipeline}
/>
</Title>
<PageTabs base={baseUrl}>
<TabLink to="/activity">Activity</TabLink>
@ -65,4 +69,5 @@ PipelinePage.propTypes = {
PipelinePage.contextTypes = {
location: PropTypes.object,
pipeline: PropTypes.object,
store: PropTypes.object,
};

View File

@ -38,7 +38,7 @@ export default class Pipelines extends Component {
];
const baseUrl = config.getRootURL();
const newJobUrl = `${baseUrl}view/All/newJob`;
const newJobUrl = `${baseUrl}/view/All/newJob`;
return (
<Page>

View File

@ -3,7 +3,6 @@ import {
ModalView,
ModalBody,
ModalHeader,
PipelineResult,
PageTabs,
TabLink,
} from '@jenkins-cd/design-language';
@ -23,6 +22,9 @@ import {
buildRunDetailsUrl,
} from '../util/UrlUtils';
import { RunDetailsHeader } from './RunDetailsHeader';
import { RunRecord } from './records';
const { func, object, array, any, string } = PropTypes;
class RunDetails extends Component {
@ -72,37 +74,29 @@ class RunDetails extends Component {
return null;
}
const {
router,
location,
params: {
organization,
branch,
runId,
pipeline: name,
},
} = this.context;
const baseUrl = buildRunDetailsUrl(organization, name, branch, runId);
/* eslint-disable arrow-body-style */
const currentRun = this.props.runs.filter((run) => {
return run.id === runId &&
decodeURIComponent(run.pipeline) === branch;
})[0];
currentRun.name = name;
const status = currentRun.result === 'UNKNOWN' ? currentRun.state : currentRun.result;
const { router, location, params } = this.context;
const baseUrl = buildRunDetailsUrl(params.organization, params.pipeline, params.branch, params.runId);
const foundRun = this.props.runs.find((run) =>
run.id === params.runId &&
decodeURIComponent(run.pipeline) === params.branch
);
// deep-linking across RunDetails for different pipelines yields 'runs' data for the wrong pipeline
// during initial render. when runs are refetched the screen will render again with 'currentRun' correctly set
if (!foundRun) {
return null;
}
const currentRun = new RunRecord(foundRun);
const status = currentRun.getComputedResult();
const afterClose = () => {
const fallbackUrl = buildPipelineUrl(organization, name);
const fallbackUrl = buildPipelineUrl(params.organization, params.pipeline);
location.pathname = this.opener || fallbackUrl;
router.push(location);
};
return (
<ModalView
isVisible
@ -113,7 +107,7 @@ class RunDetails extends Component {
>
<ModalHeader>
<div>
<PipelineResult data={currentRun}
<RunDetailsHeader data={currentRun}
onOrganizationClick={() => this.navigateToOrganization()}
onNameClick={() => this.navigateToPipeline()}
onAuthorsClick={() => this.navigateToChanges()}

View File

@ -0,0 +1,111 @@
// @flow
import React, { Component, PropTypes } from 'react';
import { Icon } from 'react-material-icons-blue';
import { ReadableDate } from '@jenkins-cd/design-language';
import { LiveStatusIndicator } from '@jenkins-cd/design-language';
import { TimeDuration } from '@jenkins-cd/design-language';
import moment from 'moment';
const { object, func } = PropTypes;
class RunDetailsHeader extends Component {
handleAuthorsClick() {
if (this.props.onAuthorsClick) {
this.props.onAuthorsClick();
}
}
handleOrganizationClick() {
if (this.props.onOrganizationClick) {
this.props.onOrganizationClick();
}
}
handleNameClick() {
if (this.props.onNameClick) {
this.props.onNameClick();
}
}
render() {
const { data: run } = this.props;
// Grab author from each change, run through a set for uniqueness
// FIXME-FLOW: Remove the ":any" cast after completion of https://github.com/facebook/flow/issues/1059
const authors = [...(new Set(run.changeSet.map(change => change.author.fullName)):any)];
const status = run.getComputedResult();
const durationMillis = run.isRunning() ?
moment().diff(moment(run.startTime)) : run.durationInMillis;
return (
<div className="pipeline-result">
<section className="status inverse">
<LiveStatusIndicator result={status} startTime={run.startTime}
estimatedDuration={run.estimatedDurationInMillis}
noBackground
/>
</section>
<section className="table">
<h4>
<a onClick={() => this.handleOrganizationClick()}>{run.organization}</a>
&nbsp;/&nbsp;
<a onClick={() => this.handleNameClick()}>{run.pipeline}</a>
&nbsp;
#{run.id}
</h4>
<div className="row">
<div className="commons">
<div>
<label>Branch</label>
<span>{decodeURIComponent(run.pipeline)}</span>
</div>
{ run.commitId ?
<div>
<label>Commit</label>
<span className="commit">
#{run.commitId.substring(0, 8)}
</span>
</div>
: null }
<div>
{ authors.length > 0 ?
<a className="authors" onClick={() => this.handleAuthorsClick()}>
Changes by {authors.map(
author => ` ${author}`)}
</a>
: 'No changes' }
</div>
</div>
<div className="times">
<div>
<Icon {...{
size: 20,
icon: 'timelapse',
style: { fill: '#fff' },
}} />
<TimeDuration millis={durationMillis} liveUpdate={run.isRunning()} />
</div>
<div>
<Icon {...{
size: 20,
icon: 'access_time',
style: { fill: '#fff' },
}} />
<ReadableDate date={run.endTime} liveUpdate />
</div>
</div>
</div>
</section>
</div>);
}
}
RunDetailsHeader.propTypes = {
data: object.isRequired,
colors: object,
onOrganizationClick: func,
onNameClick: func,
onAuthorsClick: func,
};
export { RunDetailsHeader };

View File

@ -4,6 +4,7 @@ import Extensions from '@jenkins-cd/js-extensions';
import LogConsole from './LogConsole';
import * as sse from '@jenkins-cd/sse-gateway';
import { EmptyStateView } from '@jenkins-cd/design-language';
import { Icon } from 'react-material-icons-blue';
import LogToolbar from './LogToolbar';
import Steps from './Steps';
@ -17,12 +18,25 @@ import {
createSelector,
} from '../redux';
import { calculateStepsBaseUrl, calculateRunLogURLObject, calculateNodeBaseUrl } from '../util/UrlUtils';
import { calculateStepsBaseUrl, calculateRunLogURLObject, calculateNodeBaseUrl, calculateFetchAll } from '../util/UrlUtils';
import { calculateNode } from '../util/KaraokeHelper';
const { string, object, any, func } = PropTypes;
const queuedState = () => (
<EmptyStateView tightSpacing>
<p>
<Icon {...{
size: 20,
icon: 'timer',
style: { fill: '#fff' },
}} />
<span>Waiting for run to start.</span>
</p>
</EmptyStateView>
);
export class RunDetailsPipeline extends Component {
constructor(props) {
super(props);
@ -34,80 +48,43 @@ export class RunDetailsPipeline extends Component {
}
componentWillMount() {
const { fetchNodes, fetchLog, result, fetchSteps } = this.props;
const { fetchNodes, fetchLog, result } = this.props;
this.mergedConfig = this.generateConfig(this.props);
// It should really be using capability using /rest/classes API
const supportsNode = result && result._class === 'io.jenkins.blueocean.rest.impl.pipeline.PipelineRunImpl';
if (supportsNode) {
fetchNodes(this.mergedConfig);
} else {
// console.log('fetch the log directly')
const logGeneral = calculateRunLogURLObject(this.mergedConfig);
fetchLog({ ...logGeneral });
if (!result.isQueued()) {
// It should really be using capability using /rest/classes API
const supportsNode = result && result._class === 'io.jenkins.blueocean.rest.impl.pipeline.PipelineRunImpl';
if (supportsNode) {
fetchNodes(this.mergedConfig);
} else {
// console.log('fetch the log directly')
const logGeneral = calculateRunLogURLObject(this.mergedConfig);
// fetchAll indicates whether we want all logs
const fetchAll = this.mergedConfig.fetchAll;
fetchLog({ ...logGeneral, fetchAll });
}
}
// Listen for pipeline flow node events.
// We filter them only for steps and the end event all other we let pass
const onSseEvent = (event) => {
const jenkinsEvent = event.jenkins_event;
// we are using try/catch to throw an early out error
try {
if (event.pipeline_run_id !== this.props.result.id) {
// console.log('early out');
throw new Error('exit');
}
// we turn on refetch so we always fetch a new Node result
const refetch = true;
switch (jenkinsEvent) {
case 'pipeline_step':
{
// we are not using an early out for the events since we want to refresh the node if we finished
if (this.state.followAlong) { // if we do it means we want karaoke
// if the step_stage_id has changed we need to change the focus
if (event.pipeline_step_stage_id !== this.mergedConfig.node) {
// console.log('nodes fetching via sse triggered');
delete this.mergedConfig.node;
fetchNodes({ ...this.mergedConfig, refetch });
} else {
// console.log('only steps fetching via sse triggered');
fetchSteps({ ...this.mergedConfig, refetch });
}
}
break;
}
case 'pipeline_end':
{
// we always want to refresh if the run has finished
fetchNodes({ ...this.mergedConfig, refetch });
break;
}
default:
{
// //console.log(event);
}
}
} catch (e) {
// we only ignore the exit error
if (e.message !== 'exit') {
throw e;
}
}
};
this.listener.sse = sse.subscribe('pipeline', onSseEvent);
this.listener.sse = sse.subscribe('pipeline', this._onSseEvent);
}
componentDidMount() {
// determine scroll area
const domNode = ReactDOM.findDOMNode(this.refs.scrollArea);
// add both listemer, one to the scroll area and another to the whole document
domNode.addEventListener('wheel', this.onScrollHandler, false);
document.addEventListener('keydown', this._handleKeys, false);
const { result } = this.props;
if (!result.isQueued()) {
// determine scroll area
const domNode = ReactDOM.findDOMNode(this.refs.scrollArea);
// add both listemer, one to the scroll area and another to the whole document
domNode.addEventListener('wheel', this.onScrollHandler, false);
document.addEventListener('keydown', this._handleKeys, false);
}
}
componentWillReceiveProps(nextProps) {
if (this.props.result.isQueued()) {
return;
}
const followAlong = this.state.followAlong;
this.mergedConfig = this.generateConfig({ ...nextProps, followAlong });
@ -127,10 +104,13 @@ export class RunDetailsPipeline extends Component {
// if we have actions we fire them
this.props[nodeAction.action](this.mergedConfig);
}
const fetchAll = this.mergedConfig.fetchAll;
// console.log('this.mergedConfig.fetchAll', fetchAll)
// if we only interested in logs (in case of e.g. freestyle)
const { logs, fetchLog } = nextProps;
if (logs !== this.props.logs) {
if (logs !== this.props.logs || fetchAll) {
const logGeneral = calculateRunLogURLObject(this.mergedConfig);
// console.log('logGenralReceive', logGeneral)
const log = logs ? logs[logGeneral.url] : null;
if (log && log !== null) {
// we may have a streaming log
@ -144,20 +124,29 @@ export class RunDetailsPipeline extends Component {
this.timeout = setTimeout(() => fetchLog({ ...logGeneral, newStart }), 1000);
}
}
} else if (fetchAll) {
// kill current timeout if any
clearTimeout(this.timeout);
// we need to get mpre input from the log stream
this.timeout = setTimeout(() => fetchLog({ ...logGeneral, fetchAll }), 1000);
}
}
}
componentWillUnmount() {
const domNode = ReactDOM.findDOMNode(this.refs.scrollArea);
domNode.removeEventListener('wheel', this._onScrollHandler);
document.removeEventListener('keydown', this._handleKeys);
if (this.listener.sse) {
sse.unsubscribe(this.listener.sse);
delete this.listener.sse;
}
if (this.props.result.isQueued()) {
return;
}
const domNode = ReactDOM.findDOMNode(this.refs.scrollArea);
this.props.cleanNodePointer();
clearTimeout(this.timeout);
domNode.removeEventListener('wheel', this._onScrollHandler);
document.removeEventListener('keydown', this._handleKeys);
}
// need to register handler to step out of karaoke mode
@ -167,6 +156,56 @@ export class RunDetailsPipeline extends Component {
this.setState({ followAlong: false });
}
}
// Listen for pipeline flow node events.
// We filter them only for steps and the end event all other we let pass
_onSseEvent(event) {
const { fetchNodes, fetchSteps } = this.props;
const jenkinsEvent = event.jenkins_event;
// we are using try/catch to throw an early out error
try {
if (event.pipeline_run_id !== this.props.result.id) {
// console.log('early out');
throw new Error('exit');
}
// we turn on refetch so we always fetch a new Node result
const refetch = true;
switch (jenkinsEvent) {
case 'pipeline_step':
{
// we are not using an early out for the events since we want to refresh the node if we finished
if (this.state.followAlong) { // if we do it means we want karaoke
// if the step_stage_id has changed we need to change the focus
if (event.pipeline_step_stage_id !== this.mergedConfig.node) {
// console.log('nodes fetching via sse triggered');
delete this.mergedConfig.node;
fetchNodes({ ...this.mergedConfig, refetch });
} else {
// console.log('only steps fetching via sse triggered');
fetchSteps({ ...this.mergedConfig, refetch });
}
}
break;
}
case 'pipeline_end':
{
// we always want to refresh if the run has finished
fetchNodes({ ...this.mergedConfig, refetch });
break;
}
default:
{
// //console.log(event);
}
}
} catch (e) {
// we only ignore the exit error
if (e.message !== 'exit') {
throw e;
}
}
}
// we bail out on arrow_up key
_handleKeys(event) {
if (event.keyCode === 38 && this.state.followAlong) {
@ -175,44 +214,41 @@ export class RunDetailsPipeline extends Component {
}
generateConfig(props) {
const {
config = {},
} = this.context;
const { config = {} } = this.context;
const followAlong = this.state.followAlong;
const {
isMultiBranch,
params: { pipeline: name, branch, runId, node: nodeParam },
} = props;
const { isMultiBranch, params } = props;
const fetchAll = calculateFetchAll(props);
// we would use default properties however the node can be null so no default properties will be triggered
let { nodeReducer } = props;
if (!nodeReducer) {
nodeReducer = { id: null, displayName: 'Steps' };
}
// if we have a node param we do not want the calculation of the focused node
const node = nodeParam || nodeReducer.id;
const node = params.node || nodeReducer.id;
const mergedConfig = { ...config, name, branch, runId, isMultiBranch, node, nodeReducer, followAlong };
return mergedConfig;
// Merge config
return {
...config,
name: params.pipeline,
branch: params.branch,
runId: params.runId,
isMultiBranch,
node,
nodeReducer,
followAlong,
fetchAll,
};
}
render() {
const {
location,
router,
} = this.context;
const { location, router } = this.context;
const {
params: {
pipeline: name, branch, runId,
},
isMultiBranch, steps, nodes, logs, result: resultMeta,
} = this.props;
const {
result,
state,
} = resultMeta;
const resultRun = result === 'UNKNOWN' || !result ? state : result;
const { isMultiBranch, steps, nodes, logs, result: run, params } = this.props;
if (run.isQueued()) {
return queuedState();
}
const resultRun = run.isCompleted() ? run.state : run.result;
const followAlong = this.state.followAlong;
// in certain cases we want that the log component will scroll to the end of a log
const scrollToBottom =
@ -267,16 +303,32 @@ export class RunDetailsPipeline extends Component {
};
const noSteps = !log && currentSteps && currentSteps.model && currentSteps.model.length === 0;
const shouldShowLogHeader = log !== null || !noSteps;
const logProps = {
scrollToBottom,
key: logGeneral.url,
};
if (log) {
// in follow along the Full Log button should not be shown, since you see everything already
if (followAlong) {
logProps.hasMore = false;
} else {
logProps.hasMore = log.hasMore;
}
logProps.logArray = log.logArray;
}
const stepScrollAreaClass = `step-scroll-area ${followAlong ? 'follow-along-on' : 'follow-along-off'}`;
return (
<div ref="scrollArea">
<div ref="scrollArea" className={stepScrollAreaClass}>
{ nodes && nodes[nodeKey] && <Extensions.Renderer
extensionPoint="jenkins.pipeline.run.result"
selectedStage={this.mergedConfig.nodeReducer}
callback={afterClick}
nodes={nodes[nodeKey].model}
pipelineName={name}
branchName={isMultiBranch ? branch : undefined}
runId={runId}
branchName={isMultiBranch ? params.branch : undefined}
runId={run.id}
/>
}
{ shouldShowLogHeader &&
@ -298,7 +350,7 @@ export class RunDetailsPipeline extends Component {
</EmptyStateView>
}
{ log && <LogConsole key={logGeneral.url} logArray={log.logArray} scrollToBottom={scrollToBottom} /> }
{ log && <LogConsole {...logProps} /> }
</div>
);
}

View File

@ -1,6 +1,6 @@
import React, { Component, PropTypes } from 'react';
import { ResultItem } from '@jenkins-cd/design-language';
import { calculateLogUrl } from '../util/UrlUtils';
import { calculateFetchAll, calculateLogUrl } from '../util/UrlUtils';
import LogConsole from './LogConsole';
@ -18,7 +18,8 @@ export default class Node extends Component {
const { config = {} } = this.context;
const node = this.expandAnchor(this.props);
if (node && node.isFocused) {
const mergedConfig = { ...config, node, nodesBaseUrl };
const fetchAll = node.fetchAll;
const mergedConfig = { ...config, node, nodesBaseUrl, fetchAll };
fetchLog(mergedConfig);
}
}
@ -33,8 +34,9 @@ export default class Node extends Component {
}
const { config = {} } = this.context;
const node = this.expandAnchor(nextProps);
const mergedConfig = { ...config, node, nodesBaseUrl };
if (logs && logs !== this.props.logs) {
const fetchAll = node.fetchAll;
const mergedConfig = { ...config, node, nodesBaseUrl, fetchAll };
if (logs && logs !== this.props.logs || fetchAll) {
const key = calculateLogUrl(mergedConfig);
const log = logs ? logs[key] : null;
if (log && log !== null) {
@ -47,6 +49,9 @@ export default class Node extends Component {
this.clearThisTimeout();
this.timeout = setTimeout(() => fetchLog({ ...mergedConfig }), 1000);
}
} else if (!log && fetchAll) { // in case the link "full log" is clicked we need to trigger a refetch
this.clearThisTimeout();
this.timeout = setTimeout(() => fetchLog({ ...mergedConfig }), 1000);
}
}
}
@ -60,26 +65,33 @@ export default class Node extends Component {
clearTimeout(this.timeout);
}
}
// Calculate whether we need to expand the step due to linking
/*
* Calculate whether we need to expand the step due to linking.
* When we trigger a log-0 that means we want to see the full log
*/
expandAnchor(props) {
const { node, location: { hash: anchorName } } = props;
const isFocused = true;
const fetchAll = calculateFetchAll(props);
const general = { ...node, fetchAll };
// e.g. #step-10-log-1 or #step-10
if (anchorName) {
const stepReg = /step-([0-9]{1,})?($|-log-([0-9]{1,})$)/;
const match = stepReg.exec(anchorName);
if (match && match[1] && match[1] === node.id) {
return { ...node, isFocused };
return { ...general, isFocused };
}
} else if (this.state && this.state.isFocused) {
return { ...node, isFocused };
return { ...general, isFocused };
}
return { ...node };
return general;
}
render() {
const { logs, nodesBaseUrl, fetchLog, followAlong } = this.props;
const node = this.expandAnchor(this.props);
const fetchAll = node.fetchAll;
// Early out
if (!node || !fetchLog) {
return null;
@ -95,7 +107,7 @@ export default class Node extends Component {
} = node;
const resultRun = result === 'UNKNOWN' || !result ? state : result;
const log = logs ? logs[calculateLogUrl({ ...config, node, nodesBaseUrl })] : null;
const log = logs ? logs[calculateLogUrl({ ...config, node, nodesBaseUrl, fetchAll })] : null;
const getLogForNode = () => {
// in case we do not have logs, or the logs are have no information attached we refetch them
if (!log || !log.logArray) {
@ -108,7 +120,24 @@ export default class Node extends Component {
resultRun.toLowerCase() === 'failure'
|| (resultRun.toLowerCase() === 'running' && followAlong)
;
return (<div>
const logProps = {
scrollToBottom,
key: id,
prefix: `step-${id}-`,
};
if (log) {
// in follow along the Full Log button should not be shown, since you see everything already
if (followAlong) {
logProps.hasMore = false;
} else {
logProps.hasMore = log.hasMore;
}
logProps.logArray = log.logArray;
}
const logConsoleClass = `logConsole step-${id}`;
return (<div className={logConsoleClass}>
<ResultItem
key={id}
result={runResult}
@ -117,12 +146,11 @@ export default class Node extends Component {
onExpand={getLogForNode}
durationMillis={durationInMillis}
>
{ log && <LogConsole
key={id}
logArray={log.logArray}
scrollToBottom={scrollToBottom}
prefix={`step-${id}-`}
/> } &nbsp;
{ log && <LogConsole {...logProps} /> }
{ !log && <span>
&nbsp;
</span> }
</ResultItem>
</div>);
}

View File

@ -0,0 +1,40 @@
/**
* Created by cmeyers on 7/27/16.
*/
import React, { Component, PropTypes } from 'react';
/**
* Stops propagation of click events inside this container.
* Useful for areas in UI where children should always handle the event, no matter what parent listeners are bound.
*
* This is a workaround for the following scenario:
* 1. Parent DOM element has a click listener,
* 2. Child DOM element added via an extension point calls event.stopPropagation() in its own click listener.
*
* This fails to work, even when calling stopProp and stopImmediateProp on the native event,
* probably beacuse there are two React trees each with their own document listener.
*
* see: http://stackoverflow.com/questions/24415631/reactjs-syntheticevent-stoppropagation-only-works-with-react-events
*/
export class StopPropagation extends Component {
_suppressEvent(event) {
event.stopPropagation();
}
render() {
return (
<div
className={this.props.className}
onClick={(event) => this._suppressEvent(event)}
>
{this.props.children}
</div>
);
}
}
StopPropagation.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
};

View File

@ -45,7 +45,8 @@ export const ChangeSetRecord = Record({
timestamp: null,
});
export const ActivityRecord = Record({
export class RunRecord extends Record({
_class: null,
changeSet: ChangeSetRecord,
durationInMillis: null,
enQueueTime: null,
@ -60,7 +61,24 @@ export const ActivityRecord = Record({
state: null,
type: null,
commitId: null,
});
}) {
isQueued() {
return this.state === 'QUEUED';
}
// We have a result
isCompleted() {
return this.result !== 'UNKNOWN';
}
isRunning() {
return this.state === 'RUNNING';
}
getComputedResult() {
return this.isCompleted() ? this.result : this.state;
}
}
export const PullRequestRecord = Record({
pullRequest: {
@ -72,7 +90,9 @@ export const PullRequestRecord = Record({
});
export const RunsRecord = Record({
latestRun: ActivityRecord,
_class: null,
_links: null,
latestRun: RunRecord,
name: null,
weatherScore: 0,
pullRequest: PullRequestRecord,

View File

@ -32,6 +32,7 @@ const TestCaseResultRow = (props) => {
let statusIndicator = null;
switch (t.status) {
case 'REGRESSION':
case 'FAILED':
statusIndicator = StatusIndicator.validResultValues.failure;
break;
@ -67,12 +68,11 @@ export default class TestResult extends Component {
const suites = this.props.testResults.suites;
const tests = [].concat.apply([], suites.map(t => t.cases));
// possible statuses: PASSED, FAILED, SKIPPED
const failures = tests.filter(t => t.status === 'FAILED');
// one of 5 possible statuses: PASSED, FIXED, SKIPPED, FAILED, REGRESSION see: hudson.tasks.junit.CaseResult$Status :(
const fixed = tests.filter(t => t.status === 'FIXED');
const skipped = tests.filter(t => t.status === 'SKIPPED');
const newFailures = failures.filter(t => t.age === 1);
const existingFailures = failures.filter(t => t.age > 1);
const newFailures = tests.filter(t => (t.age <= 1 && t.status === 'FAILED') || t.status === 'REGRESSION');
const existingFailures = tests.filter(t => t.age > 1 && t.status === 'FAILED');
let passBlock = null;
let newFailureBlock = null;
@ -116,42 +116,43 @@ export default class TestResult extends Component {
);
if (newFailures.length > 0) {
newFailureBlock = [
<h4>New failing - {newFailures.length}</h4>,
newFailures.map((t, i) => <TestCaseResultRow key={i} testCase={t} />),
];
newFailureBlock = (<div className="test-result-block new-failure-block">
<h4>New failing - {newFailures.length}</h4>
{newFailures.map((t, i) => <TestCaseResultRow key={i} testCase={t} />)}
</div>);
}
if (existingFailures.length > 0) {
existingFailureBlock = [
<h4>Existing failures - {existingFailures.length}</h4>,
existingFailures.map((t, i) => <TestCaseResultRow key={i} testCase={t} />),
];
}
if (fixed.length > 0) {
fixedBlock = [
<h4>Fixed</h4>,
fixed.map((t, i) => <TestCaseResultRow key={i} testCase={t} />),
];
existingFailureBlock = (<div className="test-result-block existing-failure-block">
<h4>Existing failures - {existingFailures.length}</h4>
{existingFailures.map((t, i) => <TestCaseResultRow key={i} testCase={t} />)}
</div>);
}
if (skipped.length > 0) {
skippedBlock = [
<h4>Skipped - {skipped.length}</h4>,
skipped.map((t, i) => <TestCaseResultRow key={i} testCase={t} />),
];
skippedBlock = (<div className="test-result-block skipped-block">
<h4>Skipped - {skipped.length}</h4>
{skipped.map((t, i) => <TestCaseResultRow key={i} testCase={t} />)}
</div>);
}
}
// always show fixed, whether showing totals or the encouraging message
if (fixed.length > 0) {
fixedBlock = (<div className="test-result-block fixed-block">
<h4>Fixed</h4>
{fixed.map((t, i) => <TestCaseResultRow key={i} testCase={t} />)}
</div>);
}
return (
<div>
{passBlock}
{summaryBlock}
{newFailureBlock}
{existingFailureBlock}
{fixedBlock}
{skippedBlock}
{passBlock}
</div>
);
}

View File

@ -6,6 +6,32 @@ import UrlConfig from '../config';
import { getNodesInformation } from '../util/logDisplayHelper';
import { calculateStepsBaseUrl, calculateLogUrl, calculateNodeBaseUrl } from '../util/UrlUtils';
/**
* This function maps a queue item into a run instancce.
*
* We do this because the api returns us queued items as well
* as runs and its easier to deal with them if they are modeled
* as the same thing. If the raw data is needed if can be fetched
* from _item.
*/
function _mapQueueToPsuedoRun(run) {
if (run._class === 'io.jenkins.blueocean.service.embedded.rest.QueueItemImpl') {
return {
id: String(run.expectedBuildNumber),
state: 'QUEUED',
pipeline: run.pipeline,
type: 'QueuedItem',
result: 'UNKNOWN',
job_run_queueId: run.id,
enQueueTime: run.queuedTime,
organization: run.organization,
changeSet: [],
_item: run,
};
}
return run;
}
// main actin logic
export const ACTION_TYPES = keymirror({
UPDATE_MESSAGES: null,
@ -57,7 +83,7 @@ export const actionHandlers = {
return state.set('currentRuns', null);
},
[ACTION_TYPES.SET_CURRENT_RUN_DATA](state, { payload }): State {
return state.set('currentRuns', payload);
return state.set('currentRuns', payload.map((run) => _mapQueueToPsuedoRun(run)));
},
[ACTION_TYPES.SET_NODE](state, { payload }): State {
return state.set('node', { ...payload });
@ -69,7 +95,8 @@ export const actionHandlers = {
},
[ACTION_TYPES.SET_RUNS_DATA](state, { payload, id }): State {
const runs = { ...state.runs } || {};
runs[id] = payload;
runs[id] = payload.map(run => _mapQueueToPsuedoRun(run));
return state.set('runs', runs);
},
[ACTION_TYPES.CLEAR_CURRENT_BRANCHES_DATA](state) {
@ -94,6 +121,7 @@ export const actionHandlers = {
[ACTION_TYPES.SET_LOGS](state, { payload }): State {
const logs = { ...state.logs } || {};
logs[payload.logUrl] = payload;
return state.set('logs', logs);
},
@ -557,7 +585,7 @@ export const actions = {
fetchRunsIfNeeded(config) {
return (dispatch) => {
const baseUrl = `${config.getAppURLBase()}/rest/organizations/jenkins` +
`/pipelines/${config.pipeline}/runs/`;
`/pipelines/${config.pipeline}/activities/`;
return dispatch(actions.fetchIfNeeded({
url: baseUrl,
id: config.pipeline,
@ -768,6 +796,7 @@ export const actions = {
const data = getState().adminStore.logs;
const logUrl = calculateLogUrl(config);
if (
config.fetchAll ||
!data || !data[logUrl] ||
config.newStart > 0 ||
(data && data[logUrl] && data[logUrl].newStart > 0 || !data[logUrl].logArray)
@ -777,10 +806,21 @@ export const actions = {
config.newStart || null,
response => response.response.text()
.then(text => {
// By default only last 150 KB log data is returned in the response.
const maxLength = 150000;
const contentLength = Number(response.response.headers.get('X-Text-Size'));
// set flag that there are more logs then we deliver
let hasMore = contentLength > maxLength;
// when we came from ?start=0, hasMore has to be false since there is no more
// console.log(config.fetchAll, 'inner')
if (config.fetchAll) {
hasMore = false;
}
const { newStart } = response;
const payload = {
logUrl,
newStart,
hasMore,
};
if (text && !!text.trim()) {
payload.logArray = text.trim().split('\n');

View File

@ -40,16 +40,43 @@ export const buildRunDetailsUrl = (organization, pipeline, branch, runId, tabNam
*/
export const uriString = (input) => encodeURIComponent(input).replace(/%2F/g, '%252F');
// general fetchAllTrigger
export const fetchAllSuffix = '?start=0';
// Add fetchAllSuffix in case it is needed
export const applyFetchAll = function (config, url) {
// if we pass fetchAll means we want the full log -> start=0 will trigger that on the server
if (config.fetchAll && !url.includes(fetchAllSuffix)) {
return `${url}${fetchAllSuffix}`;
}
return url;
};
// using the hook 'location.search'.includes('start=0') to trigger fetchAll
export const calculateFetchAll = function (props) {
const { location: { search } } = props;
if (search) {
const stepReg = /start=([0-9]{1,})/;
const match = stepReg.exec(search);
if (match && match[1] && Number(match[1]) === 0) {
return true;
}
}
return false;
};
/*
* helper to calculate log url. When we have a node we get create a special url, otherwise we use the url passed to us
* @param config { nodesBaseUrl, node, url}
*/
export const calculateLogUrl = (config) => {
let returnUrl = config.url;
if (config.node) {
const { nodesBaseUrl, node } = config;
return `${nodesBaseUrl}/${node.id}/log/`;
returnUrl = `${nodesBaseUrl}/${node.id}/log/`;
}
return config.url;
return applyFetchAll(config, returnUrl);
};
/*
@ -105,6 +132,7 @@ export function calculateRunLogURLObject(config) {
url = `${baseUrl}/runs/${runId}/log/`;
fileName = `${runId}.txt`;
}
url = applyFetchAll(config, url);
return {
url,
fileName,

View File

@ -10,10 +10,10 @@
height: 100%;
box-sizing: border-box;
z-index: 1000;
&.not-found {
background: @blueocean-blue;
&:before {
content: '';
background: url('./icons/kanagawa.svg') no-repeat bottom left;
@ -29,7 +29,7 @@
height: 100%;
}
}
.message-box {
position: absolute;
top: 50%;
@ -40,7 +40,7 @@
padding: 2em;
border-radius: .2em;
box-shadow: 2px 2px 8px rgba(0, 0, 0, .1);
.message {
margin: 1em 0 2em 0;
}
@ -48,113 +48,146 @@
}
.pipelines-table {
th {
width: 10%;
min-width: 100px;
}
th {
width: 10%;
min-width: 100px;
}
.name {
width: auto;
}
.name {
width: auto;
}
.favorite {
width: 30px;
}
.favorite {
width: 30px;
}
}
.activity-table {
th {
width: 75px;
}
th {
width: 75px;
}
.branch {
width: 175px;
}
.branch {
width: 175px;
}
.message {
width: 50%;
}
.message {
width: 50%;
}
.duration, .completed {
width: 125px;
}
.duration, .completed {
width: 125px;
}
.status-link {
cursor: pointer;
}
.status-link {
cursor: pointer;
}
}
.multibranch-table {
th {
width: 75px;
}
th {
width: 75px;
}
.branch {
width: 200px;
}
.branch {
width: 200px;
}
.message {
width: 50%;
}
.message {
width: 50%;
}
.lastcommit, .completed {
width: 125px;
}
.lastcommit, .completed {
width: 125px;
}
.actions {
display: flex;
align-items: center;
& > * {
margin-left: 5px;
}
}
}
.pr-table {
th {
width: 75px;
}
th {
width: 75px;
}
.summary {
width: 100%;
}
.summary {
width: 100%;
}
.build, .completed {
width: 125px;
}
.build, .completed {
width: 125px;
}
}
.changeset-table {
th {
width: 100px;
}
th {
width: 100px;
}
.author {
width: 150px;
}
.author {
width: 150px;
}
.message {
width: 100%;
}
.message {
width: 100%;
}
.date {
width: 125px;
}
.date {
width: 125px;
}
}
.artifacts-table {
th {
width: 100px;
}
th {
width: 100px;
}
.name {
width: 100%;
}
.name {
width: 100%;
}
.download {
text-align: right;
}
.download {
text-align: right;
}
}
.nodes {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px;
}
.nodes__section {
display: flex;
align-items: center;
display: flex;
align-items: center;
}
.logConsole .result-item-children {
background-color: @pre-bg;
border: none;
padding: 0;
}
code div {
font-size: 12px;
margin: 5px;
display: flex;
align-items: center;
justify-content: center;
}
code div a{
margin: 5px;
}
code div a.btn-secondary.inverse:hover{
background-color: @pre-color-hover;
}

View File

@ -1,9 +1,12 @@
.test-results-container {
h4 {
margin: 1em 0 .4em 0;
.test-result-block {
margin: 1em 0 0 0;
&:first-child {
margin-top: 0;
}
h4 {
margin: 0 0 .4em 0;
}
}
.test-console {
padding: 1em;

View File

@ -1,2 +1,6 @@
@blueocean-blue: #4A90E2;
@blueocean-blue-darkened: darken(@blueocean-blue, 20%);
@gray-base: #000;
@pre-bg: lighten(@gray-base, 20%); // #333
@pre-color: #f5f5f5;
@pre-color-hover: #444!important;

View File

@ -62,6 +62,25 @@ describe("TestResults", () => {
assert.equal(newFailed, 1);
});
it("Handles REGRESSION case", () => {
var failures = {
"_class":"hudson.tasks.junit.TestResult",
"duration":0.008, "empty":false, "failCount":3, "passCount":0, "skipCount":0, "suites":[
{ "duration":0, "id":null, "name":"failure.TestThisWontFail", "stderr":null, "stdout":null, "timestamp":null, "cases": [
{"age":5,"className":"failure.TestThisWontFail","duration":0,"errorDetails":null,"errorStackTrace":null,"failedSince":0,"name":"aPassingTest2","skipped":false,"skippedMessage":null,"status":"FAILED","stderr":null,"stdout":null},
{"age":2,"className":"failure.TestThisWontFail","duration":0,"errorDetails":null,"errorStackTrace":null,"failedSince":0,"name":"aPassingTest3","skipped":false,"skippedMessage":null,"status":"REGRESSION","stderr":null,"stdout":null},
{"age":1,"className":"failure.TestThisWontFail","duration":0,"errorDetails":null,"errorStackTrace":null,"failedSince":0,"name":"aPassingTest4","skipped":false,"skippedMessage":null,"status":"FAILED","stderr":null,"stdout":null},
],
}]};
let wrapper = shallow(<TestResults testResults={failures} />);
const newFailed = wrapper.find('.new-failure-block h4').text();
assert.equal(newFailed, 'New failing - 2');
const failed = wrapper.find('.existing-failure-block h4').text();
assert.equal(failed, 'Existing failures - 1');
});
it("All passing shown", () => {
let wrapper = shallow(<TestResults testResults={testResults1} />);
let isDone = wrapper.html().indexOf('done_all') > 0;
@ -78,7 +97,25 @@ describe("TestResults", () => {
}]};
wrapper = shallow(<TestResults testResults={success} />);
isDone = wrapper.html().indexOf('done_all') > 0;
assert(isDone, "Done all not found, when should be");
let html = wrapper.html();
assert(html.indexOf('done_all') > 0, "Done all not found, when should be");
assert(html.indexOf('fixed-block') < 0, "No fixed tests!");
});
it("All passing and fixed shown", () => {
var successWithFixed = {
"_class":"hudson.tasks.junit.TestResult",
"duration":0.008, "empty":false, "failCount":0, "passCount":3, "skipCount":0, "suites":[
{ "duration":0, "id":null, "name":"failure.TestThisWontFail", "stderr":null, "stdout":null, "timestamp":null, "cases": [
{"age":0,"className":"failure.TestThisWontFail","duration":0,"errorDetails":null,"errorStackTrace":null,"failedSince":0,"name":"aPassingTest2","skipped":false,"skippedMessage":null,"status":"FIXED","stderr":null,"stdout":null},
{"age":0,"className":"failure.TestThisWontFail","duration":0,"errorDetails":null,"errorStackTrace":null,"failedSince":0,"name":"aPassingTest3","skipped":false,"skippedMessage":null,"status":"PASSED","stderr":null,"stdout":null},
{"age":0,"className":"failure.TestThisWontFail","duration":0,"errorDetails":null,"errorStackTrace":null,"failedSince":0,"name":"aPassingTest4","skipped":false,"skippedMessage":null,"status":"PASSED","stderr":null,"stdout":null},
],
}]};
let wrapper = shallow(<TestResults testResults={successWithFixed} />);
let html = wrapper.html();
assert(html.indexOf('done_all') > 0, "Done all not found, when should be");
assert(html.indexOf('fixed-block') > 0, "Should have fixed tests!");
});
});

View File

@ -3,7 +3,7 @@
<parent>
<artifactId>blueocean-parent</artifactId>
<groupId>io.jenkins.blueocean</groupId>
<version>1.0-alpha-4-SNAPSHOT</version>
<version>1.0-alpha-7-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -34,16 +34,17 @@
"react-addons-test-utils": "15.0.1"
},
"dependencies": {
"@jenkins-cd/design-language": "0.0.63",
"@jenkins-cd/js-extensions": "0.0.17-beta-1",
"@jenkins-cd/design-language": "0.0.67",
"@jenkins-cd/js-extensions": "0.0.20",
"@jenkins-cd/js-modules": "0.0.5",
"@jenkins-cd/sse-gateway": "0.0.5",
"@jenkins-cd/sse-gateway": "0.0.7",
"immutable": "3.8.1",
"isomorphic-fetch": "2.2.1",
"keymirror": "0.1.1",
"moment": "2.13.0",
"moment-duration-format": "1.3.0",
"react": "15.1.0",
"react-addons-css-transition-group": "15.1.0",
"react-dom": "15.1.0",
"react-material-icons-blue": "1.0.4",
"react-redux": "4.4.5",

View File

@ -3,7 +3,7 @@
<parent>
<artifactId>blueocean-parent</artifactId>
<groupId>io.jenkins.blueocean</groupId>
<version>1.0-alpha-4-SNAPSHOT</version>
<version>1.0-alpha-7-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -2,13 +2,15 @@
* Created by cmeyers on 7/6/16.
*/
import React, { Component, PropTypes } from 'react';
import TransitionGroup from 'react-addons-css-transition-group';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { List } from 'immutable';
import { userSelector, favoritesSelector } from '../redux/FavoritesStore';
import { favoritesSelector } from '../redux/FavoritesStore';
import { actions } from '../redux/FavoritesActions';
import FavoritesProvider from './FavoritesProvider';
import { PipelineCard } from './PipelineCard';
// the order the cards should be displayed based on their result/state (aka 'status')
@ -90,54 +92,11 @@ const extractPath = (path, begin, end) => {
*/
export class DashboardCards extends Component {
constructor(props) {
super(props);
this.fetchUserInProgress = false;
this.fetchFavoritesInProgress = false;
}
componentWillMount() {
this._initialize(this.props);
}
componentWillReceiveProps(props) {
this._initialize(props);
}
_initialize(props) {
const config = this.context.config;
const { user, favorites } = props;
if (user) {
this.fetchUserInProgress = false;
}
if (favorites) {
this.fetchFavoritesInProgress = false;
}
if (config) {
const shouldFetchUser = !user && !this.fetchUserInProgress;
const shouldFetchFavorites = user && !favorites && !this.fetchFavoritesInProgress;
if (shouldFetchUser) {
this.fetchUserInProgress = true;
this.props.fetchUser(config);
}
if (shouldFetchFavorites) {
this.fetchFavoritesInProgress = true;
this.props.fetchFavorites(config, user);
}
}
}
_onFavoriteToggle(isFavorite, favorite) {
this.props.toggleFavorite(isFavorite, favorite.item);
this.props.toggleFavorite(isFavorite, favorite.item, favorite);
}
render() {
_renderCardStack() {
if (!this.props.favorites) {
return null;
}
@ -168,6 +127,7 @@ export class DashboardCards extends Component {
let startTime = null;
let estimatedDuration = null;
let commitId = null;
let runId = null;
if (latestRun) {
if (latestRun.result) {
@ -177,6 +137,7 @@ export class DashboardCards extends Component {
startTime = latestRun.startTime;
estimatedDuration = latestRun.estimatedDurationInMillis;
commitId = latestRun.commitId;
runId = latestRun.id;
}
if (latestRun && latestRun.result) {
@ -194,6 +155,7 @@ export class DashboardCards extends Component {
pipeline={pipelineName}
branch={branchName}
commitId={commitId}
runId={runId}
favorite
onFavoriteToggle={(isFavorite) => this._onFavoriteToggle(isFavorite, favorite)}
/>
@ -203,27 +165,34 @@ export class DashboardCards extends Component {
return (
<div className="favorites-card-stack">
{favoriteCards}
<TransitionGroup transitionName="vertical-expand-collapse"
transitionEnterTimeout={150}
transitionLeaveTimeout={150}
>
{favoriteCards}
</TransitionGroup>
</div>
);
}
render() {
return (
<FavoritesProvider store={this.props.store}>
{ this._renderCardStack() }
</FavoritesProvider>
);
}
}
DashboardCards.propTypes = {
user: PropTypes.object,
store: PropTypes.object,
favorites: PropTypes.instanceOf(List),
fetchUser: PropTypes.func,
fetchFavorites: PropTypes.func,
toggleFavorite: PropTypes.func,
};
DashboardCards.contextTypes = {
config: PropTypes.object,
};
const selectors = createSelector(
[userSelector, favoritesSelector],
(user, favorites) => ({ user, favorites })
[favoritesSelector],
(favorites) => ({ favorites })
);
export default connect(selectors, actions)(DashboardCards);

View File

@ -11,8 +11,12 @@ import { Favorite } from '@jenkins-cd/design-language';
import { favoritesSelector } from '../redux/FavoritesStore';
import { actions } from '../redux/FavoritesActions';
import { checkMatchingFavoriteUrls } from '../util/FavoriteUtils';
import FavoritesProvider from './FavoritesProvider';
/**
* A toggle button to favorite or unfavorite the provided item (pipeline or branch)
* Contains all logic for rendering the current favorite status of that item
* and toggling favorited state on the server.
*/
export class FavoritePipeline extends Component {
@ -34,18 +38,21 @@ export class FavoritePipeline extends Component {
}
}
_findMatchingFavorite(pipeline, favorites) {
if (!pipeline || !favorites) {
return null;
}
return favorites.find((fav) => {
const favUrl = fav.item._links.self.href;
const pipelineUrl = pipeline._links.self.href;
return checkMatchingFavoriteUrls(favUrl, pipelineUrl);
});
}
_updateState(props) {
const { pipeline } = props;
let favorite = null;
if (props.favorites) {
favorite = props.favorites.find((fav) => {
const favUrl = fav.item._links.self.href;
const pipelineUrl = pipeline._links.self.href;
return checkMatchingFavoriteUrls(favUrl, pipelineUrl);
});
}
const favorite = this._findMatchingFavorite(pipeline, props.favorites);
this.setState({
favorite: !!favorite,
@ -58,16 +65,20 @@ export class FavoritePipeline extends Component {
favorite: isFavorite,
});
const favorite = this._findMatchingFavorite(this.props.pipeline, this.props.favorites);
if (this.props.toggleFavorite) {
this.props.toggleFavorite(isFavorite, this.props.pipeline);
this.props.toggleFavorite(isFavorite, this.props.pipeline, favorite);
}
}
render() {
return (
<Favorite checked={this.state.favorite} className={this.props.className}
onToggle={() => this._onFavoriteToggle()}
/>
<FavoritesProvider store={this.props.store}>
<Favorite checked={this.state.favorite} className={this.props.className}
onToggle={() => this._onFavoriteToggle()}
/>
</FavoritesProvider>
);
}
}
@ -77,6 +88,7 @@ FavoritePipeline.propTypes = {
pipeline: PropTypes.object,
favorites: PropTypes.instanceOf(List),
toggleFavorite: PropTypes.func,
store: PropTypes.object,
};
FavoritePipeline.defaultProps = {

View File

@ -0,0 +1,18 @@
import React, { Component } from 'react';
import FavoritePipeline from './FavoritePipeline';
/**
* Restyled version of FavoritePipeline component.
*
* Created by cmeyers on 7/20/16.
*/
export class FavoritePipelineHeader extends Component {
render() {
return (
<FavoritePipeline { ...this.props } className="dark-yellow" />
);
}
}
export default FavoritePipelineHeader;

View File

@ -0,0 +1,64 @@
/**
* Created by cmeyers on 7/20/16.
*/
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { List } from 'immutable';
import { userSelector, favoritesSelector } from '../redux/FavoritesStore';
import { actions } from '../redux/FavoritesActions';
/**
* FavoritesProvider ensures that the current user's favorites
* are loaded for any components which may need it.
*
* Components that require this data can simply wrap themselves in
* FavoritesProvider which will ensure the store is updated correctly.
*/
export class FavoritesProvider extends Component {
componentWillMount() {
this._initialize(this.props);
}
componentWillReceiveProps(props) {
this._initialize(props);
}
_initialize(props) {
const { user, favorites } = props;
const shouldFetchUser = !user;
const shouldFetchFavorites = user && !favorites;
if (shouldFetchUser) {
this.props.fetchUser();
}
if (shouldFetchFavorites) {
this.props.fetchFavorites(user);
}
}
render() {
return this.props.children ?
React.cloneElement(this.props.children, { ...this.props }) :
null;
}
}
FavoritesProvider.propTypes = {
children: PropTypes.node,
user: PropTypes.object,
favorites: PropTypes.instanceOf(List),
fetchUser: PropTypes.func,
fetchFavorites: PropTypes.func,
};
const selectors = createSelector(
[userSelector, favoritesSelector],
(user, favorites) => ({ user, favorites })
);
export default connect(selectors, actions)(FavoritesProvider);

View File

@ -2,6 +2,7 @@
* Created by cmeyers on 6/28/16.
*/
import React, { Component, PropTypes } from 'react';
import { Link } from 'react-router';
import { Icon } from 'react-material-icons-blue';
import { Favorite, LiveStatusIndicator } from '@jenkins-cd/design-language';
@ -75,6 +76,10 @@ export class PipelineCard extends Component {
const showRun = status && (status.toLowerCase() === 'failure' || status.toLowerCase() === 'aborted');
const commitText = commitId ? commitId.substr(0, 7) : '';
const runUrl = `/organizations/${encodeURIComponent(this.props.organization)}/` +
`${encodeURIComponent(this.props.fullName)}/detail/` +
`${encodeURIComponent(this.props.branch || this.props.pipeline)}/${encodeURIComponent(this.props.runId)}/pipeline`;
return (
<div className={`pipeline-card ${bgClass}`}>
<LiveStatusIndicator
@ -83,13 +88,15 @@ export class PipelineCard extends Component {
/>
<span className="name">
{this.props.organization} / <span title={this.props.fullName}>{this.props.pipeline}</span>
<Link to={runUrl}>
{this.props.organization} / <span title={this.props.fullName}>{this.props.pipeline}</span>
</Link>
</span>
{ this.props.branch ?
<span className="branch">
<span className="octicon octicon-git-branch"></span>
<span className="branchText">{this.props.branch}</span>
<span className="branchText">{decodeURIComponent(this.props.branch)}</span>
</span>
:
<span className="branch"></span>
@ -129,6 +136,7 @@ PipelineCard.propTypes = {
pipeline: PropTypes.string,
branch: PropTypes.string,
commitId: PropTypes.string,
runId: PropTypes.string,
favorite: PropTypes.bool,
onRunClick: PropTypes.func,
onFavoriteToggle: PropTypes.func,

View File

@ -8,3 +8,7 @@ extensions:
extensionPoint: jenkins.pipeline.list.top
- component: components/FavoritePipeline
extensionPoint: jenkins.pipeline.list.action
- component: components/FavoritePipelineHeader
extensionPoint: jenkins.pipeline.detail.header.action
- component: components/FavoritePipeline
extensionPoint: jenkins.pipeline.branches.list.action

View File

@ -33,13 +33,24 @@ function parseJSON(response) {
});
}
const fetchFlags = {
[ACTION_TYPES.SET_USER]: false,
[ACTION_TYPES.SET_FAVORITES]: false,
};
export const actions = {
fetchUser(config) {
fetchUser() {
return (dispatch) => {
const baseUrl = config.getAppURLBase();
const baseUrl = urlConfig.blueoceanAppURL;
const url = `${baseUrl}/rest/organizations/jenkins/user/`;
const fetchOptions = { ...defaultFetchOptions };
if (fetchFlags[ACTION_TYPES.SET_USER]) {
return null;
}
fetchFlags[ACTION_TYPES.SET_USER] = true;
return dispatch(actions.generateData(
{ url, fetchOptions },
ACTION_TYPES.SET_USER
@ -47,13 +58,19 @@ export const actions = {
};
},
fetchFavorites(config, user) {
fetchFavorites(user) {
return (dispatch) => {
const baseUrl = config.getAppURLBase();
const baseUrl = urlConfig.blueoceanAppURL;
const username = user.id;
const url = `${baseUrl}/rest/users/${username}/favorites/`;
const fetchOptions = { ...defaultFetchOptions };
if (fetchFlags[ACTION_TYPES.SET_FAVORITES]) {
return null;
}
fetchFlags[ACTION_TYPES.SET_FAVORITES] = true;
return dispatch(actions.generateData(
{ url, fetchOptions },
ACTION_TYPES.SET_FAVORITES
@ -61,10 +78,14 @@ export const actions = {
};
},
toggleFavorite(addFavorite, branch) {
toggleFavorite(addFavorite, branch, favoriteToRemove) {
return (dispatch) => {
const baseUrl = urlConfig.jenkinsRootURL;
const url = `${baseUrl}${branch._links.self.href}/favorite`;
const url = addFavorite ?
`${baseUrl}${branch._links.self.href}/favorite` :
`${baseUrl}${favoriteToRemove._links.self.href}`;
const fetchOptions = {
...defaultFetchOptions,
method: 'PUT',
@ -89,12 +110,16 @@ export const actions = {
return (dispatch) => fetch(url, fetchOptions)
.then(checkStatus)
.then(parseJSON)
.then(json => dispatch({
...optional,
type: actionType,
payload: json,
}))
.then((json) => {
fetchFlags[actionType] = false;
return dispatch({
...optional,
type: actionType,
payload: json,
});
})
.catch((error) => {
fetchFlags[actionType] = false;
console.error(error); // eslint-disable-line no-console
// call again with no payload so actions handle missing data
dispatch({

View File

@ -13,8 +13,8 @@ const style2 = { paddingBottom: '10px' };
storiesOf('PipelineCard', module)
.add('all states', () => {
const states = 'SUCCESS,QUEUED,RUNNING,FAILURE,ABORTED,UNSTABLE,NOT_BUILT,UNKNOWN'.split(',');
const startTime = moment().subtract(30, 'seconds').toISOString();
const estimatedDuration = 60000;
const startTime = moment().subtract(60, 'seconds').toISOString();
const estimatedDuration = 1000 * 60 * 5; // 5 mins
return (
<div style={style}>

View File

@ -6,6 +6,10 @@
min-width: 400px;
padding: 15px;
a {
color: white;
}
.name, .branch, .commit {
margin-left: 10px;
}
@ -65,11 +69,15 @@
path.running {
stroke: white;
}
circle.inner {
stroke: white;
fill: white;
}
}
.progress-spinner.queued {
circle {
stroke: white;
stroke: #bcd8f1;
}
circle.inner {
stroke: white;

View File

@ -1,5 +1,9 @@
@import 'components/pipeline-card';
.multibranch-table .actions .checkbox {
margin-top: 3px;
}
.favorites-card-stack {
margin-bottom: 40px;
@ -7,3 +11,25 @@
margin-bottom: 10px;
}
}
.vertical-expand-collapse-enter {
transition: all linear 0.15s;
max-height: 0;
opacity: 0.01;
&.vertical-expand-collapse-enter-active {
max-height: 60px;
opacity: 1;
}
}
.vertical-expand-collapse-leave {
transition: all linear 0.15s;
max-height: 60px;
opacity: 1;
&.vertical-expand-collapse-leave-active {
max-height: 0;
opacity: 0.01;
}
}

View File

@ -26,7 +26,7 @@ describe('PipelineCard', () => {
assert.equal(wrapper.find('LiveStatusIndicator').length, 1);
assert.equal(wrapper.find('.name').length, 1);
assert.equal(wrapper.find('.name').text(), 'Jenkins / blueocean');
assert.equal(wrapper.find('.name').text(), '<Link />');
assert.equal(wrapper.find('.branch').length, 1);
assert.equal(wrapper.find('.branchText').text(), 'feature/JENKINS-123');
assert.equal(wrapper.find('.commit').length, 1);
@ -55,4 +55,17 @@ describe('PipelineCard', () => {
assert.equal(wrapper.find('.actions .run').length, 0);
});
it('escapes the branch name', () => {
const branchName = 'feature/JENKINS-667';
const wrapper = shallow(
<PipelineCard status="SUCCESS" organization="Jenkins" pipeline="blueocean"
branch={encodeURIComponent(branchName)} commitId="447d8e1"
/>
);
const elements = wrapper.find('.branchText');
assert.equal(elements.length, 1);
assert.equal(elements.at(0).text(), branchName);
});
});

View File

@ -5,7 +5,7 @@
<parent>
<groupId>io.jenkins.blueocean</groupId>
<artifactId>blueocean-parent</artifactId>
<version>1.0-alpha-4-SNAPSHOT</version>
<version>1.0-alpha-7-SNAPSHOT</version>
</parent>
<artifactId>blueocean-pipeline-api-impl</artifactId>

View File

@ -1,5 +1,7 @@
package io.jenkins.blueocean.rest.impl.pipeline;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
import hudson.Extension;
import hudson.model.Item;
import hudson.model.Job;
@ -9,17 +11,8 @@ import io.jenkins.blueocean.commons.ServiceException;
import io.jenkins.blueocean.rest.Navigable;
import io.jenkins.blueocean.rest.Reachable;
import io.jenkins.blueocean.rest.hal.Link;
import io.jenkins.blueocean.rest.model.BlueActionProxy;
import io.jenkins.blueocean.rest.model.BlueFavorite;
import io.jenkins.blueocean.rest.model.BlueFavoriteAction;
import io.jenkins.blueocean.rest.model.BlueMultiBranchPipeline;
import io.jenkins.blueocean.rest.model.BluePipeline;
import io.jenkins.blueocean.rest.model.BluePipelineContainer;
import io.jenkins.blueocean.rest.model.BlueQueueContainer;
import io.jenkins.blueocean.rest.model.BlueQueueItem;
import io.jenkins.blueocean.rest.model.BlueRun;
import io.jenkins.blueocean.rest.model.BlueRunContainer;
import io.jenkins.blueocean.rest.model.Resource;
import io.jenkins.blueocean.rest.hal.LinkResolver;
import io.jenkins.blueocean.rest.model.*;
import io.jenkins.blueocean.service.embedded.rest.BlueFavoriteResolver;
import io.jenkins.blueocean.service.embedded.rest.BluePipelineFactory;
import io.jenkins.blueocean.service.embedded.rest.FavoriteImpl;
@ -29,6 +22,7 @@ import io.jenkins.blueocean.service.embedded.util.FavoriteUtil;
import jenkins.branch.MultiBranchProject;
import jenkins.scm.api.SCMHead;
import jenkins.scm.api.actions.ChangeRequestAction;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.json.JsonBody;
import java.util.ArrayList;
@ -69,12 +63,8 @@ public class MultiBranchPipelineImpl extends BlueMultiBranchPipeline {
}
FavoriteUtil.favoriteJob(mbp.getFullName(), favoriteAction.isFavorite());
return FavoriteUtil.getFavorite(mbp, new Reachable() {
@Override
public Link getLink() {
return getLink().rel("branches");
}
});
return new FavoriteImpl(new BranchImpl(job,getLink().rel("branches")), getLink().rel("favorite"));
}
@Override
@ -351,10 +341,19 @@ public class MultiBranchPipelineImpl extends BlueMultiBranchPipeline {
Job job = project.getItem("master");
if(job != null){
Resource resource = BluePipelineFactory.resolve(job);
return new FavoriteImpl(resource, FavoriteUtil.getFavoriteLink(item.getFullName()));
Link l = LinkResolver.resolveLink(project);
if(l != null) {
return new FavoriteImpl(resource, l.rel("favorite"));
}
}
}
return null;
}
}
@Exported(inline = true)
@Navigable
public Container<Resource> getActivities() {
return Containers.fromResource(getLink(), Lists.newArrayList(Iterators.concat(getQueue().iterator(), getRuns().iterator())));
}
}

View File

@ -1,22 +1,17 @@
package io.jenkins.blueocean.rest.impl.pipeline;
import com.google.common.collect.Lists;
import hudson.model.Job;
import hudson.model.Queue;
import io.jenkins.blueocean.commons.ServiceException;
import io.jenkins.blueocean.rest.hal.Link;
import io.jenkins.blueocean.rest.model.BluePipeline;
import io.jenkins.blueocean.rest.model.BlueQueueContainer;
import io.jenkins.blueocean.rest.model.BlueQueueItem;
import io.jenkins.blueocean.service.embedded.rest.QueueContainerImpl;
import io.jenkins.blueocean.service.embedded.rest.QueueItemImpl;
import jenkins.model.Jenkins;
import org.jenkinsci.plugins.workflow.support.steps.ExecutorStepExecution;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
/**
* @author Vivek Pandey
@ -34,21 +29,11 @@ public class MultiBranchPipelineQueueContainer extends BlueQueueContainer {
@Override
public BlueQueueItem get(String name) {
try {
Queue.Item item = Jenkins.getActiveInstance().getQueue().getItem(Long.parseLong(name));
if(item != null){
BranchImpl pipeline = (BranchImpl) multiBranchPipeline.getBranches().get(item.task.getOwnerTask().getName());
if(pipeline != null) {
if(item.task instanceof ExecutorStepExecution.PlaceholderTask) {
ExecutorStepExecution.PlaceholderTask task = (ExecutorStepExecution.PlaceholderTask) item.task;
if(task.run() == null){
return QueueContainerImpl.getQueuedItem(item, pipeline.job);
}else{
return new QueueItemImpl(item, item.task.getOwnerTask().getName(), task.run().getNumber(),
self.rel(String.valueOf(item.getId())));
}
}
Queue.Item item = Jenkins.getInstance().getQueue().getItem(Long.parseLong(name));
if(item != null && item.task instanceof Job){
Job job = ((Job) item.task);
if(job.getParent() != null && job.getParent().getFullName().equals(multiBranchPipeline.mbp.getFullName())) {
return QueueContainerImpl.getQueuedItem(item, job);
}
}
}catch (NumberFormatException e){
@ -64,43 +49,12 @@ public class MultiBranchPipelineQueueContainer extends BlueQueueContainer {
@Override
public Iterator<BlueQueueItem> iterator() {
final List<BlueQueueItem> items = new ArrayList<>();
Map<String,List<Queue.Item>> queueMap = new HashMap<>();
for(Queue.Item item: Jenkins.getActiveInstance().getQueue().getItems()){
if(item.task instanceof ExecutorStepExecution.PlaceholderTask){
ExecutorStepExecution.PlaceholderTask task = (ExecutorStepExecution.PlaceholderTask) item.task;
String ownerTaskName = task.getOwnerTask().getName();
List<Queue.Item> its = queueMap.get(task.getOwnerTask().getName());
if(its == null){
its = new ArrayList<>();
queueMap.put(ownerTaskName,its);
}
its.add(item);
List<BlueQueueItem> queueItems = Lists.newArrayList();
for(Object o: multiBranchPipeline.mbp.getItems()) {
if(o instanceof Job) {
queueItems.addAll(QueueContainerImpl.getQueuedItems((Job)o));
}
}
for(final BluePipeline p:multiBranchPipeline.getBranches()){
Job job = ((BranchImpl)p).job;
List<Queue.Item> its = queueMap.get(job.getName());
if(its == null || its.isEmpty()){
continue;
}
int count=0;
for(Queue.Item item:its){
ExecutorStepExecution.PlaceholderTask task = (ExecutorStepExecution.PlaceholderTask) item.task;
if(task != null){
int runNumber;
if(task.run() == null){
runNumber = job.getNextBuildNumber() + count;
count++;
}else{
runNumber = task.run().getNumber();
}
items.add(new QueueItemImpl(item,p.getName(),
runNumber, self.rel(String.valueOf(item.getId()))));
}
}
}
return items.iterator();
return queueItems.iterator();
}
}

View File

@ -3,6 +3,7 @@ package io.jenkins.blueocean.rest.impl.pipeline;
import com.google.common.collect.ImmutableMap;
import hudson.Util;
import hudson.model.FreeStyleProject;
import hudson.model.Queue;
import hudson.plugins.favorite.user.FavoriteUserProperty;
import hudson.plugins.git.util.BuildData;
import hudson.scm.ChangeLogSet;
@ -14,8 +15,10 @@ import jenkins.branch.DefaultBranchPropertyStrategy;
import jenkins.plugins.git.GitSCMSource;
import jenkins.scm.api.SCMSource;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.SystemUtils;
import org.hamcrest.collection.IsArrayContainingInAnyOrder;
import org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval;
import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
import org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject;
@ -50,6 +53,9 @@ public class MultiBranchTest extends PipelineBaseTest {
@Rule
public GitSampleRepoRule sampleRepo = new GitSampleRepoRule();
@Rule
public GitSampleRepoRule sampleRepo1 = new GitSampleRepoRule();
private final String[] branches={"master", "feature%2Fux-1", "feature2"};
@ -446,7 +452,7 @@ public class MultiBranchTest extends PipelineBaseTest {
.auth("alice","alice")
.build(List.class);
Assert.assertEquals(l.size(), 1);
Assert.assertEquals(1,l.size());
branch = (Map)((Map)l.get(0)).get("item");
validatePipeline(p, branch);
@ -454,6 +460,30 @@ public class MultiBranchTest extends PipelineBaseTest {
c = (String) branch.get("_class");
Assert.assertEquals(BranchImpl.class.getName(), c);
Assert.assertEquals("/blue/rest/organizations/jenkins/pipelines/p/favorite/", getHrefFromLinks((Map)l.get(0), "self"));
String ref = getHrefFromLinks((Map)l.get(0), "self");
m = new RequestBuilder(baseUrl)
.put(getUrlFromHref(ref))
.auth("alice", "alice")
.data(ImmutableMap.of("favorite", false))
.build(Map.class);
branch = (Map) m.get("item");
validatePipeline(p, branch);
c = (String) branch.get("_class");
Assert.assertEquals(BranchImpl.class.getName(), c);
l = new RequestBuilder(baseUrl)
.get("/users/"+user.getId()+"/favorites/")
.auth("alice","alice")
.build(List.class);
Assert.assertEquals(0,l.size());
new RequestBuilder(baseUrl)
.get("/users/"+user.getId()+"/favorites/")
.auth("bob","bob")
@ -488,20 +518,44 @@ public class MultiBranchTest extends PipelineBaseTest {
validatePipeline(p1, (Map) map.get("item"));
Assert.assertEquals("/blue/rest/organizations/jenkins/pipelines/p/branches/feature2/favorite/", getHrefFromLinks(map, "self"));
List l = new RequestBuilder(baseUrl)
.get("/users/"+user.getId()+"/favorites/")
.auth("alice","alice")
.build(List.class);
Assert.assertEquals(l.size(), 1);
Assert.assertEquals(1, l.size());
Map branch = (Map)((Map)l.get(0)).get("item");
Assert.assertEquals("/blue/rest/organizations/jenkins/pipelines/p/branches/feature2/favorite/", getHrefFromLinks((Map)l.get(0), "self"));
validatePipeline(p1, branch);
String c = (String) branch.get("_class");
Assert.assertEquals(BranchImpl.class.getName(), c);
map = new RequestBuilder(baseUrl)
.put(getUrlFromHref(getHrefFromLinks((Map)l.get(0), "self")))
.auth("alice", "alice")
.data(ImmutableMap.of("favorite", false))
.build(Map.class);
validatePipeline(p1, (Map) map.get("item"));
Assert.assertEquals("/blue/rest/organizations/jenkins/pipelines/p/branches/feature2/favorite/", getHrefFromLinks(map, "self"));
l = new RequestBuilder(baseUrl)
.get("/users/"+user.getId()+"/favorites/")
.auth("alice","alice")
.build(List.class);
Assert.assertEquals(0, l.size());
new RequestBuilder(baseUrl)
.get("/users/"+user.getId()+"/favorites/")
.auth("bob","bob")
@ -548,15 +602,28 @@ public class MultiBranchTest extends PipelineBaseTest {
String c = (String) branch.get("_class");
Assert.assertEquals(BranchImpl.class.getName(), c);
String ref = getHrefFromLinks((Map)l.get(0), "self");
String href = getHrefFromLinks((Map)l.get(0), "self");
Assert.assertEquals("/blue/rest/organizations/jenkins/pipelines/p/favorite/", href);
Map r = new RequestBuilder(baseUrl)
.get(ref.substring("/blue/rest".length()))
.auth("alice","alice")
Map m = new RequestBuilder(baseUrl)
.put(getUrlFromHref(getUrlFromHref(href)))
.auth("alice", "alice")
.data(ImmutableMap.of("favorite", false))
.build(Map.class);
validatePipeline(p, (Map)r.get("item"));
branch = (Map) m.get("item");
validatePipeline(p, branch);
c = (String) branch.get("_class");
Assert.assertEquals(BranchImpl.class.getName(), c);
l = new RequestBuilder(baseUrl)
.get("/users/"+user.getId()+"/favorites/")
.auth("alice","alice")
.build(List.class);
Assert.assertEquals(0,l.size());
}
@Test
@ -662,4 +729,56 @@ public class MultiBranchTest extends PipelineBaseTest {
return p;
}
//Disabled test for now as I can't get it to work. Tested manually.
//@Test
public void getPipelineJobActivities() throws Exception {
WorkflowMultiBranchProject mp = j.jenkins.createProject(WorkflowMultiBranchProject.class, "p");
sampleRepo1.init();
sampleRepo1.write("Jenkinsfile", "stage 'build'\n "+"node {echo 'Building'}\n"+
"stage 'test'\nnode { echo 'Testing'}\n" +
"sleep 10000 \n"+
"stage 'deploy'\nnode { echo 'Deploying'}\n"
);
sampleRepo1.write("file", "initial content");
sampleRepo1.git("add", "Jenkinsfile");
sampleRepo1.git("commit", "--all", "--message=flow");
//create feature branch
sampleRepo1.git("checkout", "-b", "abc");
sampleRepo1.write("Jenkinsfile", "echo \"branch=${env.BRANCH_NAME}\"; "+"node {" +
" stage ('Build'); " +
" echo ('Building'); " +
" stage ('Test'); sleep 10000; " +
" echo ('Testing'); " +
" stage ('Deploy'); " +
" echo ('Deploying'); " +
"}");
ScriptApproval.get().approveSignature("method java.lang.String toUpperCase");
sampleRepo1.write("file", "subsequent content1");
sampleRepo1.git("commit", "--all", "--message=tweaked1");
mp.getSourcesList().add(new BranchSource(new GitSCMSource(null, sampleRepo1.toString(), "", "*", "", false),
new DefaultBranchPropertyStrategy(new BranchProperty[0])));
for (SCMSource source : mp.getSCMSources()) {
assertEquals(mp, source.getOwner());
}
scheduleAndFindBranchProject(mp);
for(WorkflowJob job : mp.getItems()) {
Queue.Item item = job.getQueueItem();
if(item != null ) {
item.getFuture().waitForStart();
}
job.setConcurrentBuild(false);
job.scheduleBuild2(0);
job.scheduleBuild2(0);
}
List l = request().get("/organizations/jenkins/pipelines/p/activities").build(List.class);
Assert.assertEquals(4, l.size());
Assert.assertEquals("io.jenkins.blueocean.service.embedded.rest.QueueItemImpl", ((Map) l.get(0)).get("_class"));
Assert.assertEquals("io.jenkins.blueocean.rest.impl.pipeline.PipelineRunImpl", ((Map) l.get(2)).get("_class"));
}
}

View File

@ -182,4 +182,29 @@ public class PipelineApiTest extends PipelineBaseTest {
Assert.assertTrue(size > 0);
}
@Test
public void getPipelineJobActivities() throws Exception {
WorkflowJob job1 = j.jenkins.createProject(WorkflowJob.class, "pipeline1");
job1.setDefinition(new CpsFlowDefinition("" +
"node {" +
" stage ('Build1'); " +
" echo ('Building'); " +
" stage ('Test1'); " +
" sleep 10000 " +
" echo ('Testing'); " +
"}"));
job1.setConcurrentBuild(false);
WorkflowRun r = job1.scheduleBuild2(0).waitForStart();
job1.scheduleBuild2(0);
List l = request().get("/organizations/jenkins/pipelines/pipeline1/activities").build(List.class);
Assert.assertEquals(2, l.size());
Assert.assertEquals("io.jenkins.blueocean.service.embedded.rest.QueueItemImpl", ((Map) l.get(0)).get("_class"));
Assert.assertEquals("io.jenkins.blueocean.rest.impl.pipeline.PipelineRunImpl", ((Map) l.get(1)).get("_class"));
}
}

View File

@ -332,6 +332,13 @@ public abstract class PipelineBaseTest{
return (String) l.get("href");
}
protected String getUrlFromHref(String href){
if(href.startsWith("/blue/rest")){
return href.substring("/blue/rest".length());
}
return href;
}
protected List<FlowNode> getParallelNodes(FlowGraphTable nodeGraphTable){
List<FlowNode> parallelNodes = new ArrayList<>();

View File

@ -5,7 +5,7 @@
<parent>
<groupId>io.jenkins.blueocean</groupId>
<artifactId>blueocean-parent</artifactId>
<version>1.0-alpha-4-SNAPSHOT</version>
<version>1.0-alpha-7-SNAPSHOT</version>
</parent>
<artifactId>blueocean</artifactId>
@ -44,6 +44,11 @@
<artifactId>blueocean-pipeline-api-impl</artifactId>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>blueocean-analytics-tools</artifactId>
</dependency>
<!-- Test deps -->
<dependency>
<groupId>${project.groupId}</groupId>
@ -107,6 +112,7 @@
linkHPI('blueocean-commons');
linkHPI('blueocean-rest-impl');
linkHPI('blueocean-pipeline-api-impl')
linkHPI('blueocean-analytics-tools')
</source>
</configuration>
</plugin>

View File

@ -54,13 +54,17 @@ public class JenkinsJSExtensionsTest extends BaseTest {
Assert.assertEquals("AdminNavLink", extensionPoints.get(0).get("component"));
Assert.assertEquals("jenkins.logo.top", extensionPoints.get(0).get("extensionPoint"));
} else if ("blueocean-personalization".equals(pluginId)) {
Assert.assertEquals(3, extensionPoints.size());
Assert.assertEquals(5, extensionPoints.size());
Assert.assertEquals("redux/FavoritesStore", extensionPoints.get(0).get("component"));
Assert.assertEquals("jenkins.main.stores", extensionPoints.get(0).get("extensionPoint"));
Assert.assertEquals("components/DashboardCards", extensionPoints.get(1).get("component"));
Assert.assertEquals("jenkins.pipeline.list.top", extensionPoints.get(1).get("extensionPoint"));
Assert.assertEquals("components/FavoritePipeline", extensionPoints.get(2).get("component"));
Assert.assertEquals("jenkins.pipeline.list.action", extensionPoints.get(2).get("extensionPoint"));
Assert.assertEquals("components/FavoritePipelineHeader", extensionPoints.get(3).get("component"));
Assert.assertEquals("jenkins.pipeline.detail.header.action", extensionPoints.get(3).get("extensionPoint"));
Assert.assertEquals("components/FavoritePipeline", extensionPoints.get(4).get("component"));
Assert.assertEquals("jenkins.pipeline.branches.list.action", extensionPoints.get(4).get("extensionPoint"));
} else {
Assert.fail("Found extensions from unknown pluginId: " + pluginId);
}

View File

@ -5,7 +5,7 @@
<parent>
<groupId>io.jenkins.blueocean</groupId>
<artifactId>blueocean-parent</artifactId>
<version>1.0-alpha-4-SNAPSHOT</version>
<version>1.0-alpha-7-SNAPSHOT</version>
</parent>
<artifactId>blueocean-rest-impl</artifactId>

View File

@ -1,5 +1,7 @@
package io.jenkins.blueocean.service.embedded.rest;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
import hudson.Extension;
import hudson.model.Action;
import hudson.model.Item;
@ -8,7 +10,6 @@ import io.jenkins.blueocean.commons.ServiceException;
import io.jenkins.blueocean.rest.Navigable;
import io.jenkins.blueocean.rest.Reachable;
import io.jenkins.blueocean.rest.hal.Link;
import io.jenkins.blueocean.service.embedded.util.FavoriteUtil;
import io.jenkins.blueocean.rest.model.BlueActionProxy;
import io.jenkins.blueocean.rest.model.BlueFavorite;
import io.jenkins.blueocean.rest.model.BlueFavoriteAction;
@ -16,9 +17,13 @@ import io.jenkins.blueocean.rest.model.BluePipeline;
import io.jenkins.blueocean.rest.model.BlueQueueContainer;
import io.jenkins.blueocean.rest.model.BlueRun;
import io.jenkins.blueocean.rest.model.BlueRunContainer;
import io.jenkins.blueocean.rest.model.Container;
import io.jenkins.blueocean.rest.model.Containers;
import io.jenkins.blueocean.rest.model.Resource;
import io.jenkins.blueocean.service.embedded.util.FavoriteUtil;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.WebMethod;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.json.JsonBody;
import org.kohsuke.stapler.verb.DELETE;
@ -96,6 +101,7 @@ public class PipelineImpl extends BluePipeline {
return new QueueContainerImpl(this);
}
@WebMethod(name="") @DELETE
public void delete() throws IOException, InterruptedException {
job.delete();
@ -179,4 +185,9 @@ public class PipelineImpl extends BluePipeline {
}
@Exported(inline = true)
@Navigable
public Container<Resource> getActivities() {
return Containers.fromResource(getLink(),Lists.newArrayList(Iterators.concat(getQueue().iterator(), getRuns().iterator())));
}
}

View File

@ -34,6 +34,11 @@ public class QueueItemImpl extends BlueQueueItem {
return Long.toString(item.getId());
}
@Override
public String getOrganization() {
return OrganizationImpl.INSTANCE.getName();
}
@Override
public String getPipeline() {
return pipelineName;

View File

@ -1,9 +1,6 @@
package io.jenkins.blueocean.service.embedded.util;
import hudson.Util;
import hudson.model.Item;
import hudson.model.ItemGroup;
import hudson.model.Job;
import hudson.model.User;
import hudson.plugins.favorite.FavoritePlugin;
import hudson.plugins.favorite.user.FavoriteUserProperty;
@ -16,7 +13,6 @@ import io.jenkins.blueocean.rest.model.BluePipeline;
import io.jenkins.blueocean.service.embedded.rest.BlueFavoriteResolver;
import io.jenkins.blueocean.service.embedded.rest.BluePipelineFactory;
import io.jenkins.blueocean.service.embedded.rest.FavoriteImpl;
import io.jenkins.blueocean.service.embedded.rest.UserImpl;
import jenkins.model.Jenkins;
import org.kohsuke.stapler.Stapler;
@ -53,22 +49,6 @@ public class FavoriteUtil {
}
}
public static Link getFavoriteLink(String fullName){
User user = User.current();
if(user != null) {
return new UserImpl(user).getLink().rel("favorites/"+ FavoriteUtil.encodeFullName(fullName));
}
return null;
}
public static boolean isFavorableItem(Item i){
return i!= null && (i instanceof Job || i instanceof ItemGroup);
}
public static String encodeFullName(String name){
return Util.rawEncode(Util.rawEncode(name));
}
public static String decodeFullName(String name){
try {
return URLDecoder.decode(URLDecoder.decode(name, "UTF-8"), "UTF-8");
@ -119,15 +99,9 @@ public class FavoriteUtil {
}
}
//otherwise, default
Link favouriteLink = getFavoriteLink(item.getFullName());
if(favouriteLink == null){
return null;
}
BluePipeline pipeline = BluePipelineFactory.getPipelineInstance(item, parent);
if(pipeline != null){
return new FavoriteImpl(pipeline,favouriteLink);
return new FavoriteImpl(pipeline,pipeline.getLink().rel("favorite"));
}
return null;

View File

@ -15,6 +15,7 @@ import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.MockFolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -227,24 +228,6 @@ public abstract class BaseTest {
throw new RuntimeException(e);
}
}
//
// protected void validateMultiBranchPipeline(WorkflowMultiBranchProject p, Map resp, int numBranches){
// validateMultiBranchPipeline(p, resp, numBranches, -1, -1);
// }
// protected void validateMultiBranchPipeline(WorkflowMultiBranchProject p, Map resp, int numBranches, int numSuccBranches, int numOfFailingBranches){
// Assert.assertEquals("jenkins", resp.get("organization"));
// Assert.assertEquals(p.getName(), resp.get("name"));
// Assert.assertEquals(p.getDisplayName(), resp.get("displayName"));
// Assert.assertNull(resp.get("lastSuccessfulRun"));
// Assert.assertEquals(numBranches, resp.get("totalNumberOfBranches"));
// if(numOfFailingBranches >= 0) {
// Assert.assertEquals(numOfFailingBranches, resp.get("numberOfFailingBranches"));
// }
// if(numSuccBranches >= 0) {
// Assert.assertEquals(numSuccBranches, resp.get("numberOfSuccessfulBranches"));
// }
// Assert.assertEquals(p.getBuildHealth().getScore(), resp.get("weatherScore"));
// }
protected void validatePipeline(Job p, Map resp){
Assert.assertEquals("jenkins", resp.get("organization"));
@ -271,6 +254,17 @@ public abstract class BaseTest {
}
}
protected void validateFolder(MockFolder folder, Map resp){
Assert.assertEquals("jenkins", resp.get("organization"));
Assert.assertEquals(folder.getName(), resp.get("name"));
Assert.assertEquals(folder.getDisplayName(), resp.get("displayName"));
Assert.assertEquals(folder.getFullName(), resp.get("fullName"));
Assert.assertNull(resp.get("lastSuccessfulRun"));
Assert.assertEquals(folder.getAllJobs().size(), resp.get("numberOfPipelines"));
Assert.assertEquals(folder.getAllJobs().size(), resp.get("numberOfPipelines"));
}
protected void validateRun(Run r, Map resp){
validateRun(r,resp, "FINISHED");
}
@ -285,39 +279,12 @@ public abstract class BaseTest {
Assert.assertEquals(state, resp.get("state"));
}
// protected String getNodeName(FlowNode n){
// return n.getAction(ThreadNameAction.class) != null
// ? n.getAction(ThreadNameAction.class).getThreadName()
// : n.getDisplayName();
// }
private String getBaseUrl(String path){
return baseUrl + path;
}
// protected List<FlowNode> getStages(FlowGraphTable nodeGraphTable){
// List<FlowNode> nodes = new ArrayList<>();
// for(FlowGraphTable.Row row: nodeGraphTable.getRows()){
// if(PipelineNodeUtil.isStage(row.getNode()) ||
// PipelineNodeUtil.isParallelBranch(row.getNode())){
// nodes.add(row.getNode());
// }
// }
// return nodes;
// }
// protected List<FlowNode> getParallelNodes(FlowGraphTable nodeGraphTable){
// List<FlowNode> parallelNodes = new ArrayList<>();
//
// for(FlowGraphTable.Row row: nodeGraphTable.getRows()){
// if(PipelineNodeUtil.isParallelBranch(row.getNode())){
// parallelNodes.add(row.getNode());
// }
// }
// return parallelNodes;
// }
protected String getHrefFromLinks(Map resp, String link){
Map links = (Map) resp.get("_links");
if(links == null){

View File

@ -2,11 +2,13 @@ package io.jenkins.blueocean.service.embedded;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import hudson.model.FreeStyleProject;
import hudson.model.Project;
import hudson.model.User;
import hudson.tasks.Mailer;
import org.junit.Assert;
import org.junit.Test;
import org.jvnet.hudson.test.MockFolder;
import java.util.Collections;
import java.util.List;
@ -90,8 +92,10 @@ public class ProfileApiTest extends BaseTest{
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
hudson.model.User user = j.jenkins.getUser("alice");
user.setFullName("Alice Cooper");
Project p = j.createFreeStyleProject("pipeline1");
Map map = new RequestBuilder(baseUrl)
.put("/organizations/jenkins/pipelines/pipeline1/favorite")
.auth("alice", "alice")
@ -104,11 +108,121 @@ public class ProfileApiTest extends BaseTest{
.auth("alice","alice")
.build(List.class);
Assert.assertEquals(l.size(), 1);
Assert.assertEquals(1, l.size());
Map pipeline = (Map)((Map)l.get(0)).get("item");
validatePipeline(p, pipeline);
String href = getHrefFromLinks((Map)l.get(0),"self");
Assert.assertEquals("/blue/rest/organizations/jenkins/pipelines/pipeline1/favorite/", href);
map = new RequestBuilder(baseUrl)
.put(href.substring("/blue/rest".length()))
.auth("alice", "alice")
.data(ImmutableMap.of("favorite", false))
.build(Map.class);
validatePipeline(p, (Map) map.get("item"));
l = new RequestBuilder(baseUrl)
.get("/users/"+user.getId()+"/favorites/")
.auth("alice","alice")
.build(List.class);
Assert.assertEquals(0, l.size());
new RequestBuilder(baseUrl)
.get("/users/"+user.getId()+"/favorites/")
.auth("bob","bob")
.status(403)
.build(String.class);
}
@Test
public void createUserFavouriteFolderTest() throws Exception {
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
hudson.model.User user = j.jenkins.getUser("alice");
user.setFullName("Alice Cooper");
MockFolder folder1 = j.createFolder("folder1");
Project p = folder1.createProject(FreeStyleProject.class, "pipeline1");
Map map = new RequestBuilder(baseUrl)
.put("/organizations/jenkins/pipelines/folder1/pipelines/pipeline1/favorite/")
.auth("alice", "alice")
.data(ImmutableMap.of("favorite", true))
.build(Map.class);
validatePipeline(p, (Map) map.get("item"));
List l = new RequestBuilder(baseUrl)
.get("/users/"+user.getId()+"/favorites/")
.auth("alice","alice")
.build(List.class);
Assert.assertEquals(1, l.size());
Map pipeline = (Map)((Map)l.get(0)).get("item");
validatePipeline(p, pipeline);
String href = getHrefFromLinks((Map)l.get(0),"self");
Assert.assertEquals("/blue/rest/organizations/jenkins/pipelines/folder1/pipelines/pipeline1/favorite/", href);
map = new RequestBuilder(baseUrl)
.put(href.substring("/blue/rest".length()))
.auth("alice", "alice")
.data(ImmutableMap.of("favorite", false))
.build(Map.class);
validatePipeline(p, (Map) map.get("item"));
l = new RequestBuilder(baseUrl)
.get("/users/"+user.getId()+"/favorites/")
.auth("alice","alice")
.build(List.class);
Assert.assertEquals(0, l.size());
map = new RequestBuilder(baseUrl)
.put("/organizations/jenkins/pipelines/folder1/favorite/")
.auth("alice", "alice")
.data(ImmutableMap.of("favorite", true))
.build(Map.class);
validateFolder(folder1, (Map) map.get("item"));
l = new RequestBuilder(baseUrl)
.get("/users/"+user.getId()+"/favorites/")
.auth("alice","alice")
.build(List.class);
Assert.assertEquals(1, l.size());
Map folder = (Map)((Map)l.get(0)).get("item");
validateFolder(folder1, folder);
href = getHrefFromLinks((Map)l.get(0),"self");
Assert.assertEquals("/blue/rest/organizations/jenkins/pipelines/folder1/favorite/", href);
map = new RequestBuilder(baseUrl)
.put(href.substring("/blue/rest".length()))
.auth("alice", "alice")
.data(ImmutableMap.of("favorite", false))
.build(Map.class);
validateFolder(folder1, (Map) map.get("item"));
l = new RequestBuilder(baseUrl)
.get("/users/"+user.getId()+"/favorites/")
.auth("alice","alice")
.build(List.class);
Assert.assertEquals(0, l.size());
new RequestBuilder(baseUrl)
.get("/users/"+user.getId()+"/favorites/")
.auth("bob","bob")

View File

@ -56,7 +56,9 @@
- [Favorite API](#favorite-api)
- [Favorite a pipeline](#favorite-a-pipeline)
- [Favorite a multi branch pipeline](#favorite-a-multi-branch-pipeline)
- [Un-favorite a multi branch pipeline](#un-favorite-a-multi-branch-pipeline)
- [Favorite a multi branch pipeline branch](#favorite-a-multi-branch-pipeline-branch)
- [Un-favorite a multi branch pipeline branch](#un-favorite-a-multi-branch-pipeline-branch)
- [Fetch user favorites](#fetch-user-favorites)
- [Log API](#log-api)
- [Fetching logs](#fetching-logs)
@ -1279,7 +1281,7 @@ If favorite request is successful then the repsonse is favorited item.
"_links" : {
"self" : {
"_class" : "io.jenkins.blueocean.rest.hal.Link",
"href" : "/blue/rest/users/alice/favorites/pipeline1/"
"href" : "/blue/rest/organizations/jenkins/pipelines/pipeline1/favorite/"
}
},
"item" : {
@ -1319,19 +1321,32 @@ If favorite request is successful then the repsonse is favorited item.
## Favorite a pipeline
Returns 200 on success. Must be authenticated.
curl -u bob:bob -H"Content-Type:application/json" -XPUT -d '{"favorite":true} ttp://localhost:56748/jenkins/blue/rest/organizations/jenkins/pipelines/pipeline1/favorite
curl -u bob:bob -H"Content-Type:application/json" -XPUT -d '{"favorite":true} ttp://localhost:56748/jenkins/blue/rest/organizations/jenkins/pipelines/pipeline1/favorite/
## Favorite a multi branch pipeline
Must be authenticated.
This favorites the master branch. Returns 200 on success. 500 if master does not exist
Favorited multi-branch pipeline returns master branch as favorited item. Returns 200 on success. 400 if master does not exist
curl -u bob:bob -H"Content-Type:application/json" -XPUT -d '{"favorite":true} http://localhost:56748/jenkins/blue/rest/organizations/jenkins/pipelines/pipeline1/favorite/
## Un-favorite a multi branch pipeline
Must be authenticated.
This un-favorites the master branch. Returns 200 on success. 400 if master does not exist
curl -u bob:bob -H"Content-Type:application/json" -XPUT -d '{"favorite":false} http://localhost:56748/jenkins/blue/rest/organizations/jenkins/pipelines/pipeline1/favorite/
curl -u bob:bob -H"Content-Type:application/json" -XPUT -d '{"favorite":true} http://localhost:56748/jenkins/blue/rest/organizations/jenkins/pipelines/pipeline1/favorite
## Favorite a multi branch pipeline branch
Returns 200 on success. Must be authenticated.
curl -H"Content-Type:application/json" -XPUT -d '{"favorite":true} http://localhost:56748/jenkins/blue/rest/organizations/jenkins/pipelines/pipeline1/branches/master/favorite
curl -H"Content-Type:application/json" -XPUT -d '{"favorite":true} http://localhost:56748/jenkins/blue/rest/organizations/jenkins/pipelines/pipeline1/branches/master/favorite/
## Un-favorite a multi branch pipeline branch
Returns 200 on success. Must be authenticated.
curl -H"Content-Type:application/json" -XPUT -d '{"favorite":false} http://localhost:56748/jenkins/blue/rest/organizations/jenkins/pipelines/pipeline1/branches/master/favorite/
## Fetch user favorites

View File

@ -5,7 +5,7 @@
<parent>
<groupId>io.jenkins.blueocean</groupId>
<artifactId>blueocean-parent</artifactId>
<version>1.0-alpha-4-SNAPSHOT</version>
<version>1.0-alpha-7-SNAPSHOT</version>
</parent>
<artifactId>blueocean-rest</artifactId>

View File

@ -26,6 +26,8 @@ public abstract class BlueQueueItem extends Resource {
@Exported
public abstract String getId();
@Exported
public abstract String getOrganization();
/**
*
* @return pipeline this queued item belongs too

View File

@ -18,15 +18,15 @@
"babel-preset-react": "^6.5.0",
"eslint": "2.8.0",
"eslint-plugin-react": "^5.0.1",
"giti": "^1.0.6",
"giti": "1.1.3",
"gulp": "^3.9.1",
"jquery-detached": "^2.1.4-v4",
"ncp": "^2.0.0",
"zombie": "^4.2.1"
},
"dependencies": {
"@jenkins-cd/design-language": "0.0.63",
"@jenkins-cd/js-extensions": "0.0.19",
"@jenkins-cd/design-language": "0.0.67",
"@jenkins-cd/js-extensions": "0.0.20",
"@jenkins-cd/js-modules": "0.0.5",
"history": "2.0.2",
"immutable": "3.8.1",
@ -47,7 +47,6 @@
"immutable",
"react-router",
"keymirror",
"react-addons-css-transition-group",
"react-redux",
"react-router",
"redux",

View File

@ -5,7 +5,7 @@
<parent>
<groupId>io.jenkins.blueocean</groupId>
<artifactId>blueocean-parent</artifactId>
<version>1.0-alpha-4-SNAPSHOT</version>
<version>1.0-alpha-7-SNAPSHOT</version>
</parent>
<artifactId>blueocean-web</artifactId>

View File

@ -1,8 +1,8 @@
package io.jenkins.blueocean;
import hudson.ExtensionList;
import hudson.model.UsageStatistics;
import jenkins.model.Jenkins;
import java.util.List;
/**
* Root of Blue Ocean UI
@ -37,18 +37,7 @@ public class BlueOceanUI {
return urlBase;
}
/**
* @return True if jenkins reports usage statistics.
*/
public boolean includeRollbar() {
return false;
// return Jenkins.getInstance().isUsageStatisticsCollected() && !UsageStatistics.DISABLED;
}
/**
* @return Version on the plugin e.g 1.0-SNAPSHOT (private-f9a14d3e-jenkins)
*/
public String getPluginVersion() {
return Jenkins.getInstance().getPlugin("blueocean-web").getWrapper().getVersion();
public List<BluePageDecorator> getPageDecorators(){
return BluePageDecorator.all();
}
}

View File

@ -0,0 +1,61 @@
package io.jenkins.blueocean;
import hudson.Extension;
import hudson.ExtensionList;
import hudson.ExtensionPoint;
/**
* Participates in the rendering of HTML pages for all pages of Hudson.
*
* <p>
* This class provides a few hooks to augument the HTML generation process of Hudson, across
* all the HTML pages that Hudson delivers.
*
* <p>
* For example, if you'd like to add a Google Analytics stat to Hudson, then you need to inject
* a small script fragment to all Hudson pages. This extension point provides a means to do that.
*
* <h2>Life-cycle</h2>
* <p>
* Plugins that contribute this extension point
* should implement a new decorator and put {@link Extension} on the class.
*
* <h2>Associated Views</h2>
*
*
* <h3>header.jelly</h3>
* <p>
* This page is added right before the &lt;/head&gt; tag. Convenient place for additional stylesheet, &lt;meta&gt; tags, etc.
*
* <pre>
* &lt;j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler"&gt;
* &lt;script&gt;
* //your JS code
* &lt;/script&gt;
* &lt;/j:jelly&gt;
* </pre>
*
* <h3>httpHeaders.jelly</h3>
*
* For example, this httpHeader.jelly adds HTTP X-MY-HEADER
*
* <pre>
*
* &lt;?jelly escape-by-default='true'?&gt;
* &lt;j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler"&gt;
* &lt;st:header name="X-MY-HEADER" value="${it.someValue}"/&gt;
* &lt;/j:jelly&gt;
*
* </pre>
* <p>
* This is a generalization of the X-Jenkins header that aids auto-discovery.
* This fragment can write additional &lt;st:header name="..." value="..." /&gt; tags that go along with it.
*
* @author Vivek Pandey
*/
public abstract class BluePageDecorator implements ExtensionPoint {
public static ExtensionList<BluePageDecorator> all() {
return ExtensionList.lookup(BluePageDecorator.class);
}
}

View File

@ -5,13 +5,12 @@ import revisionInfo from '../../../target/classes/io/jenkins/blueocean/revisionI
export class DevelopmentFooter extends Component {
render() {
if (!revisionInfo || !revisionInfo.name) {
var blueOceanVersion = document.getElementsByTagName('head')[0].getAttribute('data-blue-ocean-version');
return (
<div className="development-footer">
<span>Blue Ocean UI v{blueOceanVersion}</span>
</div>
);
// testing basic integrity
if (!revisionInfo || !revisionInfo.sha) {
// TODO: At minimum we should return Jenkins version. Jenkins version is always present
// in X-Hudson HTTP header. Something to be handled elsewhere during load time by
// inspecting HTTP response headers
return null;
}
return (
<div className="development-footer">

View File

@ -6,10 +6,10 @@
export default class Config {
constructor(options) {
this._appURLBase = options.appURLBase || "/";
this._rootURL = options.rootURL || "/";
this._resourceURL = options.resourceURL || "/";
this._adjunctURL = options.adjunctURL || "/";
this._appURLBase = options.appURLBase || '';
this._rootURL = options.rootURL || '';
this._resourceURL = options.resourceURL || '';
this._adjunctURL = options.adjunctURL || '';
}
getAppURLBase() {

View File

@ -51,8 +51,10 @@ exports.initialize = function (oncomplete) {
// Load and export the react modules, allowing them to be imported by other bundles.
const react = require('react');
const reactDOM = require('react-dom');
const reactCSSTransitions = require('react-addons-css-transition-group');
jenkinsMods.export('react', 'react', react);
jenkinsMods.export('react', 'react-dom', reactDOM);
jenkinsMods.export('react', 'react-addons-css-transition-group', reactCSSTransitions);
// Get the extension list metadata from Jenkins.
// Might want to do some flux fancy-pants stuff for this.

View File

@ -84,7 +84,7 @@ function startApp(routes, stores) {
let appURLBase = headElement.getAttribute("data-appurl");
if (typeof appURLBase !== "string") {
appURLBase = "/";
appURLBase = '';
}
// Look up some other URLs we may need

View File

@ -1,6 +1,11 @@
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:x="jelly:xml">
<st:contentType value="text/html;charset=UTF-8"/>
<!-- Add HTTP headers from extensions. See BluePageDecorator.java -->
<j:forEach var="pd" items="${it.pageDecorators}">
<st:include it="${pd}" page="httpHeaders.jelly" optional="true"/>
</j:forEach>
<x:doctype name="html"/>
<html>
@ -9,11 +14,10 @@
<j:invokeStatic var="j" className="jenkins.model.Jenkins" method="getActiveInstance"/>
${h.initPageVariables(context)}
<head data-rooturl="${rootURL}/"
<head data-rooturl="${rootURL}"
data-resurl="${resURL}"
data-appurl="${rootURL}/${it.urlBase}"
data-adjuncturl="${rootURL}/${j.getAdjuncts('').rootURL}"
data-blue-ocean-version="${it.getPluginVersion()}">
data-adjuncturl="${rootURL}/${j.getAdjuncts('').rootURL}">
<title>Jenkins Blue Ocean</title>
@ -31,36 +35,10 @@
href="${resURL}/plugin/blueocean-web/assets/css/jenkins-design-language.css"
type="text/css"/>
<j:if test="${it.includeRollbar()}">
<script>
function normalizeURL(location) {
var normalizedUrl = 'http://anon.blueocean.io' + location;
var headElements = document.getElementsByTagName('head');
if (headElements.length == 1) {
var rootUrl = headElements[0].getAttribute('data-rooturl');
if (location.startsWith(rootUrl)) {
normalizedUrl = 'http://anon.blueocean.io' + location.substring(rootUrl.length -1, location.length);
}
}
return normalizedUrl;
}
var transformer = function(payload) {
payload.data.request.user_ip = '0.0.0.0';
payload.data.request.url = normalizeURL(window.location.pathname);
};
var _rollbarConfig = {
accessToken: "81f3134dedf44871b9cc0a347b1313df",
captureUncaught: true,
code_version: "${it.getPluginVersion()}",
source_map_enabled: true,
guess_uncaught_frames: true,
transform: transformer
};
<![CDATA[
!function(r){function e(t){if(o[t])return o[t].exports;var n=o[t]={exports:{},id:t,loaded:!1};return r[t].call(n.exports,n,n.exports,e),n.loaded=!0,n.exports}var o={};return e.m=r,e.c=o,e.p="",e(0)}([function(r,e,o){"use strict";var t=o(1).Rollbar,n=o(2);_rollbarConfig.rollbarJsUrl=_rollbarConfig.rollbarJsUrl||"https://d37gvrvc0wt4s1.cloudfront.net/js/v1.9/rollbar.min.js";var a=t.init(window,_rollbarConfig),i=n(a,_rollbarConfig);a.loadFull(window,document,!_rollbarConfig.async,_rollbarConfig,i)},function(r,e){"use strict";function o(r){return function(){try{return r.apply(this,arguments)}catch(e){try{console.error("[Rollbar]: Internal error",e)}catch(o){}}}}function t(r,e,o){window._rollbarWrappedError&&(o[4]||(o[4]=window._rollbarWrappedError),o[5]||(o[5]=window._rollbarWrappedError._rollbarContext),window._rollbarWrappedError=null),r.uncaughtError.apply(r,o),e&&e.apply(window,o)}function n(r){var e=function(){var e=Array.prototype.slice.call(arguments,0);t(r,r._rollbarOldOnError,e)};return e.belongsToShim=!0,e}function a(r){this.shimId=++c,this.notifier=null,this.parentShim=r,this._rollbarOldOnError=null}function i(r){var e=a;return o(function(){if(this.notifier)return this.notifier[r].apply(this.notifier,arguments);var o=this,t="scope"===r;t&&(o=new e(this));var n=Array.prototype.slice.call(arguments,0),a={shim:o,method:r,args:n,ts:new Date};return window._rollbarShimQueue.push(a),t?o:void 0})}function l(r,e){if(e.hasOwnProperty&&e.hasOwnProperty("addEventListener")){var o=e.addEventListener;e.addEventListener=function(e,t,n){o.call(this,e,r.wrap(t),n)};var t=e.removeEventListener;e.removeEventListener=function(r,e,o){t.call(this,r,e&&e._wrapped?e._wrapped:e,o)}}}var c=0;a.init=function(r,e){var t=e.globalAlias||"Rollbar";if("object"==typeof r[t])return r[t];r._rollbarShimQueue=[],r._rollbarWrappedError=null,e=e||{};var i=new a;return o(function(){if(i.configure(e),e.captureUncaught){i._rollbarOldOnError=r.onerror,r.onerror=n(i);var o,a,c="EventTarget,Window,Node,ApplicationCache,AudioTrackList,ChannelMergerNode,CryptoOperation,EventSource,FileReader,HTMLUnknownElement,IDBDatabase,IDBRequest,IDBTransaction,KeyOperation,MediaController,MessagePort,ModalWindow,Notification,SVGElementInstance,Screen,TextTrack,TextTrackCue,TextTrackList,WebSocket,WebSocketWorker,Worker,XMLHttpRequest,XMLHttpRequestEventTarget,XMLHttpRequestUpload".split(",");for(o=0;o<c.length;++o)a=c[o],r[a]&&r[a].prototype&&l(i,r[a].prototype)}return e.captureUnhandledRejections&&(i._unhandledRejectionHandler=function(r){var e=r.reason,o=r.promise,t=r.detail;!e&&t&&(e=t.reason,o=t.promise),i.unhandledRejection(e,o)},r.addEventListener("unhandledrejection",i._unhandledRejectionHandler)),r[t]=i,i})()},a.prototype.loadFull=function(r,e,t,n,a){var i=function(){var e;if(void 0===r._rollbarPayloadQueue){var o,t,n,i;for(e=new Error("rollbar.js did not load");o=r._rollbarShimQueue.shift();)for(n=o.args,i=0;i<n.length;++i)if(t=n[i],"function"==typeof t){t(e);break}}"function"==typeof a&&a(e)},l=!1,c=e.createElement("script"),d=e.getElementsByTagName("script")[0],p=d.parentNode;c.crossOrigin="",c.src=n.rollbarJsUrl,c.async=!t,c.onload=c.onreadystatechange=o(function(){if(!(l||this.readyState&&"loaded"!==this.readyState&&"complete"!==this.readyState)){c.onload=c.onreadystatechange=null;try{p.removeChild(c)}catch(r){}l=!0,i()}}),p.insertBefore(c,d)},a.prototype.wrap=function(r,e){try{var o;if(o="function"==typeof e?e:function(){return e||{}},"function"!=typeof r)return r;if(r._isWrap)return r;if(!r._wrapped){r._wrapped=function(){try{return r.apply(this,arguments)}catch(e){throw e._rollbarContext=o()||{},e._rollbarContext._wrappedSource=r.toString(),window._rollbarWrappedError=e,e}},r._wrapped._isWrap=!0;for(var t in r)r.hasOwnProperty(t)&&(r._wrapped[t]=r[t])}return r._wrapped}catch(n){return r}};for(var d="log,debug,info,warn,warning,error,critical,global,configure,scope,uncaughtError,unhandledRejection".split(","),p=0;p<d.length;++p)a.prototype[d[p]]=i(d[p]);r.exports={Rollbar:a,_rollbarWindowOnError:t}},function(r,e){"use strict";r.exports=function(r,e){return function(o){if(!o&&!window._rollbarInitialized){var t=window.RollbarNotifier,n=e||{},a=n.globalAlias||"Rollbar",i=window.Rollbar.init(n,r);i._processShimQueue(window._rollbarShimQueue||[]),window[a]=i,window._rollbarInitialized=!0,t.processPayloads()}}}}]);
]]>
</script>
</j:if>
<!-- Inject headers from other extensions. See BluePageDecorator.java -->
<j:forEach var="pd" items="${it.pageDecorators}">
<st:include it="${pd}" page="header.jelly" optional="true" />
</j:forEach>
</head>
<body>

View File

@ -146,6 +146,7 @@ function createBundle(jsxFile) {
.withExternalModuleMapping('@jenkins-cd/design-language', 'jenkins-cd:jdl')
.withExternalModuleMapping('react', 'react:react')
.withExternalModuleMapping('react-dom', 'react:react-dom')
.withExternalModuleMapping('react-addons-css-transition-group', 'react:react-addons-css-transition-group')
.inDir('target/classes/org/jenkins/ui/jsmodules/' + maven.getArtifactId());
}

View File

@ -1,6 +1,6 @@
{
"name": "@jenkins-cd/js-extensions",
"version": "0.0.19",
"version": "0.0.20",
"description": "Jenkins Extension Store",
"main": "index.js",
"files": [

11
pom.xml
View File

@ -10,7 +10,7 @@
<groupId>io.jenkins.blueocean</groupId>
<artifactId>blueocean-parent</artifactId>
<version>1.0-alpha-4-SNAPSHOT</version>
<version>1.0-alpha-7-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Blue Ocean UI Parent</name>
@ -105,6 +105,7 @@
<module>blueocean-dashboard</module>
<module>blueocean-personalization</module>
<module>blueocean-plugin</module>
<module>blueocean-analytics-tools</module>
</modules>
<repositories>
@ -172,6 +173,12 @@
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>blueocean-analytics-tools</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>blueocean-plugin</artifactId>
@ -183,7 +190,7 @@
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>sse-gateway</artifactId>
<version>1.5</version>
<version>1.8</version>
</dependency>
<dependency>