Compare commits
59 Commits
keith/JENK
...
master
Author | SHA1 | Date |
---|---|---|
Ivan Meredith | 574aed8ce3 | |
Michael Neale | db31a290f7 | |
Michael Neale | 5ee06d03c7 | |
Tom Fennelly | 29dbde63eb | |
Tom Fennelly | 0dfe60a6a8 | |
Tom Fennelly | f98037d136 | |
Tom Fennelly | 34eff2a236 | |
Tom Fennelly | f5f0c0ecf9 | |
Cliff Meyers | eea4b1697d | |
Cliff Meyers | e52c309a83 | |
Cliff Meyers | c8197db439 | |
Vivek Pandey | b8f4cf9699 | |
Vivek Pandey | ad0bfecd72 | |
Thorsten Scherler | 693c48fd3f | |
Cliff Meyers | 203df4b7d8 | |
Tom Fennelly | a12ae4e452 | |
Cliff Meyers | 36966d4793 | |
Cliff Meyers | 39b4ffa0de | |
Cliff Meyers | d65165f6ab | |
Cliff Meyers | 33f643ea37 | |
vivek | e181cf5a91 | |
Cliff Meyers | 789882e4ea | |
tfennelly | 759e920c5b | |
tfennelly | c5011d8cc1 | |
Cliff Meyers | 8a636726fa | |
Thorsten Scherler | 3958b8aaa3 | |
Cliff Meyers | e84f3204e0 | |
Cliff Meyers | b8789358b1 | |
Cliff Meyers | 19bdafa271 | |
Cliff Meyers | cbb7fc9687 | |
Thorsten Scherler | 4b96cb3af6 | |
Cliff Meyers | 7f74eeb7ec | |
Cliff Meyers | 29c877055b | |
Keith Zantow | 91c4188539 | |
vivek | b8c038abac | |
Cliff Meyers | 0624b7d416 | |
Cliff Meyers | 3227570ee9 | |
Vivek Pandey | 619618213c | |
Cliff Meyers | 387f470af8 | |
Thorsten Scherler | 6de55426ef | |
Keith Zantow | 501e74db31 | |
Thorsten Scherler | 3b3cd1764e | |
Thorsten Scherler | 64f4ce5ea6 | |
vivek | b71a48c31d | |
Michael Neale | 2a7baee0d9 | |
Michael Neale | f344c261c6 | |
Keith Zantow | d78a16e21a | |
Thorsten Scherler | 6c1a0c0543 | |
Cliff Meyers | 18038c83f0 | |
Cliff Meyers | 7ec7d89157 | |
Cliff Meyers | d2f6cc00f6 | |
Cliff Meyers | 429eb8e898 | |
Cliff Meyers | 9beb47a37f | |
Cliff Meyers | 072c6883f8 | |
Cliff Meyers | e759913ed1 | |
Cliff Meyers | e3b616ad45 | |
Cliff Meyers | b804b8cf38 | |
Cliff Meyers | 32978e73a5 | |
Cliff Meyers | 3299527fae |
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
|
@ -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);
|
||||
}
|
|
@ -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');
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
//}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<?jelly escape-by-default='true'?>
|
||||
<div>
|
||||
BlueOcean Analytics Tools plugin
|
||||
</div>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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>);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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()}
|
||||
|
|
|
@ -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>
|
||||
/
|
||||
<a onClick={() => this.handleNameClick()}>{run.pipeline}</a>
|
||||
|
||||
#{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 };
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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}-`}
|
||||
/> }
|
||||
{ log && <LogConsole {...logProps} /> }
|
||||
|
||||
{ !log && <span>
|
||||
|
||||
</span> }
|
||||
</ResultItem>
|
||||
</div>);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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!");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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;
|
|
@ -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);
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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())));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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<>();
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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())));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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){
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 </head> tag. Convenient place for additional stylesheet, <meta> tags, etc.
|
||||
*
|
||||
* <pre>
|
||||
* <j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler">
|
||||
* <script>
|
||||
* //your JS code
|
||||
* </script>
|
||||
* </j:jelly>
|
||||
* </pre>
|
||||
*
|
||||
* <h3>httpHeaders.jelly</h3>
|
||||
*
|
||||
* For example, this httpHeader.jelly adds HTTP X-MY-HEADER
|
||||
*
|
||||
* <pre>
|
||||
*
|
||||
* <?jelly escape-by-default='true'?>
|
||||
* <j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler">
|
||||
* <st:header name="X-MY-HEADER" value="${it.someValue}"/>
|
||||
* </j:jelly>
|
||||
*
|
||||
* </pre>
|
||||
* <p>
|
||||
* This is a generalization of the X-Jenkins header that aids auto-discovery.
|
||||
* This fragment can write additional <st:header name="..." value="..." /> 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);
|
||||
}
|
||||
}
|
|
@ -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">
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
@ -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
11
pom.xml
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue