Compare commits

..

5 Commits

Author SHA1 Message Date
Cliff Meyers 895177c9fb [JENKINS-37023] fix js-extensions that was incorrectly merged 2016-08-02 08:01:42 -04:00
Cliff Meyers d94f197878 Merge branch 'master' into feature/JENKINS-37023-pipeline-card-linking
# Conflicts:
#	blueocean-dashboard/package.json
#	blueocean-personalization/package.json
#	blueocean-web/package.json
2016-07-29 12:45:05 -04:00
Cliff Meyers fb22a820d5 [JENKINS-37023] allow clicking anywhere on the card itself to open the run details; allow clicking on the title to open the activity view 2016-07-29 12:40:22 -04:00
Cliff Meyers 3e00e91d42 [JENKINS-37023] pass down the router explicitly 2016-07-29 12:38:13 -04:00
Cliff Meyers d087530e52 [JENKINS-37023] tick JDL to get updated Favorite component 2016-07-29 12:37:15 -04:00
35 changed files with 311 additions and 486 deletions

View File

@ -1,17 +1,13 @@
# Description
**Decription**
See [JENKINS-XXXXX](https://issues.jenkins-ci.org/browse/JENKINS-XXXXX).
# Submitter checklist
- [ ] Link to JIRA ticket in description, if appropriate.
**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'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
@jenkinsci/code-reviewers @reviewbybees
@reviewbybees

View File

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

View File

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

View File

@ -35,7 +35,7 @@
"skin-deep": "^0.16.0"
},
"dependencies": {
"@jenkins-cd/design-language": "0.0.67",
"@jenkins-cd/design-language": "0.0.66",
"@jenkins-cd/js-extensions": "0.0.20",
"@jenkins-cd/js-modules": "0.0.5",
"@jenkins-cd/sse-gateway": "0.0.7",

View File

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

View File

@ -2,7 +2,7 @@ import React, { Component, PropTypes } from 'react';
import { EmptyStateView, Table } from '@jenkins-cd/design-language';
import Runs from './Runs';
import Pipeline from '../api/Pipeline';
import { RunRecord, ChangeSetRecord } from './records';
import { ActivityRecord, ChangeSetRecord } from './records';
import RunPipeline from './RunPipeline.jsx';
import {
actions,
@ -88,27 +88,26 @@ 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]
]);
}
return (<Runs {...{
key: index,
changeset: latestRecord,
result: new RunRecord(run) }} />);
})
}
{ 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} />);
})}
</Table>
</article>
</main>);

View File

@ -60,6 +60,7 @@ export default class Pipelines extends Component {
<Extensions.Renderer
extensionPoint="jenkins.pipeline.list.top"
store={this.context.store}
router={this.context.router}
/>
<Table
className="pipelines-table fixed"
@ -88,4 +89,5 @@ Pipelines.contextTypes = {
params: PropTypes.object,
pipelines: array,
store: PropTypes.object,
router: PropTypes.object,
};

View File

@ -3,6 +3,7 @@ import {
ModalView,
ModalBody,
ModalHeader,
PipelineResult,
PageTabs,
TabLink,
} from '@jenkins-cd/design-language';
@ -22,9 +23,6 @@ import {
buildRunDetailsUrl,
} from '../util/UrlUtils';
import { RunDetailsHeader } from './RunDetailsHeader';
import { RunRecord } from './records';
const { func, object, array, any, string } = PropTypes;
class RunDetails extends Component {
@ -74,29 +72,43 @@ class RunDetails extends Component {
return null;
}
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
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];
// 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) {
if (!currentRun) {
return null;
}
const currentRun = new RunRecord(foundRun);
const status = currentRun.getComputedResult();
currentRun.name = name;
const status = currentRun.result === 'UNKNOWN' ? currentRun.state : currentRun.result;
const afterClose = () => {
const fallbackUrl = buildPipelineUrl(params.organization, params.pipeline);
const fallbackUrl = buildPipelineUrl(organization, name);
location.pathname = this.opener || fallbackUrl;
router.push(location);
};
return (
<ModalView
isVisible
@ -107,7 +119,7 @@ class RunDetails extends Component {
>
<ModalHeader>
<div>
<RunDetailsHeader data={currentRun}
<PipelineResult data={currentRun}
onOrganizationClick={() => this.navigateToOrganization()}
onNameClick={() => this.navigateToPipeline()}
onAuthorsClick={() => this.navigateToChanges()}

View File

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

View File

@ -4,7 +4,6 @@ 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';
@ -24,19 +23,6 @@ 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);
@ -48,43 +34,82 @@ export class RunDetailsPipeline extends Component {
}
componentWillMount() {
const { fetchNodes, fetchLog, result } = this.props;
const { fetchNodes, fetchLog, result, fetchSteps } = this.props;
this.mergedConfig = this.generateConfig(this.props);
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 });
// 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', this._onSseEvent);
this.listener.sse = sse.subscribe('pipeline', onSseEvent);
}
componentDidMount() {
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);
}
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 (this.props.result.isQueued()) {
return;
}
const followAlong = this.state.followAlong;
this.mergedConfig = this.generateConfig({ ...nextProps, followAlong });
@ -133,16 +158,13 @@ export class RunDetailsPipeline extends Component {
}
}
componentWillUnmount() {
const domNode = ReactDOM.findDOMNode(this.refs.scrollArea);
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);
@ -156,56 +178,6 @@ 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) {
@ -214,9 +186,14 @@ export class RunDetailsPipeline extends Component {
}
generateConfig(props) {
const { config = {} } = this.context;
const {
config = {},
} = this.context;
const followAlong = this.state.followAlong;
const { isMultiBranch, params } = props;
const {
isMultiBranch,
params: { pipeline: name, branch, runId, node: nodeParam },
} = 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;
@ -224,31 +201,30 @@ export class RunDetailsPipeline extends Component {
nodeReducer = { id: null, displayName: 'Steps' };
}
// if we have a node param we do not want the calculation of the focused node
const node = params.node || nodeReducer.id;
const node = nodeParam || nodeReducer.id;
// Merge config
return {
...config,
name: params.pipeline,
branch: params.branch,
runId: params.runId,
isMultiBranch,
node,
nodeReducer,
followAlong,
fetchAll,
};
const mergedConfig = { ...config, name, branch, runId, isMultiBranch, node, nodeReducer, followAlong, fetchAll };
return mergedConfig;
}
render() {
const { location, router } = this.context;
const {
location,
router,
} = this.context;
const { isMultiBranch, steps, nodes, logs, result: run, params } = this.props;
if (run.isQueued()) {
return queuedState();
}
const resultRun = run.isCompleted() ? run.state : run.result;
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 followAlong = this.state.followAlong;
// in certain cases we want that the log component will scroll to the end of a log
const scrollToBottom =
@ -316,19 +292,16 @@ export class RunDetailsPipeline extends Component {
}
logProps.logArray = log.logArray;
}
const stepScrollAreaClass = `step-scroll-area ${followAlong ? 'follow-along-on' : 'follow-along-off'}`;
return (
<div ref="scrollArea" className={stepScrollAreaClass}>
<div ref="scrollArea">
{ nodes && nodes[nodeKey] && <Extensions.Renderer
extensionPoint="jenkins.pipeline.run.result"
selectedStage={this.mergedConfig.nodeReducer}
callback={afterClick}
nodes={nodes[nodeKey].model}
pipelineName={name}
branchName={isMultiBranch ? params.branch : undefined}
runId={run.id}
branchName={isMultiBranch ? branch : undefined}
runId={runId}
/>
}
{ shouldShowLogHeader &&

View File

@ -134,10 +134,7 @@ export default class Node extends Component {
}
logProps.logArray = log.logArray;
}
const logConsoleClass = `logConsole step-${id}`;
return (<div className={logConsoleClass}>
return (<div className="logConsole">
<ResultItem
key={id}
result={runResult}

View File

@ -45,8 +45,7 @@ export const ChangeSetRecord = Record({
timestamp: null,
});
export class RunRecord extends Record({
_class: null,
export const ActivityRecord = Record({
changeSet: ChangeSetRecord,
durationInMillis: null,
enQueueTime: null,
@ -61,24 +60,7 @@ export class RunRecord extends 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: {
@ -92,7 +74,7 @@ export const PullRequestRecord = Record({
export const RunsRecord = Record({
_class: null,
_links: null,
latestRun: RunRecord,
latestRun: ActivityRecord,
name: null,
weatherScore: 0,
pullRequest: PullRequestRecord,

View File

@ -6,32 +6,6 @@ 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,
@ -83,7 +57,7 @@ export const actionHandlers = {
return state.set('currentRuns', null);
},
[ACTION_TYPES.SET_CURRENT_RUN_DATA](state, { payload }): State {
return state.set('currentRuns', payload.map((run) => _mapQueueToPsuedoRun(run)));
return state.set('currentRuns', payload);
},
[ACTION_TYPES.SET_NODE](state, { payload }): State {
return state.set('node', { ...payload });
@ -95,8 +69,7 @@ export const actionHandlers = {
},
[ACTION_TYPES.SET_RUNS_DATA](state, { payload, id }): State {
const runs = { ...state.runs } || {};
runs[id] = payload.map(run => _mapQueueToPsuedoRun(run));
runs[id] = payload;
return state.set('runs', runs);
},
[ACTION_TYPES.CLEAR_CURRENT_BRANCHES_DATA](state) {
@ -585,7 +558,7 @@ export const actions = {
fetchRunsIfNeeded(config) {
return (dispatch) => {
const baseUrl = `${config.getAppURLBase()}/rest/organizations/jenkins` +
`/pipelines/${config.pipeline}/activities/`;
`/pipelines/${config.pipeline}/runs/`;
return dispatch(actions.fetchIfNeeded({
url: baseUrl,
id: config.pipeline,

View File

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

View File

@ -34,7 +34,7 @@
"react-addons-test-utils": "15.0.1"
},
"dependencies": {
"@jenkins-cd/design-language": "0.0.67",
"@jenkins-cd/design-language": "0.0.66",
"@jenkins-cd/js-extensions": "0.0.20",
"@jenkins-cd/js-modules": "0.0.5",
"@jenkins-cd/sse-gateway": "0.0.7",

View File

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

View File

@ -147,6 +147,7 @@ export class DashboardCards extends Component {
return (
<div key={favorite._links.self.href}>
<PipelineCard
router={this.props.router}
status={status}
startTime={startTime}
estimatedDuration={estimatedDuration}
@ -186,6 +187,7 @@ export class DashboardCards extends Component {
DashboardCards.propTypes = {
store: PropTypes.object,
router: PropTypes.object,
favorites: PropTypes.instanceOf(List),
toggleFavorite: PropTypes.func,
};

View File

@ -6,6 +6,8 @@ import { Link } from 'react-router';
import { Icon } from 'react-material-icons-blue';
import { Favorite, LiveStatusIndicator } from '@jenkins-cd/design-language';
import { stopProp } from './StopPropagation';
/**
* PipelineCard displays an informational card about a Pipeline and its status.
*
@ -76,19 +78,28 @@ 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`;
const activityUrl = `/organizations/${encodeURIComponent(this.props.organization)}/` +
`${encodeURIComponent(this.props.fullName)}/activity`;
const navigateToRunDetails = () => {
const runUrl = `/organizations/${encodeURIComponent(this.props.organization)}/` +
`${encodeURIComponent(this.props.fullName)}/detail/` +
`${this.props.branch || this.props.pipeline}/${encodeURIComponent(this.props.runId)}/pipeline`;
this.props.router.push({
pathname: runUrl,
});
};
return (
<div className={`pipeline-card ${bgClass}`}>
<div className={`pipeline-card ${bgClass}`} onClick={() => navigateToRunDetails()}>
<LiveStatusIndicator
result={status} startTime={startTime} estimatedDuration={estimatedDuration}
width={'24px'} height={'24px'} noBackground
/>
<span className="name">
<Link to={runUrl}>
<Link to={activityUrl} onClick={(event) => stopProp(event)}>
{this.props.organization} / <span title={this.props.fullName}>{this.props.pipeline}</span>
</Link>
</span>
@ -113,7 +124,7 @@ export class PipelineCard extends Component {
<span className="actions">
{ showRun &&
<a className="run" title="Run Again" onClick={() => this._onRunClick()}>
<a className="run" title="Run Again" onClick={(event) => {stopProp(event); this._onRunClick();}}>
<Icon size={24} icon="replay" />
</a>
}
@ -128,6 +139,7 @@ export class PipelineCard extends Component {
}
PipelineCard.propTypes = {
router: PropTypes.object,
status: PropTypes.string,
startTime: PropTypes.string,
estimatedDuration: PropTypes.number,

View File

@ -0,0 +1,44 @@
/**
* 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 (
<span
className={this.props.className}
onClick={(event) => this._suppressEvent(event)}
>
{this.props.children}
</span>
);
}
}
export const stopProp = (event) => {
event.stopPropagation();
};
StopPropagation.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
};

View File

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

View File

@ -5,9 +5,11 @@
color: white;
min-width: 400px;
padding: 15px;
cursor: pointer;
a {
color: white;
cursor: pointer;
}
.name, .branch, .commit {
@ -69,15 +71,11 @@
path.running {
stroke: white;
}
circle.inner {
stroke: white;
fill: white;
}
}
.progress-spinner.queued {
circle {
stroke: #bcd8f1;
stroke: white;
}
circle.inner {
stroke: white;

View File

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

View File

@ -1,7 +1,5 @@
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;
@ -12,7 +10,17 @@ import io.jenkins.blueocean.rest.Navigable;
import io.jenkins.blueocean.rest.Reachable;
import io.jenkins.blueocean.rest.hal.Link;
import io.jenkins.blueocean.rest.hal.LinkResolver;
import io.jenkins.blueocean.rest.model.*;
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.service.embedded.rest.BlueFavoriteResolver;
import io.jenkins.blueocean.service.embedded.rest.BluePipelineFactory;
import io.jenkins.blueocean.service.embedded.rest.FavoriteImpl;
@ -22,7 +30,6 @@ 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;
@ -350,10 +357,4 @@ public class MultiBranchPipelineImpl extends BlueMultiBranchPipeline {
return null;
}
}
@Exported(inline = true)
@Navigable
public Container<Resource> getActivities() {
return Containers.fromResource(getLink(), Lists.newArrayList(Iterators.concat(getQueue().iterator(), getRuns().iterator())));
}
}

View File

@ -1,17 +1,22 @@
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
@ -29,11 +34,21 @@ public class MultiBranchPipelineQueueContainer extends BlueQueueContainer {
@Override
public BlueQueueItem get(String name) {
try {
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);
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())));
}
}
}
}
}catch (NumberFormatException e){
@ -49,12 +64,43 @@ public class MultiBranchPipelineQueueContainer extends BlueQueueContainer {
@Override
public Iterator<BlueQueueItem> iterator() {
List<BlueQueueItem> queueItems = Lists.newArrayList();
for(Object o: multiBranchPipeline.mbp.getItems()) {
if(o instanceof Job) {
queueItems.addAll(QueueContainerImpl.getQueuedItems((Job)o));
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);
}
}
return queueItems.iterator();
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();
}
}

View File

@ -3,7 +3,6 @@ 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;
@ -15,10 +14,8 @@ 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;
@ -53,9 +50,6 @@ 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"};
@ -729,56 +723,4 @@ public class MultiBranchTest extends PipelineBaseTest {
return p;
}
//Disabled test for now as I can't get it to work. Tested manually.
//@Test
public void getPipelineJobActivities() throws Exception {
WorkflowMultiBranchProject mp = j.jenkins.createProject(WorkflowMultiBranchProject.class, "p");
sampleRepo1.init();
sampleRepo1.write("Jenkinsfile", "stage 'build'\n "+"node {echo 'Building'}\n"+
"stage 'test'\nnode { echo 'Testing'}\n" +
"sleep 10000 \n"+
"stage 'deploy'\nnode { echo 'Deploying'}\n"
);
sampleRepo1.write("file", "initial content");
sampleRepo1.git("add", "Jenkinsfile");
sampleRepo1.git("commit", "--all", "--message=flow");
//create feature branch
sampleRepo1.git("checkout", "-b", "abc");
sampleRepo1.write("Jenkinsfile", "echo \"branch=${env.BRANCH_NAME}\"; "+"node {" +
" stage ('Build'); " +
" echo ('Building'); " +
" stage ('Test'); sleep 10000; " +
" echo ('Testing'); " +
" stage ('Deploy'); " +
" echo ('Deploying'); " +
"}");
ScriptApproval.get().approveSignature("method java.lang.String toUpperCase");
sampleRepo1.write("file", "subsequent content1");
sampleRepo1.git("commit", "--all", "--message=tweaked1");
mp.getSourcesList().add(new BranchSource(new GitSCMSource(null, sampleRepo1.toString(), "", "*", "", false),
new DefaultBranchPropertyStrategy(new BranchProperty[0])));
for (SCMSource source : mp.getSCMSources()) {
assertEquals(mp, source.getOwner());
}
scheduleAndFindBranchProject(mp);
for(WorkflowJob job : mp.getItems()) {
Queue.Item item = job.getQueueItem();
if(item != null ) {
item.getFuture().waitForStart();
}
job.setConcurrentBuild(false);
job.scheduleBuild2(0);
job.scheduleBuild2(0);
}
List l = request().get("/organizations/jenkins/pipelines/p/activities").build(List.class);
Assert.assertEquals(4, l.size());
Assert.assertEquals("io.jenkins.blueocean.service.embedded.rest.QueueItemImpl", ((Map) l.get(0)).get("_class"));
Assert.assertEquals("io.jenkins.blueocean.rest.impl.pipeline.PipelineRunImpl", ((Map) l.get(2)).get("_class"));
}
}

View File

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

View File

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

View File

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

View File

@ -1,7 +1,5 @@
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;
@ -10,6 +8,7 @@ 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;
@ -17,13 +16,9 @@ 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;
@ -101,7 +96,6 @@ public class PipelineImpl extends BluePipeline {
return new QueueContainerImpl(this);
}
@WebMethod(name="") @DELETE
public void delete() throws IOException, InterruptedException {
job.delete();
@ -185,9 +179,4 @@ public class PipelineImpl extends BluePipeline {
}
@Exported(inline = true)
@Navigable
public Container<Resource> getActivities() {
return Containers.fromResource(getLink(),Lists.newArrayList(Iterators.concat(getQueue().iterator(), getRuns().iterator())));
}
}

View File

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

View File

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

View File

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

View File

@ -25,7 +25,7 @@
"zombie": "^4.2.1"
},
"dependencies": {
"@jenkins-cd/design-language": "0.0.67",
"@jenkins-cd/design-language": "0.0.66",
"@jenkins-cd/js-extensions": "0.0.20",
"@jenkins-cd/js-modules": "0.0.5",
"history": "2.0.2",

View File

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

View File

@ -10,7 +10,7 @@
<groupId>io.jenkins.blueocean</groupId>
<artifactId>blueocean-parent</artifactId>
<version>1.0-alpha-7-SNAPSHOT</version>
<version>1.0-alpha-5-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Blue Ocean UI Parent</name>
@ -190,7 +190,7 @@
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>sse-gateway</artifactId>
<version>1.8</version>
<version>1.6</version>
</dependency>
<dependency>