Compare commits

...

40 Commits

Author SHA1 Message Date
Cliff Meyers de70f06aab Merge branch 'bug/JENKINS-36668' into bug/JENKINS-36615-link-resolver-message-enricher 2016-07-14 10:18:31 -04:00
Cliff Meyers cc429589c0 [JENKINS-36615] use LinkResolver to populate "blueocean_job_rest_url" 2016-07-13 16:42:27 -04:00
Cliff Meyers 11e065f7b8 Merge pull request #336 from jenkinsci/bug/url-encoding-bugs
bug/url encoding bugs
2016-07-13 09:53:58 -04:00
James William Dumay 54aeee582b Update PULL_REQUEST_TEMPLATE 2016-07-13 12:31:50 +10:00
James William Dumay 1b2416c142 Update PULL_REQUEST_TEMPLATE 2016-07-13 12:31:17 +10:00
Thorsten Scherler b487981981 Merge pull request #324 from jenkinsci/feature/JENKINS-36211
Feature/jenkins 36211
2016-07-12 23:57:43 +02:00
Cliff Meyers 466ca34cc3 Merge branch 'master' into bug/url-encoding-bugs 2016-07-12 16:51:36 -04:00
Cliff Meyers 2d656ec22a Merge pull request #333 from jenkinsci/bug/JENKINS-36618-dup-job-name
bug/jenkins 36618 - dup job name
2016-07-12 16:49:12 -04:00
vivek 5ceaaebb0a Merge pull request #335 from jenkinsci/bug/JENKINS-36489
JENKINS-36489# NPE fix
2016-07-12 13:09:58 -07:00
Cliff Meyers c2ee18aed0 Merge branch 'bug/JENKINS-36616-multibranch-in-folder-broken' into bug/url-encoding-bugs 2016-07-12 15:50:47 -04:00
Cliff Meyers 34ee8f2df3 [JENKINS-36613] fix a bug where multi-branch pipelines with a slash in branch name would not load steps on Run Details -> Pipeline; also tweak filter syntax to make things easier to debug 2016-07-12 15:45:47 -04:00
Thorsten Scherler 6e60171920 [feature/JENKINS-36211] better handling of eventlistener 2016-07-12 21:41:08 +02:00
Cliff Meyers e257a5ef51 [JENKINS-36616] fix a bug where navigating to run details from Branches tab was double URL-encoding the branch name, causing the subsequent page to break 2016-07-12 15:40:16 -04:00
Vivek Pandey e28c77e13a JENKINS-36489# NPE fix 2016-07-12 12:09:24 -07:00
Cliff Meyers 7260759b0e [JENKINS-36616] part 1: fix a bug where clicking on a branch in Pipeline Detail -> Branches was going to an old URL that did not include the folder name 2016-07-12 14:59:43 -04:00
Cliff Meyers 22a2c93676 [JENKINS-36618] port the other tests over from skin-deep to Enzyme 2016-07-12 13:57:39 -04:00
Cliff Meyers cfd82f0613 [JENKINS-36618] actually fix the bug by using the self.href as the unique key 2016-07-12 13:50:29 -04:00
Cliff Meyers c1d7e4ca2e [JENKINS-36618] refactor JSDOM bootstrapping and js-extensions fixes into a util; cleanup some of hte test data; drop Immutable usage from spec to avoid spurious warnings 2016-07-12 13:50:02 -04:00
Cliff Meyers 74bc89dfb0 [JENKINS-36618] implement a unit test that exhibits the bug: this requires Enzyme.mount to expose a duplicate key error in React, and some correspond work to integrate jsdom and make ExtensionStore happy. next step, reduce boilerplate 2016-07-12 13:16:54 -04:00
Thorsten Scherler 2b71744a94 Merge remote-tracking branch 'origin/master' into feature/JENKINS-36211 2016-07-12 19:10:54 +02:00
Thorsten Scherler 585ea88e0d [feature/JENKINS-36211] Show nothing in case we do not have steps 2016-07-12 19:10:37 +02:00
Thorsten Scherler e50fb5c616 [feature/JENKINS-36211] remove all listener on unMount as spotted by Cliff 2016-07-12 03:45:01 +02:00
Thorsten Scherler 4757643650 Merge remote-tracking branch 'origin/master' into feature/JENKINS-36211 2016-07-11 19:33:15 +02:00
Thorsten Scherler 195ed2fa17 Merge remote-tracking branch 'origin/master' into feature/JENKINS-36211 2016-07-11 12:40:05 +02:00
Thorsten Scherler 6bfc400a20 eslint - formating changes and fix offences 2016-07-09 01:06:26 +02:00
Thorsten Scherler 7993c8f2e2 [feature/JENKINS-36211] remove legacy console log comments. Better comments on the code, so it is clear what we are doing 2016-07-09 00:45:16 +02:00
Thorsten Scherler 430fd3caaa Merge remote-tracking branch 'origin/master' into feature/JENKINS-36211 2016-07-09 00:00:16 +02:00
Thorsten Scherler 82db85e07d Merge remote-tracking branch 'origin/master' into feature/JENKINS-36211 2016-07-08 21:22:22 +02:00
Thorsten Scherler 85fdcf203c [feature/JENKINS-36211] remove console log statements finish up the logic of stepping back and forward in a running node 2016-07-08 21:15:39 +02:00
Thorsten Scherler 7d3e1188a7 [feature/JENKINS-36211] remove only 2016-07-08 21:14:29 +02:00
Thorsten Scherler fa1bfcc8ad [feature/JENKINS-36211] fix usage of test and make sure the original fetchJson is used 2016-07-08 21:14:15 +02:00
Thorsten Scherler 359253262b [feature/JENKINS-36211] WIP adding test for testing store for nodes, working on leaving logic 2016-07-05 13:38:34 +02:00
Thorsten Scherler a8fab9a2a4 Merge remote-tracking branch 'origin/master' into feature/JENKINS-36211 2016-07-04 11:19:44 +02:00
Thorsten Scherler 95da276c51 [feature/JENKINS-36211] WIP debugging leaving of karaoke mode 2016-07-04 11:19:01 +02:00
Thorsten Scherler 825c06b3bc Merge branch 'temp' into feature/JENKINS-36211 2016-06-30 01:32:21 +02:00
Thorsten Scherler 51ed61c95f Merge remote-tracking branch 'origin/master' into feature/JENKINS-36211 2016-06-30 01:26:48 +02:00
Thorsten Scherler cb3eef2400 [master] WIP tracking bug that refreshes RunDetails when it should not. Half way finished with unFollow. 2016-06-30 01:25:10 +02:00
Thorsten Scherler 2b54b4c03c Merge remote-tracking branch 'origin/master' into feature/JENKINS-36211 2016-06-29 14:40:11 +02:00
Thorsten Scherler 89b7aed2a9 [feature/JENKINS-36211] ignore - just testing 2016-06-29 14:39:54 +02:00
Thorsten Scherler 25c00598bf [feature/JENKINS-36211] ignore - just testing 2016-06-29 14:37:33 +02:00
25 changed files with 1073 additions and 189 deletions

View File

@ -1,8 +1,12 @@
Related to issue # .
**Decription**
Summary of this pull request:
.
.
.
**Submitter checklist**
- [ ] Change is code complete and matches issue description
- [ ] Apppropriate unit or acceptance tests or explaination to why this change has no tests
**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

View File

@ -2,6 +2,7 @@ import React, { Component, PropTypes } from 'react';
import { CommitHash, ReadableDate } from '@jenkins-cd/design-language';
import { LiveStatusIndicator, WeatherIcon } from '@jenkins-cd/design-language';
import RunPipeline from './RunPipeline.jsx';
import { buildRunDetailsUrl } from '../util/UrlUtils';
const { object } = PropTypes;
@ -22,6 +23,7 @@ export default class Branches extends Component {
location,
pipeline: {
name: pipelineName,
fullName,
organization,
},
},
@ -29,23 +31,26 @@ export default class Branches extends Component {
const {
latestRun: { id, result, startTime, endTime, changeSet, state, commitId, estimatedDurationInMillis },
weatherScore,
name,
name: branchName,
} = data;
const url = `/organizations/${organization}/${pipelineName}/detail/${name}/${id}/pipeline`;
const cleanBranchName = decodeURIComponent(branchName);
const url = buildRunDetailsUrl(organization, fullName, cleanBranchName, id, 'pipeline');
const open = () => {
location.pathname = url;
router.push(location);
};
const { msg } = changeSet[0] || {};
return (<tr key={name} onClick={open} id={`${name}-${id}`} >
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>{decodeURIComponent(name)}</td>
<td>{cleanBranchName}</td>
<td><CommitHash commitId={commitId} /></td>
<td>{msg || '-'}</td>
<td><ReadableDate date={endTime} liveUpdate /></td>

View File

@ -1,7 +1,7 @@
import React, { Component, PropTypes } from 'react';
import { PipelineGraph } from '@jenkins-cd/design-language';
const { string, array, object, any } = PropTypes;
const { string, array, any, func } = PropTypes;
function badNode(jenkinsNode) {
@ -149,22 +149,7 @@ export default class PipelineRunGraph extends Component {
stages={graphNodes}
onNodeClick={
(name, id) => {
const pathname = this.props.location.pathname;
// if path ends with pipeline we simply add the node id
if (pathname.endsWith('pipeline/')) {
this.props.router.push(`${pathname}${id}`);
} else if (pathname.endsWith('pipeline')) {
this.props.router.push(`${pathname}/${id}`);
} else {
// remove last bit and replace it with node
const pathArray = pathname.split('/');
pathArray.pop();
if (pathname.endsWith('/')) {
pathArray.pop();
}
pathArray.shift();
this.props.router.push(`${pathArray.join('/')}/${id}`);
}
this.props.callback(id);
}
}
/>
@ -179,6 +164,5 @@ PipelineRunGraph.propTypes = {
runId: string,
nodes: array,
node: any,
router: object.isRequired, // From react-router
location: object.isRequired, // From react-router
callback: func,
};

View File

@ -62,10 +62,15 @@ export default class Pipelines extends Component {
headers={headers}
>
{ pipelineRecords
.map(pipeline => <PipelineRowItem
key={pipeline.name} pipeline={pipeline}
showOrganization={!organization}
/>)
.map(pipeline => {
const key = pipeline._links.self.href;
return (
<PipelineRowItem
key={key} pipeline={pipeline}
showOrganization={!organization}
/>
);
})
}
</Table>
</article>

View File

@ -85,8 +85,11 @@ class RunDetails extends Component {
const baseUrl = buildRunDetailsUrl(organization, name, branch, runId);
const currentRun = this.props.runs.filter(
(run) => run.id === runId && decodeURIComponent(run.pipeline) === branch)[0];
/* eslint-disable arrow-body-style */
const currentRun = this.props.runs.filter((run) => {
return run.id === runId &&
decodeURIComponent(run.pipeline) === branch;
})[0];
currentRun.name = name;

View File

@ -1,8 +1,10 @@
import React, { Component, PropTypes } from 'react';
import ReactDOM from 'react-dom';
import Extensions from '@jenkins-cd/js-extensions';
import LogConsole from './LogConsole';
import * as sse from '@jenkins-cd/sse-gateway';
import LogToolbar from './LogToolbar';
import Steps from './Steps';
import {
steps as stepsSelector,
@ -15,89 +17,164 @@ import {
} from '../redux';
import { calculateStepsBaseUrl, calculateRunLogURLObject, calculateNodeBaseUrl } from '../util/UrlUtils';
import { calculateNode } from '../util/KaraokeHelper';
import LogToolbar from './LogToolbar';
const { string, object, any, func } = PropTypes;
export class RunDetailsPipeline extends Component {
constructor(props) {
super(props);
// we do not want to follow any builds that are finished
this.state = { followAlong: props && props.result && props.result.state !== 'FINISHED' };
this.listener = {};
}
componentWillMount() {
const { fetchNodes, fetchLog, result, fetchSteps } = this.props;
const mergedConfig = this.generateConfig(this.props);
this.mergedConfig = this.generateConfig(this.props);
const supportsNode = result && result._class === 'io.jenkins.blueocean.service.embedded.rest.PipelineRunImpl';
if (supportsNode) {
fetchNodes(mergedConfig);
fetchNodes(this.mergedConfig);
} else {
// console.log('fetch the log directly')
const logGeneral = calculateRunLogURLObject(mergedConfig);
const logGeneral = calculateRunLogURLObject(this.mergedConfig);
fetchLog({ ...logGeneral });
}
// Listen for pipeline flow node events.
// We filter them only for steps and the end event all other we let pass
this.pipelineListener = sse.subscribe('pipeline', (event) => {
const onSseEvent = (event) => {
const jenkinsEvent = event.jenkins_event;
// we turn on refetch so we always fetch a new Node result
const refetch = true;
switch (jenkinsEvent) {
case 'pipeline_step': {
// if the step_stage_id has changed we need to change the focus
if (event.pipeline_step_stage_id !== mergedConfig.node) {
mergedConfig.node = event.pipeline_step_stage_id;
fetchNodes({ ...mergedConfig, refetch });
} else {
fetchSteps({ ...mergedConfig, refetch });
// 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;
}
break;
}
case 'pipeline_end': {
fetchNodes({ ...mergedConfig, refetch });
break;
}
default: {
// console.log(event);
}
}
});
};
this.listener.sse = sse.subscribe('pipeline', 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);
}
componentWillReceiveProps(nextProps) {
if (nextProps.params.node !== this.props.params.node) {
const config = this.generateConfig(nextProps);
this.props.setNode(config);
this.props.fetchSteps(config);
}
const followAlong = this.state.followAlong;
this.mergedConfig = this.generateConfig({ ...nextProps, followAlong });
// we do not want any timeouts if we are not doing karaoke
if (!this.state.followAlong && this.timeout) {
clearTimeout(this.timeout);
}
// calculate if we need to trigger any actions to get into the right state (is plain js for testing reasons)
const nodeAction = calculateNode(this.props, nextProps, this.mergedConfig);
if (nodeAction && nodeAction.action) {
// use updated config
this.mergedConfig = nodeAction.config;
// we may need to stop following
if (this.state.followAlong !== nodeAction.state.followAlong) {
this.setState({ followAlong: nodeAction.state.followAlong });
}
// if we have actions we fire them
this.props[nodeAction.action](this.mergedConfig);
}
// if we only interested in logs (in case of e.g. freestyle)
const { logs, fetchLog } = nextProps;
if (logs !== this.props.logs) {
const mergedConfig = this.generateConfig(nextProps);
const logGeneral = calculateRunLogURLObject(mergedConfig);
const logGeneral = calculateRunLogURLObject(this.mergedConfig);
const log = logs ? logs[logGeneral.url] : null;
if (log && log !== null) {
// we may have a streaming log
const newStart = log.newStart;
if (Number(newStart) > 0) {
// kill current timeout if any
clearTimeout(this.timeout);
this.timeout = setTimeout(() => fetchLog({ ...logGeneral, newStart }), 1000);
// in case we doing karaoke we want to see more logs
if (this.state.followAlong) {
// kill current timeout if any
clearTimeout(this.timeout);
// we need to get mpre input from the log stream
this.timeout = setTimeout(() => fetchLog({ ...logGeneral, newStart }), 1000);
}
}
}
}
}
componentWillUnmount() {
if (this.pipelineListener) {
sse.unsubscribe(this.pipelineListener);
delete this.pipelineListener;
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;
}
this.props.cleanNodePointer();
clearTimeout(this.timeout);
}
// need to register handler to step out of karaoke mode
// we bail out on scroll up
onScrollHandler(elem) {
if (elem.deltaY < 0 && this.state.followAlong) {
this.setState({ followAlong: false });
}
}
// we bail out on arrow_up key
_handleKeys(event) {
if (event.keyCode === 38 && this.state.followAlong) {
this.setState({ followAlong: false });
}
}
generateConfig(props) {
const {
config = {},
} = this.context;
const followAlong = this.state.followAlong;
const {
isMultiBranch,
params: { pipeline: name, branch, runId, node: nodeParam },
@ -109,7 +186,8 @@ export class RunDetailsPipeline extends Component {
}
// if we have a node param we do not want the calculation of the focused node
const node = nodeParam || nodeReducer.id;
const mergedConfig = { ...config, name, branch, runId, isMultiBranch, node, nodeReducer };
const mergedConfig = { ...config, name, branch, runId, isMultiBranch, node, nodeReducer, followAlong };
return mergedConfig;
}
@ -127,43 +205,84 @@ export class RunDetailsPipeline extends Component {
} = this.props;
const {
result,
state,
result,
state,
} = resultMeta;
const resultRun = result === 'UNKNOWN' || !result ? state : result;
const scrollToBottom = resultRun.toLowerCase() === 'failure' || resultRun.toLowerCase() === 'running';
const followAlong = this.state.followAlong;
// in certain cases we want that the log component will scroll to the end of a log
const scrollToBottom =
resultRun.toLowerCase() === 'failure'
|| (resultRun.toLowerCase() === 'running' && followAlong)
;
const mergedConfig = this.generateConfig(this.props);
const nodeKey = calculateNodeBaseUrl(mergedConfig);
const key = calculateStepsBaseUrl(mergedConfig);
const logGeneral = calculateRunLogURLObject(mergedConfig);
const nodeKey = calculateNodeBaseUrl(this.mergedConfig);
const key = calculateStepsBaseUrl(this.mergedConfig);
const logGeneral = calculateRunLogURLObject(this.mergedConfig);
const log = logs ? logs[logGeneral.url] : null;
let title = mergedConfig.nodeReducer.displayName;
let title = this.mergedConfig.nodeReducer.displayName;
if (log) {
title = 'Logs';
} else if (mergedConfig.nodeReducer.id !== null) {
} else if (this.mergedConfig.nodeReducer.id !== null && title) {
title = `Steps - ${title}`;
}
const currentSteps = steps ? steps[key] : null;
// here we decide what to do next if somebody clicks on a flowNode
const afterClick = (id) => {
// get some information about the node the user clicked
const nodeInfo = nodes[nodeKey].model.filter((item) => item.id === id)[0];
const pathname = location.pathname;
let newPath;
// if path ends with pipeline we simply use it
if (pathname.endsWith('pipeline/')) {
newPath = pathname;
} else if (pathname.endsWith('pipeline')) {
newPath = `${pathname}/`;
} else {
// remove last bits
const pathArray = pathname.split('/');
pathArray.pop();
if (pathname.endsWith('/')) {
pathArray.pop();
}
pathArray.shift();
newPath = `${pathArray.join('/')}/`;
}
// we only want to redirect to the node if the node is finished
if (nodeInfo.state === 'FINISHED') {
newPath = `${newPath}${id}`;
}
// see whether we need to update the state
if (nodeInfo.state === 'FINISHED' && followAlong) {
this.setState({ followAlong: false });
}
if (nodeInfo.state !== 'FINISHED' && !followAlong) {
this.setState({ followAlong: true });
}
router.push(newPath);
};
const shouldShowLogHeader = log !== null || (currentSteps && currentSteps.model && currentSteps.model.length > 0);
return (
<div>
<div ref="scrollArea">
{ nodes && nodes[nodeKey] && <Extensions.Renderer
extensionPoint="jenkins.pipeline.run.result"
router={router}
location={location}
callback={afterClick}
nodes={nodes[nodeKey].model}
pipelineName={name}
branchName={isMultiBranch ? branch : undefined}
runId={runId}
/>
}
<LogToolbar
fileName={logGeneral.fileName}
url={logGeneral.url}
title={title}
/>
{ steps && steps[key] && <Steps
nodeInformation={steps[key]}
{ shouldShowLogHeader &&
<LogToolbar
fileName={logGeneral.fileName}
url={logGeneral.url}
title={title}
/>
}
{ currentSteps && <Steps
nodeInformation={currentSteps}
followAlong={followAlong}
{...this.props}
/>
}

View File

@ -4,7 +4,7 @@ import { calculateLogUrl } from '../util/UrlUtils';
import LogConsole from './LogConsole';
const { object, func, string } = PropTypes;
const { object, func, string, bool } = PropTypes;
export default class Node extends Component {
componentWillMount() {
@ -17,30 +17,38 @@ export default class Node extends Component {
}
componentWillReceiveProps(nextProps) {
const { node, logs, nodesBaseUrl, fetchLog } = nextProps;
const { node, logs, nodesBaseUrl, fetchLog, followAlong } = nextProps;
const { config = {} } = this.context;
const mergedConfig = { ...config, node, nodesBaseUrl };
if (logs !== this.props.logs) {
if (logs && logs !== this.props.logs) {
const key = calculateLogUrl(mergedConfig);
const log = logs ? logs[key] : null;
if (log && log !== null) {
// we may have a streaming log
const number = Number(log.newStart);
if (number > 0) {
// in case we doing karaoke we want to see more logs
if (number > 0 && followAlong) {
mergedConfig.newStart = log.newStart;
// kill current timeout if any
clearTimeout(this.timeout);
this.timeout = setTimeout(() => fetchLog(mergedConfig), 1000);
this.clearThisTimeout();
this.timeout = setTimeout(() => fetchLog({ ...mergedConfig }), 1000);
}
}
}
}
componentWillUnmount() {
clearTimeout(this.timeout);
this.clearThisTimeout();
}
clearThisTimeout() {
if (this.timeout) {
clearTimeout(this.timeout);
}
}
render() {
const { node, logs, nodesBaseUrl, fetchLog } = this.props;
const { node, logs, nodesBaseUrl, fetchLog, followAlong } = this.props;
// Early out
if (!node || !fetchLog) {
return null;
@ -58,13 +66,16 @@ export default class Node extends Component {
const resultRun = result === 'UNKNOWN' || !result ? state : result;
const log = logs ? logs[calculateLogUrl({ ...config, node, nodesBaseUrl })] : null;
const getLogForNode = () => {
if (!log) {
// in case we do not have logs, or the logs are have no information attached we refetch them
if (!log || !log.logArray) {
fetchLog({ ...config, node, nodesBaseUrl });
}
};
const runResult = resultRun.toLowerCase();
const scrollToBottom = runResult === 'failure' || runResult === 'running';
const scrollToBottom =
resultRun.toLowerCase() === 'failure'
|| (resultRun.toLowerCase() === 'running' && followAlong)
;
return (<div>
<ResultItem
key={id}
@ -82,6 +93,7 @@ export default class Node extends Component {
Node.propTypes = {
node: object.isRequired,
followAlong: bool,
logs: object,
fetchLog: func,
nodesBaseUrl: string,

View File

@ -12,7 +12,6 @@ export default class Nodes extends Component {
model,
nodesBaseUrl,
} = nodeInformation;
return (<div>
{
model.map((item, index) =>

View File

@ -5,6 +5,7 @@ since we would return a function, */
const { Record } = Immutable;
export class PipelineRecord extends Record({
_class: null,
_links: null,
branchNames: null,
displayName: '',
estimatedDurationInMillis: 0,

View File

@ -61,7 +61,7 @@ export const actionHandlers = {
return state.set('currentRuns', payload);
},
[ACTION_TYPES.SET_NODE](state, { payload }): State {
return state.set('node', payload);
return state.set('node', { ...payload });
},
[ACTION_TYPES.SET_NODES](state, { payload }): State {
const nodes = { ...state.nodes } || {};
@ -157,7 +157,7 @@ function parseMoreDataHeader(response) {
* @param onError
*/
exports.fetchJson = function fetchJson(url, onSuccess, onError) {
fetch(url, fetchOptions)
return fetch(url, fetchOptions)
.then(checkStatus)
.then(parseJSON)
.then(onSuccess)
@ -188,7 +188,7 @@ exports.fetchLogsInjectStart = function fetchJson(url, start, onSuccess, onError
} else {
refetchUrl = `${url}?start=${start}`;
}
fetch(refetchUrl, fetchOptions)
return fetch(refetchUrl, fetchOptions)
.then(checkStatus)
.then(parseMoreDataHeader)
.then(onSuccess)
@ -684,6 +684,7 @@ export const actions = {
nodeModel = information.model.filter((item) => item.id === config.node)[0];
node = config.node;
}
dispatch({
type: ACTION_TYPES.SET_NODE,
payload: nodeModel,
@ -770,7 +771,7 @@ export const actions = {
if (
!data || !data[logUrl] ||
config.newStart > 0 ||
(data && data[logUrl] && data[logUrl].newStart > 0)
(data && data[logUrl] && data[logUrl].newStart > 0 || !data[logUrl].logArray)
) {
return exports.fetchLogsInjectStart(
logUrl,

View File

@ -0,0 +1,21 @@
/*
* Calculate whether to fetch a node
* @param props
* @param nextProps
* @param mergedConfig
*/
export function calculateNode(props, nextProps, mergedConfig) {
const refetch = nextProps.result.state === 'RUNNING';
// case the param is different from the one we currently in
if (nextProps.params.node !== props.params.node) {
// clone config
const clonedConfig = { ...mergedConfig };
clonedConfig.node = nextProps.params.node;
const answer = { state: { followAlong: mergedConfig.followAlong } };
answer.config = { ...clonedConfig, refetch };
answer.action = 'fetchNodes';
return answer;
}
return null;
}

View File

@ -47,7 +47,7 @@ export const uriString = (input) => encodeURIComponent(input).replace(/%2F/g, '%
export const calculateLogUrl = (config) => {
if (config.node) {
const { nodesBaseUrl, node } = config;
return `${nodesBaseUrl}/${node.id}/log`;
return `${nodesBaseUrl}/${node.id}/log/`;
}
return config.url;
};
@ -80,14 +80,13 @@ export function calculateStepsBaseUrl(config) {
`${_appURLBase}/rest/organizations/jenkins/` +
`pipelines/${name}`;
if (isMultiBranch) {
baseUrl = `${baseUrl}/branches/${branch}`;
baseUrl = `${baseUrl}/branches/${uriString(branch)}`;
}
if (node && node !== null) {
return `${baseUrl}/runs/${runId}/nodes/${node}/steps`;
return `${baseUrl}/runs/${runId}/nodes/${node}/steps/`;
}
return `${baseUrl}/runs/${runId}/steps/`;
}
/*
* helper to calculate general log url, includes filename.
* If we have multibranch we generate a slightly different url

View File

@ -0,0 +1,25 @@
import { assert } from 'chai';
import {calculateNode } from '../../main/js/util/KaraokeHelper';
describe('KaraokeHelper', () => {
describe('KaraokeHelper calculateNode', () => {
const props = { params: {} };
const nextProps = { params: { node: 21}, result:{} };
const mergedConfig = {
node: 32,
nodeReducer: {
id:32,
},
};
it('should return an answer if our node param is different (case if some one clicks a flownode)', () => {
const answer = calculateNode(props, nextProps, mergedConfig);
assert.notEqual(answer, null);
});
});
describe('calculateNode real life', () => {
})
});

View File

@ -98,7 +98,7 @@ describe('UrlUtils', () => {
}
);
assert.equal(url, `${testUrl}/1/log`);
assert.equal(url, `${testUrl}/1/log/`);
});
});
describe('calculate calculateNodeBaseUrl', () => {
@ -133,7 +133,7 @@ describe('UrlUtils', () => {
const node = 15;
const url = calculateStepsBaseUrl({...testData, node});
assert.equal(url, `${testData._appURLBase}/rest/organizations/jenkins/` +
`pipelines/${testData.name}/runs/${testData.runId}/nodes/${node}/steps`);
`pipelines/${testData.name}/runs/${testData.runId}/nodes/${node}/steps/`);
});
it('should build the url with mutibranch and no node', () => {
const isMultiBranch = true;

View File

@ -0,0 +1,34 @@
/* eslint-disable quotes,quote-props,comma-dangle */
export const pipelines = [
{
'_links': {
'self': {
'_class': 'io.jenkins.blueocean.rest.hal.Link',
'href': '/blue/rest/organizations/jenkins/pipelines/morebeers/'
},
},
'displayName': 'moreBeers',
'name': 'morebeers',
'organization': 'jenkins',
'weatherScore': 0,
'branchNames': ['master'],
'numberOfFailingBranches': 1,
'numberOfFailingPullRequests': 0,
'numberOfSuccessfulBranches': 0,
'numberOfSuccessfulPullRequests': 0,
'totalNumberOfBranches': 1,
'totalNumberOfPullRequests': 0
},
{
'_links': {
'self': {
'_class': 'io.jenkins.blueocean.rest.hal.Link',
'href': '/blue/rest/organizations/jenkins/pipelines/beers/'
},
},
'displayName': 'beers',
'name': 'beers',
'organization': 'jenkins',
'weatherScore': 0
}
];

View File

@ -0,0 +1,85 @@
/* eslint-disable quotes,quote-props,comma-dangle */
export const pipelinesDupName = [
{
"_class": "io.jenkins.blueocean.service.embedded.rest.MultiBranchPipelineImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/blueocean-pr-testing/"
},
"branches": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/blueocean-pr-testing/branches/"
},
"actions": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/blueocean-pr-testing/actions/"
},
"runs": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/blueocean-pr-testing/runs/"
},
"queue": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/blueocean-pr-testing/queue/"
}
},
"actions": [],
"displayName": "blueocean-pr-testing",
"fullName": "blueocean-pr-testing",
"name": "blueocean-pr-testing",
"organization": "jenkins",
"estimatedDurationInMillis": 1152,
"numberOfFolders": 0,
"numberOfPipelines": 8,
"weatherScore": 100,
"branchNames": ["develop", "master", "cliff-60s", "feature%2Fxxx", "feature%2Fcliff-2", "cliff-120s", "feature%2Fcliff-1", "feature%2Fyyy"],
"numberOfFailingBranches": 0,
"numberOfFailingPullRequests": 0,
"numberOfSuccessfulBranches": 8,
"numberOfSuccessfulPullRequests": 0,
"totalNumberOfBranches": 8,
"totalNumberOfPullRequests": 0
},
{
"_class": "io.jenkins.blueocean.service.embedded.rest.MultiBranchPipelineImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/folder1/pipelines/blueocean-pr-testing/"
},
"branches": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/folder1/pipelines/blueocean-pr-testing/branches/"
},
"actions": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/folder1/pipelines/blueocean-pr-testing/actions/"
},
"runs": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/folder1/pipelines/blueocean-pr-testing/runs/"
},
"queue": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/folder1/pipelines/blueocean-pr-testing/queue/"
}
},
"actions": [],
"displayName": "blueocean-pr-testing",
"fullName": "folder1/blueocean-pr-testing",
"name": "blueocean-pr-testing",
"organization": "jenkins",
"estimatedDurationInMillis": 1206,
"numberOfFolders": 0,
"numberOfPipelines": 8,
"weatherScore": 100,
"branchNames": ["feature%2Fcliff-1", "develop", "feature%2Fyyy", "feature%2Fcliff-2", "feature%2Fxxx", "cliff-120s", "cliff-60s", "master"],
"numberOfFailingBranches": 0,
"numberOfFailingPullRequests": 0,
"numberOfSuccessfulBranches": 8,
"numberOfSuccessfulPullRequests": 0,
"totalNumberOfBranches": 8,
"totalNumberOfPullRequests": 0
}
];

View File

@ -0,0 +1,424 @@
export const nodes = [{
"_class": "io.jenkins.blueocean.service.embedded.rest.PipelineNodeImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/steps/runs/16/nodes/5/"
},
"steps": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/steps/runs/16/nodes/5/steps/"
}
},
"displayName": "deploy",
"durationInMillis": 30779,
"id": "5",
"result": "SUCCESS",
"startTime": "2016-07-01T14:02:38.829+0200",
"state": "FINISHED",
"edges": [{"_class": "io.jenkins.blueocean.service.embedded.rest.PipelineNodeImpl$EdgeImpl", "id": "23"}]
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.PipelineNodeImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/steps/runs/16/nodes/23/"
},
"steps": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/steps/runs/16/nodes/23/steps/"
}
},
"displayName": "testing",
"durationInMillis": 20235,
"id": "23",
"result": "SUCCESS",
"startTime": "2016-07-01T14:03:09.608+0200",
"state": "FINISHED",
"edges": [{
"_class": "io.jenkins.blueocean.service.embedded.rest.PipelineNodeImpl$EdgeImpl",
"id": "26"
}, {"_class": "io.jenkins.blueocean.service.embedded.rest.PipelineNodeImpl$EdgeImpl", "id": "27"}]
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.PipelineNodeImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/steps/runs/16/nodes/26/"
},
"steps": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/steps/runs/16/nodes/26/steps/"
}
},
"displayName": "firstBranch",
"durationInMillis": 20176,
"id": "26",
"result": "SUCCESS",
"startTime": "2016-07-01T14:03:09.609+0200",
"state": "FINISHED",
"edges": [{"_class": "io.jenkins.blueocean.service.embedded.rest.PipelineNodeImpl$EdgeImpl", "id": "45"}]
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.PipelineNodeImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/steps/runs/16/nodes/27/"
},
"steps": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/steps/runs/16/nodes/27/steps/"
}
},
"displayName": "secondBranch",
"durationInMillis": 15652,
"id": "27",
"result": "SUCCESS",
"startTime": "2016-07-01T14:03:09.609+0200",
"state": "FINISHED",
"edges": [{"_class": "io.jenkins.blueocean.service.embedded.rest.PipelineNodeImpl$EdgeImpl", "id": "45"}]
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.PipelineNodeImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/steps/runs/16/nodes/45/"
},
"steps": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/steps/runs/16/nodes/45/steps/"
}
},
"displayName": "fin",
"durationInMillis": 26951,
"id": "45",
"result": "SUCCESS",
"startTime": "2016-07-01T14:03:29.843+0200",
"state": "FINISHED",
"edges": []
}];
export const stepsNode5 = [{
"_class": "io.jenkins.blueocean.service.embedded.rest.PipelineStepImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/steps/runs/16/nodes/5/steps/6/"
}
},
"displayName": "Shell Script",
"durationInMillis": 4389,
"id": "6",
"result": "SUCCESS",
"startTime": "2016-07-01T14:02:38.830+0200",
"state": "FINISHED"
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.PipelineStepImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/steps/runs/16/nodes/5/steps/7/"
}
},
"displayName": "Print Message",
"durationInMillis": 2,
"id": "7",
"result": "SUCCESS",
"startTime": "2016-07-01T14:02:43.219+0200",
"state": "FINISHED"
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.PipelineStepImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/steps/runs/16/nodes/5/steps/8/"
}
},
"displayName": "Shell Script",
"durationInMillis": 4390,
"id": "8",
"result": "SUCCESS",
"startTime": "2016-07-01T14:02:43.221+0200",
"state": "FINISHED"
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.PipelineStepImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/steps/runs/16/nodes/5/steps/9/"
}
},
"displayName": "Print Message",
"durationInMillis": 1,
"id": "9",
"result": "SUCCESS",
"startTime": "2016-07-01T14:02:47.611+0200",
"state": "FINISHED"
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.PipelineStepImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/steps/runs/16/nodes/5/steps/10/"
}
},
"displayName": "Shell Script",
"durationInMillis": 4391,
"id": "10",
"result": "SUCCESS",
"startTime": "2016-07-01T14:02:47.612+0200",
"state": "FINISHED"
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.PipelineStepImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/steps/runs/16/nodes/5/steps/11/"
}
},
"displayName": "Print Message",
"durationInMillis": 2,
"id": "11",
"result": "SUCCESS",
"startTime": "2016-07-01T14:02:52.003+0200",
"state": "FINISHED"
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.PipelineStepImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/steps/runs/16/nodes/5/steps/12/"
}
},
"displayName": "Shell Script",
"durationInMillis": 4389,
"id": "12",
"result": "SUCCESS",
"startTime": "2016-07-01T14:02:52.005+0200",
"state": "FINISHED"
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.PipelineStepImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/steps/runs/16/nodes/5/steps/13/"
}
},
"displayName": "Print Message",
"durationInMillis": 2,
"id": "13",
"result": "SUCCESS",
"startTime": "2016-07-01T14:02:56.394+0200",
"state": "FINISHED"
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.PipelineStepImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/steps/runs/16/nodes/5/steps/14/"
}
},
"displayName": "Shell Script",
"durationInMillis": 4386,
"id": "14",
"result": "SUCCESS",
"startTime": "2016-07-01T14:02:56.396+0200",
"state": "FINISHED"
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.PipelineStepImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/steps/runs/16/nodes/5/steps/15/"
}
},
"displayName": "Print Message",
"durationInMillis": 2,
"id": "15",
"result": "SUCCESS",
"startTime": "2016-07-01T14:03:00.782+0200",
"state": "FINISHED"
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.PipelineStepImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/steps/runs/16/nodes/5/steps/16/"
}
},
"displayName": "Shell Script",
"durationInMillis": 4384,
"id": "16",
"result": "SUCCESS",
"startTime": "2016-07-01T14:03:00.784+0200",
"state": "FINISHED"
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.PipelineStepImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/steps/runs/16/nodes/5/steps/17/"
}
},
"displayName": "Print Message",
"durationInMillis": 1,
"id": "17",
"result": "SUCCESS",
"startTime": "2016-07-01T14:03:05.168+0200",
"state": "FINISHED"
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.PipelineStepImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/steps/runs/16/nodes/5/steps/18/"
}
},
"displayName": "Shell Script",
"durationInMillis": 4413,
"id": "18",
"result": "SUCCESS",
"startTime": "2016-07-01T14:03:05.169+0200",
"state": "FINISHED"
}];
export const stepsNode45 = [{
"_class": "io.jenkins.blueocean.service.embedded.rest.PipelineStepImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/steps/runs/16/nodes/45/steps/46/"
}
},
"displayName": "Shell Script",
"durationInMillis": 6727,
"id": "46",
"result": "SUCCESS",
"startTime": "2016-07-01T14:03:29.844+0200",
"state": "FINISHED"
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.PipelineStepImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/steps/runs/16/nodes/45/steps/47/"
}
},
"displayName": "Print Message",
"durationInMillis": 1,
"id": "47",
"result": "SUCCESS",
"startTime": "2016-07-01T14:03:36.571+0200",
"state": "FINISHED"
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.PipelineStepImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/steps/runs/16/nodes/45/steps/48/"
}
},
"displayName": "Shell Script",
"durationInMillis": 6726,
"id": "48",
"result": "SUCCESS",
"startTime": "2016-07-01T14:03:36.572+0200",
"state": "FINISHED"
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.PipelineStepImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/steps/runs/16/nodes/45/steps/49/"
}
},
"displayName": "Print Message",
"durationInMillis": 1,
"id": "49",
"result": "SUCCESS",
"startTime": "2016-07-01T14:03:43.298+0200",
"state": "FINISHED"
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.PipelineStepImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/steps/runs/16/nodes/45/steps/50/"
}
},
"displayName": "Print Message",
"durationInMillis": 1,
"id": "50",
"result": "SUCCESS",
"startTime": "2016-07-01T14:03:43.299+0200",
"state": "FINISHED"
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.PipelineStepImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/steps/runs/16/nodes/45/steps/51/"
}
},
"displayName": "Shell Script",
"durationInMillis": 6737,
"id": "51",
"result": "SUCCESS",
"startTime": "2016-07-01T14:03:43.300+0200",
"state": "FINISHED"
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.PipelineStepImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/steps/runs/16/nodes/45/steps/52/"
}
},
"displayName": "Print Message",
"durationInMillis": 0,
"id": "52",
"result": "SUCCESS",
"startTime": "2016-07-01T14:03:50.037+0200",
"state": "FINISHED"
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.PipelineStepImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/steps/runs/16/nodes/45/steps/53/"
}
},
"displayName": "Print Message",
"durationInMillis": 1,
"id": "53",
"result": "SUCCESS",
"startTime": "2016-07-01T14:03:50.037+0200",
"state": "FINISHED"
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.PipelineStepImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/steps/runs/16/nodes/45/steps/54/"
}
},
"displayName": "Print Message",
"durationInMillis": 1,
"id": "54",
"result": "SUCCESS",
"startTime": "2016-07-01T14:03:50.038+0200",
"state": "FINISHED"
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.PipelineStepImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/steps/runs/16/nodes/45/steps/55/"
}
},
"displayName": "Shell Script",
"durationInMillis": 6749,
"id": "55",
"result": "SUCCESS",
"startTime": "2016-07-01T14:03:50.039+0200",
"state": "FINISHED"
}];

View File

@ -1,43 +1,65 @@
import { prepareMount } from './util/EnzymeUtils';
prepareMount();
import React from 'react';
import { assert} from 'chai';
import sd from 'skin-deep';
import Immutable from 'immutable';
import { assert } from 'chai';
import { mount, shallow } from 'enzyme';
import Pipelines from '../../main/js/components/Pipelines.jsx';
import { pipelines } from './pipelines';
import { pipelines } from './data/pipelines/pipelinesSingle';
import { pipelinesDupName } from './data/pipelines/pipelinesTwoJobsSameName';
const
resultArrayHeaders = ['Name', 'Status', 'Branches', 'Pull Requests', '']
;
describe("pipelines", () => {
let tree;
const resultArrayHeaders = ['Name', 'Status', 'Branches', 'Pull Requests', ''];
describe('Pipelines', () => {
const config = {
getRootURL: () => '/',
};
const params = {};
beforeEach(() => {
tree = sd.shallowRender(
() => React.createElement(Pipelines), // For some reason using a fn turns on context
{
pipelines: Immutable.fromJS(pipelines),
describe('basic table rendering', () => {
let wrapper;
let context;
beforeEach(() => {
context = {
pipelines,
params,
config,
}
);
};
wrapper = shallow(
<Pipelines />,
{
context,
}
);
});
it('check header to be as expected', () => {
assert.equal(wrapper.find('Table').props().headers.length, resultArrayHeaders.length);
});
it('check rows number to be as expected', () => {
assert.equal(wrapper.find('PipelineRowItem').length, 2);
});
});
it("renders pipelines - check header to be as expected", () => {
const header = tree.subTree('Table').getRenderOutput();
assert.equal(header.props.headers.length, resultArrayHeaders.length);
});
describe('duplicate job names', () => {
it('should render two rows when job names are duplicated across folders', () => {
const context = {
config,
params,
pipelines: pipelinesDupName,
};
it("renders pipelines - check rows number to be as expected", () => {
const row = tree.everySubTree('PipelineRowItem');
assert.equal(row.length, 2);
});
const wrapper = mount(
<Pipelines />,
{ context },
);
assert.equal(wrapper.find('PipelineRowItem').length, 2);
});
});
});

View File

@ -1,18 +0,0 @@
export const pipelines = [{
'displayName': 'moreBeers',
'name': 'morebeers',
'organization': 'jenkins',
'weatherScore': 0,
'branchNames': ['master'],
'numberOfFailingBranches': 1,
'numberOfFailingPullRequests': 0,
'numberOfSuccessfulBranches': 0,
'numberOfSuccessfulPullRequests': 0,
'totalNumberOfBranches': 1,
'totalNumberOfPullRequests': 0
}, {
'displayName': 'beers',
'name': 'beers',
'organization': 'jenkins',
'weatherScore': 0
}];

View File

@ -259,4 +259,4 @@ describe("push events - started run tests", () => {
assert.equal(runs[0].enQueueTime, undefined);
assert.equal(runs[0].state, 'RUNNING');
});
});
});

View File

@ -0,0 +1,119 @@
import React from 'react';
import {assert} from 'chai';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import nock from 'nock';
import {getNodesInformation} from './../../main/js/util/logDisplayHelper';
import {
actions,
} from '../../main/js/redux';
import {nodes, stepsNode45} from './nodes';
import {
calculateRunLogURLObject, calculateStepsBaseUrl, calculateNodeBaseUrl,
} from '../../main/js/util/UrlUtils';
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
describe("Store should work", () => {
describe("Log Store should work", () => {
afterEach(() => {
nock.cleanAll()
});
it("create store with run data", () => {
const name = 'testName',
runId = 4,
branch = 'testing',
_appURLBase = '',
isMultiBranch = false;
const mergedConfig = {name, runId, branch, _appURLBase, isMultiBranch};
const logGeneral = calculateRunLogURLObject(mergedConfig);
nock('http://example.com')
.get(logGeneral.url)
.reply(200, `Hello World 2 parallel
[workspace] Running shell script
+ date
Tue May 24 13:42:18 CEST 2016
+ sleep 20
+ date
Tue May 24 13:42:38 CEST 2016
`);
const store = mockStore({adminStore: {logs: {}}});
logGeneral.url = `http://example.com${logGeneral.url}`;
return store.dispatch(
actions.fetchLog({...logGeneral}))
.then(() => { // return of async actions
assert.equal(store.getActions()[0].type, 'SET_LOGS');
assert.equal(store.getActions()[0].payload.logUrl, logGeneral.url);
});
});
});
describe("Store should work with steps", () => {
afterEach(() => {
nock.cleanAll()
});
it("create store with step data", () => {
const baseUrl = 'http://127.0.0.1';
const name = 'steps',
runId = 16,
branch = 'testing',
_appURLBase = '',
isMultiBranch = false;
const mergedConfig = {name, runId, branch, _appURLBase, isMultiBranch};
const node = 45;
const stepsUrl = calculateStepsBaseUrl({...mergedConfig, node});
const stepsNock = nock(baseUrl)
.get(stepsUrl)
.reply(200, stepsNode45)
;
const store = mockStore({adminStore: {}});
mergedConfig._appURLBase = `${baseUrl}:80`;
store.dispatch(
actions.fetchSteps({...mergedConfig, node}))
.then(() => { // return of async actions
assert.equal(store.getActions()[0].type, 'SET_STEPS');
assert.equal(store.getActions()[0].payload.isFinished, true);
assert.equal(store.getActions()[0].payload.isError, false);
assert.equal(store.getActions()[0].payload.nodesBaseUrl, `${baseUrl}${stepsUrl}`);
assert.equal(store.getActions()[0].payload.model.length, 10);
});
assert.equal(stepsNock.isDone(), true);
});
});
describe("Store should work with nodes", () => {
afterEach(() => {
nock.cleanAll()
});
it("create store with node data", () => {
const baseUrl = 'http://127.0.0.1';
const name = 'steps',
runId = 16,
branch = 'testing',
_appURLBase = '',
isMultiBranch = false;
const mergedConfig = {name, runId, branch, _appURLBase, isMultiBranch};
const nodesBaseUrl = calculateNodeBaseUrl(mergedConfig);
const node = 45;
const stepsUrl = calculateStepsBaseUrl({...mergedConfig, node});
const nodeNock = nock(baseUrl)
.get(nodesBaseUrl)
.reply(200, nodes)
;
mergedConfig._appURLBase = `${baseUrl}:80`;
const steps = {};
steps[`${baseUrl}:80${stepsUrl}`] = getNodesInformation(stepsNode45);
const otherStore = mockStore({adminStore: {steps}});
otherStore.dispatch(actions.fetchNodes(mergedConfig));
assert.equal(nodeNock.isDone(), true);
});
});
});

View File

@ -10,7 +10,7 @@ import {
pipelines as pipelinesSelector,
currentRuns as currentRunsSelector,
} from '../../main/js/redux';
import { pipelines } from './pipelines';
import { pipelines } from './data/pipelines/pipelinesSingle';
import { latestRuns } from './data/runs/latestRuns';
import job_crud_created_multibranch from './data/sse/job_crud_created_multibranch';
import fetchedBranches from './data/branches/latestBranches';

View File

@ -0,0 +1,36 @@
import { store as ExtensionStore } from '@jenkins-cd/js-extensions';
const jsdom = require('jsdom').jsdom;
/**
* Prepares Enzyme's "mount" function for use by binding it to JSDOM.
* Also takes care of a little bootstrapping of @js-extensions/ExtensionStore to avoid errors.
* Place this snippet at the very top of your test file, before importing React: *
* import { prepareMount } from './EnzymeUtils';
* prepareMount();
*
* Created by cmeyers on 7/12/16.
*/
export const prepareMount = () => {
// code to boostrap mount with JSDOM
// see: https://github.com/airbnb/enzyme/blob/master/docs/guides/jsdom.md
const exposedProperties = ['window', 'navigator', 'document'];
global.document = jsdom('');
global.window = document.defaultView;
Object.keys(document.defaultView).forEach((property) => {
if (typeof global[property] === 'undefined') {
exposedProperties.push(property);
global[property] = document.defaultView[property];
}
});
global.navigator = {
userAgent: 'node.js',
};
// Extensions.Renderer will fail during Enzyme.mount without an
// 'extensionDataProvider' function being provided
// FIXME: there's probably a much cleaner way to do this.
ExtensionStore.init({
extensionDataProvider: () => undefined,
});
};

View File

@ -27,6 +27,7 @@ import hudson.Extension;
import hudson.model.Item;
import hudson.model.ItemGroup;
import io.jenkins.blueocean.rest.hal.Link;
import io.jenkins.blueocean.rest.hal.LinkResolver;
import io.jenkins.blueocean.service.embedded.rest.OrganizationImpl;
import jenkins.model.ParameterizedJobMixIn;
import org.jenkins.pubsub.EventProps;
@ -38,31 +39,34 @@ import org.jenkinsci.plugins.workflow.job.WorkflowJob;
import org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject;
import javax.annotation.Nonnull;
import javax.inject.Inject;
/**
* @author <a href="mailto:tom.fennelly@gmail.com">tom.fennelly@gmail.com</a>
*/
@Extension
public class BlueMessageEnricher extends MessageEnricher {
@Inject
private LinkResolver linkResolver;
enum BlueEventProps {
blueocean_job_rest_url,
blueocean_job_pipeline_name,
blueocean_job_branch_name,
}
@Override
public void enrich(@Nonnull Message message) {
// TODO: Replace once https://issues.jenkins-ci.org/browse/JENKINS-36286 is done
// TODO: Get organization name in generic way once multi-organization support is implemented in API
message.set(EventProps.Jenkins.jenkins_org, OrganizationImpl.INSTANCE.getName());
String channelName = message.getChannelName();
if (channelName.equals(Events.JobChannel.NAME)) {
JobChannelMessage jobChannelMessage = (JobChannelMessage) message;
ParameterizedJobMixIn.ParameterizedJob job = jobChannelMessage.getJob();
Link jobUrl = getLink(job);
Link jobUrl = linkResolver.resolve(job);
jobChannelMessage.set(BlueEventProps.blueocean_job_rest_url, jobUrl.getHref());
jobChannelMessage.set(BlueEventProps.blueocean_job_pipeline_name, job.getName());
if (job instanceof WorkflowJob) {
@ -76,19 +80,4 @@ public class BlueMessageEnricher extends MessageEnricher {
}
}
}
// TODO: Replace once https://issues.jenkins-ci.org/browse/JENKINS-36286 is done
private static @Nonnull Link getLink(@Nonnull ParameterizedJobMixIn.ParameterizedJob job) {
Link orgLink = new Link("/rest/organizations/" + OrganizationImpl.INSTANCE.getName());
if (job instanceof WorkflowJob) {
ItemGroup<? extends Item> parent = job.getParent();
if (parent instanceof WorkflowMultiBranchProject) {
String multiBranchProjectName = ((WorkflowMultiBranchProject) parent).getName();
return orgLink.rel("pipelines").rel(multiBranchProjectName).rel("branches").rel(job.getName());
}
}
return orgLink.rel("pipelines").rel(job.getName());
}
}

View File

@ -133,24 +133,39 @@ public class MultiBranchPipelineImpl extends BlueMultiBranchPipeline {
/**
* TODO: this code need cleanup once MultiBranchProject exposes default branch. At present
*
* At present we look for master as primary branch, if not found we find the latest build and return
* its score.
* At present we look for master as primary branch, if not found we find the latest build across all branches and
* return its score.
*
* If there are no builds taken place 0 score is returned.
*/
Job j = mbp.getBranch("master");
Job j = mbp.getItem("master");
if(j == null) {
j = mbp.getBranch("production");
if(j == null){ //get latest
j = mbp.getItem("production");
/**
* If there are no master or production branch then we return weather score of
*
* - Sort latest build of all branches in ascending order
* - Return the latest
*
*/
if(j == null){
Collection<Job> jbs = mbp.getAllJobs();
if(jbs.size() > 0){
Job[] jobs = jbs.toArray(new Job[jbs.size()]);
Arrays.sort(jobs, new Comparator<Job>() {
@Override
public int compare(Job o1, Job o2) {
long t1 = o1.getLastBuild().getTimeInMillis() + o1.getLastBuild().getDuration();
long t2 = o2.getLastBuild().getTimeInMillis() + o2.getLastBuild().getDuration();
long t1 = 0;
if(o1.getLastBuild() != null){
t1 = o1.getLastBuild().getTimeInMillis() + o1.getLastBuild().getDuration();
}
long t2 = 0;
if(o2.getLastBuild() != null){
t2 = o2.getLastBuild().getTimeInMillis() + o2.getLastBuild().getDuration();
}
if(t1<2){
return -1;
}else if(t1 > t2){