Compare commits
5 Commits
master
...
feature/JE
Author | SHA1 | Date |
---|---|---|
Cliff Meyers | de36bfab87 | |
Cliff Meyers | 93a0968449 | |
Cliff Meyers | 820d9d7c9f | |
Cliff Meyers | 7e1a81e928 | |
Cliff Meyers | fcc0a421a4 |
|
@ -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
|
||||
|
|
|
@ -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-6-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>blueocean-analytics-tools</artifactId>
|
||||
|
|
|
@ -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-6-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>blueocean-commons</artifactId>
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
"skin-deep": "^0.16.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jenkins-cd/design-language": "0.0.67",
|
||||
"@jenkins-cd/design-language": "0.0.65",
|
||||
"@jenkins-cd/js-extensions": "0.0.20",
|
||||
"@jenkins-cd/js-modules": "0.0.5",
|
||||
"@jenkins-cd/sse-gateway": "0.0.7",
|
||||
|
|
|
@ -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-6-SNAPSHOT</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
|
|
|
@ -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>);
|
||||
|
|
|
@ -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()}
|
||||
|
|
|
@ -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>
|
||||
/
|
||||
<a onClick={() => this.handleNameClick()}>{run.pipeline}</a>
|
||||
|
||||
#{run.id}
|
||||
</h4>
|
||||
|
||||
<div className="row">
|
||||
<div className="commons">
|
||||
<div>
|
||||
<label>Branch</label>
|
||||
<span>{decodeURIComponent(run.pipeline)}</span>
|
||||
</div>
|
||||
{ run.commitId ?
|
||||
<div>
|
||||
<label>Commit</label>
|
||||
<span className="commit">
|
||||
#{run.commitId.substring(0, 8)}
|
||||
</span>
|
||||
</div>
|
||||
: null }
|
||||
<div>
|
||||
{ authors.length > 0 ?
|
||||
<a className="authors" onClick={() => this.handleAuthorsClick()}>
|
||||
Changes by {authors.map(
|
||||
author => ` ${author}`)}
|
||||
</a>
|
||||
: 'No changes' }
|
||||
</div>
|
||||
</div>
|
||||
<div className="times">
|
||||
<div>
|
||||
<Icon {...{
|
||||
size: 20,
|
||||
icon: 'timelapse',
|
||||
style: { fill: '#fff' },
|
||||
}} />
|
||||
<TimeDuration millis={durationMillis} liveUpdate={run.isRunning()} />
|
||||
</div>
|
||||
<div>
|
||||
<Icon {...{
|
||||
size: 20,
|
||||
icon: 'access_time',
|
||||
style: { fill: '#fff' },
|
||||
}} />
|
||||
<ReadableDate date={run.endTime} liveUpdate />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
||||
RunDetailsHeader.propTypes = {
|
||||
data: object.isRequired,
|
||||
colors: object,
|
||||
onOrganizationClick: func,
|
||||
onNameClick: func,
|
||||
onAuthorsClick: func,
|
||||
};
|
||||
|
||||
export { RunDetailsHeader };
|
|
@ -4,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 &&
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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-6-SNAPSHOT</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
|
|
|
@ -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.63",
|
||||
"@jenkins-cd/js-extensions": "0.0.20",
|
||||
"@jenkins-cd/js-modules": "0.0.5",
|
||||
"@jenkins-cd/sse-gateway": "0.0.7",
|
||||
|
|
|
@ -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-6-SNAPSHOT</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
|
|
|
@ -89,6 +89,7 @@ const extractPath = (path, begin, end) => {
|
|||
};
|
||||
|
||||
/**
|
||||
* Renders a stack of "favorites cards" including current most recent status.
|
||||
*/
|
||||
export class DashboardCards extends Component {
|
||||
|
||||
|
@ -112,7 +113,7 @@ export class DashboardCards extends Component {
|
|||
let branchName;
|
||||
|
||||
if (pipeline._class === 'io.jenkins.blueocean.rest.impl.pipeline.BranchImpl') {
|
||||
// branch.fullName is in the form folder1/folder2/pipeline/branch ...
|
||||
// pipeline.fullName is in the form folder1/folder2/pipeline/branch ...
|
||||
// "pipeline"
|
||||
pipelineName = extractPath(pipeline.fullName, -2, -1);
|
||||
// everything up to "branch"
|
||||
|
@ -166,8 +167,8 @@ export class DashboardCards extends Component {
|
|||
return (
|
||||
<div className="favorites-card-stack">
|
||||
<TransitionGroup transitionName="vertical-expand-collapse"
|
||||
transitionEnterTimeout={150}
|
||||
transitionLeaveTimeout={150}
|
||||
transitionEnterTimeout={300}
|
||||
transitionLeaveTimeout={300}
|
||||
>
|
||||
{favoriteCards}
|
||||
</TransitionGroup>
|
||||
|
|
|
@ -9,6 +9,8 @@ import { List } from 'immutable';
|
|||
import { userSelector, favoritesSelector } from '../redux/FavoritesStore';
|
||||
import { actions } from '../redux/FavoritesActions';
|
||||
|
||||
import favoritesSseListener from '../model/FavoritesSseListener';
|
||||
|
||||
/**
|
||||
* FavoritesProvider ensures that the current user's favorites
|
||||
* are loaded for any components which may need it.
|
||||
|
@ -20,12 +22,17 @@ export class FavoritesProvider extends Component {
|
|||
|
||||
componentWillMount() {
|
||||
this._initialize(this.props);
|
||||
favoritesSseListener.initialize(this.props.updateRun);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(props) {
|
||||
this._initialize(props);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
favoritesSseListener.dispose();
|
||||
}
|
||||
|
||||
_initialize(props) {
|
||||
const { user, favorites } = props;
|
||||
|
||||
|
@ -54,6 +61,7 @@ FavoritesProvider.propTypes = {
|
|||
favorites: PropTypes.instanceOf(List),
|
||||
fetchUser: PropTypes.func,
|
||||
fetchFavorites: PropTypes.func,
|
||||
updateRun: PropTypes.func,
|
||||
};
|
||||
|
||||
const selectors = createSelector(
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* Created by cmeyers on 7/29/16.
|
||||
*/
|
||||
|
||||
import { SseBus } from './SseBus';
|
||||
|
||||
import fetch from 'isomorphic-fetch';
|
||||
import * as sse from '@jenkins-cd/sse-gateway';
|
||||
|
||||
class FavoritesSseListener {
|
||||
initialize(listener) {
|
||||
if (!this.sseBus) {
|
||||
this.sseBus = new SseBus(sse, fetch);
|
||||
this.sseBus.subscribeToJob(listener);
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
if (this.sseBus) {
|
||||
this.sseBus.dispose();
|
||||
this.sseBus = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const instance = new FavoritesSseListener();
|
||||
|
||||
export default instance;
|
|
@ -0,0 +1,159 @@
|
|||
/**
|
||||
* Created by cmeyers on 7/29/16.
|
||||
*/
|
||||
import defaultFetch from 'isomorphic-fetch';
|
||||
|
||||
/**
|
||||
* Wraps the SSE Gateway and fetches data related to events from REST API.
|
||||
* TODO: should probably send additional data *and* the original event to callback
|
||||
*/
|
||||
export class SseBus {
|
||||
|
||||
constructor(sse, fetch) {
|
||||
this.sse = sse;
|
||||
this.fetch = fetch || defaultFetch;
|
||||
this.jobListenerSse = null;
|
||||
this.jobListenerExternal = null;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
if (this.jobListenerSse) {
|
||||
this.sse.unsubscribe(this.jobListenerSse);
|
||||
this.jobListenerSse = null;
|
||||
}
|
||||
|
||||
if (this.jobListenerExternal) {
|
||||
this.jobListenerExternal = null;
|
||||
}
|
||||
}
|
||||
|
||||
subscribeToJob(callback) {
|
||||
console.log('subscribeToJob with ', callback);
|
||||
this.jobListenerExternal = callback;
|
||||
this.jobListenerSse = this.sse.subscribe('job', (event) => {
|
||||
this._handleJobEvent(event);
|
||||
});
|
||||
}
|
||||
|
||||
_handleJobEvent(event) {
|
||||
switch (event.jenkins_event) {
|
||||
case 'job_crud_created':
|
||||
case 'job_crud_deleted':
|
||||
case 'job_crud_renamed':
|
||||
this._refetchPipelines();
|
||||
break;
|
||||
case 'job_run_queue_buildable':
|
||||
case 'job_run_queue_enter':
|
||||
this._enqueueJob(event);
|
||||
break;
|
||||
case 'job_run_queue_left':
|
||||
case 'job_run_queue_blocked': {
|
||||
break;
|
||||
}
|
||||
case 'job_run_started': {
|
||||
this._updateJob(event);
|
||||
break;
|
||||
}
|
||||
case 'job_run_ended': {
|
||||
this._updateJob(event);
|
||||
break;
|
||||
}
|
||||
default :
|
||||
// Else ignore the event.
|
||||
}
|
||||
}
|
||||
|
||||
_refetchPipelines() {
|
||||
// TODO: implement once migration into commons JS
|
||||
}
|
||||
|
||||
_enqueueJob(event) {
|
||||
const newRun = {
|
||||
event,
|
||||
};
|
||||
|
||||
newRun.pipeline = event.job_ismultibranch ?
|
||||
event.blueocean_job_branch_name :
|
||||
event.blueocean_job_pipeline_name;
|
||||
|
||||
const baseUrl = '/blue';
|
||||
const runUrl = cleanSlashes(`${baseUrl}/${event.blueocean_job_rest_url}/runs/${event.job_run_queueId}`);
|
||||
|
||||
newRun._links = {
|
||||
self: {
|
||||
href: runUrl,
|
||||
},
|
||||
};
|
||||
|
||||
newRun.state = 'QUEUED';
|
||||
newRun.result = 'UNKNOWN';
|
||||
|
||||
console.log('enqueueJob', event);
|
||||
|
||||
if (this.jobListenerExternal) {
|
||||
this.jobListenerExternal(newRun);
|
||||
}
|
||||
}
|
||||
|
||||
_updateJob(event) {
|
||||
const baseUrl = '/jenkins/blue';
|
||||
const url = cleanSlashes(`${baseUrl}/${event.blueocean_job_rest_url}/runs/${event.jenkins_object_id}`);
|
||||
|
||||
this.fetch(url)
|
||||
.then(checkStatus)
|
||||
.then(parseJSON)
|
||||
.then((data) => {
|
||||
console.log('updateJob', event, data);
|
||||
|
||||
if (event.jenkins_event === 'job_run_ended') {
|
||||
data.state = 'FINISHED';
|
||||
} else {
|
||||
data.state = 'RUNNING';
|
||||
}
|
||||
|
||||
if (this.jobListenerExternal) {
|
||||
this.jobListenerExternal(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_updateMultiBranchPipelineBranches() {
|
||||
// TODO: implement once migration into commons JS
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// TODO: migrate all this code down to 'fetch'
|
||||
function checkStatus(response) {
|
||||
if (response.status >= 300 || response.status < 200) {
|
||||
const error = new Error(response.statusText);
|
||||
error.response = response;
|
||||
throw error;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
function parseJSON(response) {
|
||||
return response.json()
|
||||
// FIXME: workaround for status=200 w/ empty response body that causes error in Chrome
|
||||
// server should probably return HTTP 204 instead
|
||||
.catch((error) => {
|
||||
if (error.message === 'Unexpected end of JSON input') {
|
||||
return {};
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
export const cleanSlashes = (url) => {
|
||||
if (url.indexOf('//') !== -1) {
|
||||
let cleanUrl = url.replace('//', '/');
|
||||
cleanUrl = cleanUrl.substr(-1) === '/' ?
|
||||
cleanUrl : `${cleanUrl}/`;
|
||||
|
||||
return cleanSlashes(cleanUrl);
|
||||
}
|
||||
|
||||
return url;
|
||||
};
|
|
@ -105,6 +105,15 @@ export const actions = {
|
|||
};
|
||||
},
|
||||
|
||||
updateRun(jobRun) {
|
||||
return (dispatch) => {
|
||||
dispatch({
|
||||
type: ACTION_TYPES.UPDATE_RUN,
|
||||
jobRun,
|
||||
});
|
||||
};
|
||||
},
|
||||
|
||||
generateData(request, actionType, optional) {
|
||||
const { url, fetchOptions } = request;
|
||||
return (dispatch) => fetch(url, fetchOptions)
|
||||
|
|
|
@ -21,8 +21,13 @@ export const ACTION_TYPES = keymirror({
|
|||
SET_USER: null,
|
||||
SET_FAVORITES: null,
|
||||
TOGGLE_FAVORITE: null,
|
||||
UPDATE_RUN: null,
|
||||
});
|
||||
|
||||
function clone(json) {
|
||||
return JSON.parse(JSON.stringify(json));
|
||||
}
|
||||
|
||||
const actionHandlers = {
|
||||
[ACTION_TYPES.SET_USER](state, { payload }) {
|
||||
const user = new User(payload);
|
||||
|
@ -52,6 +57,26 @@ const actionHandlers = {
|
|||
|
||||
return state.set('favorites', prunedList);
|
||||
},
|
||||
[ACTION_TYPES.UPDATE_RUN](state, { jobRun }) {
|
||||
const favorites = state.get('favorites');
|
||||
|
||||
for (const fav of favorites) {
|
||||
const runsBaseUrl = `${fav.item._links.self.href}runs`;
|
||||
const runUrl = jobRun._links.self.href;
|
||||
|
||||
// TODO; this might be broken for non-multibranch as the URL structures are different
|
||||
if (runUrl.indexOf(runsBaseUrl) === 0) {
|
||||
const index = favorites.indexOf(fav);
|
||||
const updatedFavorite = clone(fav);
|
||||
updatedFavorite.item.latestRun = jobRun;
|
||||
const updatedFavorites = favorites.set(index, updatedFavorite);
|
||||
return state.set('favorites', updatedFavorites);
|
||||
}
|
||||
}
|
||||
|
||||
console.warn('run was not updated; likely an error?');
|
||||
return state;
|
||||
},
|
||||
};
|
||||
|
||||
const favoritesStore = state => state.favoritesStore;
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -69,15 +69,11 @@
|
|||
path.running {
|
||||
stroke: white;
|
||||
}
|
||||
circle.inner {
|
||||
stroke: white;
|
||||
fill: white;
|
||||
}
|
||||
}
|
||||
|
||||
.progress-spinner.queued {
|
||||
circle {
|
||||
stroke: #bcd8f1;
|
||||
stroke: white;
|
||||
}
|
||||
circle.inner {
|
||||
stroke: white;
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
}
|
||||
|
||||
.vertical-expand-collapse-enter {
|
||||
transition: all linear 0.15s;
|
||||
transition: all linear 0.3s;
|
||||
max-height: 0;
|
||||
opacity: 0.01;
|
||||
|
||||
|
@ -24,7 +24,7 @@
|
|||
}
|
||||
|
||||
.vertical-expand-collapse-leave {
|
||||
transition: all linear 0.15s;
|
||||
transition: all linear 0.3s;
|
||||
max-height: 60px;
|
||||
opacity: 1;
|
||||
|
||||
|
|
|
@ -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-6-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>blueocean-pipeline-api-impl</artifactId>
|
||||
|
|
|
@ -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())));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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-6-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>blueocean</artifactId>
|
||||
|
|
|
@ -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-6-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>blueocean-rest-impl</artifactId>
|
||||
|
|
|
@ -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())));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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-6-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>blueocean-rest</artifactId>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
"zombie": "^4.2.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jenkins-cd/design-language": "0.0.67",
|
||||
"@jenkins-cd/design-language": "0.0.65",
|
||||
"@jenkins-cd/js-extensions": "0.0.20",
|
||||
"@jenkins-cd/js-modules": "0.0.5",
|
||||
"history": "2.0.2",
|
||||
|
|
|
@ -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-6-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>blueocean-web</artifactId>
|
||||
|
|
4
pom.xml
4
pom.xml
|
@ -10,7 +10,7 @@
|
|||
|
||||
<groupId>io.jenkins.blueocean</groupId>
|
||||
<artifactId>blueocean-parent</artifactId>
|
||||
<version>1.0-alpha-7-SNAPSHOT</version>
|
||||
<version>1.0-alpha-6-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>
|
||||
|
|
Loading…
Reference in New Issue