JENKINS-48075 add next/prev links to run details screen (#1722)

* add next/prev links to run details screen

* implement PR suggestions
This commit is contained in:
Nicolae Pascu 2018-04-24 09:14:48 +10:00 committed by GitHub
parent fdcc18f3d5
commit 16d42cd0f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 197 additions and 10 deletions

View File

@ -1,12 +1,46 @@
import React, { Component, PropTypes } from 'react';
import { Icon } from '@jenkins-cd/design-language';
import { UrlConfig, AppConfig, logging, ResultPageHeader, TimeManager } from '@jenkins-cd/blueocean-core-js';
import { UrlConfig, AppConfig, logging, ResultPageHeader, TimeManager, UrlBuilder } from '@jenkins-cd/blueocean-core-js';
import { ExpandablePath, ReadableDate, TimeDuration, CommitId } from '@jenkins-cd/design-language';
import ChangeSetToAuthors from './ChangeSetToAuthors';
import { Link } from 'react-router';
import { UrlBuilder } from '@jenkins-cd/blueocean-core-js';
import RunIdCell from './RunIdCell';
export class RunIdNavigation extends Component {
render() {
const { run, pipeline, branchName, t } = this.props;
const nextRunId = run._links.nextRun ? /[\/].*runs\/*([0-9]*)/g.exec(run._links.nextRun.href)[1] : '';
const prevRunId = run._links.prevRun ? /[\/].*runs\/*([0-9]*)/g.exec(run._links.prevRun.href)[1] : '';
const nextRunUrl = nextRunId ? UrlBuilder.buildRunUrl(pipeline.organization, pipeline.fullName, branchName, nextRunId, 'pipeline') : '';
const prevRunUrl = prevRunId ? UrlBuilder.buildRunUrl(pipeline.organization, pipeline.fullName, branchName, prevRunId, 'pipeline') : '';
return (
<span className="run-nav-container">
{prevRunUrl && (
<Link to={prevRunUrl} title={t('rundetail.header.prev_run', { defaultValue: 'Previous Run' })}>
<Icon size={24} icon="HardwareKeyboardArrowLeft" style={{ verticalAlign: 'bottom' }} />
</Link>
)}
<RunIdCell run={run} />
{nextRunUrl && (
<Link to={nextRunUrl} title={t('rundetail.header.next_run', { defaultValue: 'Next Run' })}>
<Icon size={24} icon="HardwareKeyboardArrowRight" style={{ verticalAlign: 'bottom' }} />
</Link>
)}
</span>
);
}
}
RunIdNavigation.propTypes = {
run: PropTypes.object,
pipeline: PropTypes.object,
branchName: PropTypes.string,
t: PropTypes.func,
};
class RunDetailsHeader extends Component {
componentWillMount() {
this._setDuration(this.props);
@ -81,9 +115,7 @@ class RunDetailsHeader extends Component {
<Link className="path-link" to={activityUrl}>
<ExpandablePath path={fullDisplayName} hideFirst className="dark-theme" iconSize={20} />
</Link>
<span>
&nbsp;<RunIdCell run={run} />
</span>
<RunIdNavigation run={run} pipeline={pipeline} branchName={displayName} t={t} />
</h1>
);

View File

@ -12,8 +12,12 @@
}
}
.RunDetailsHeader-title > span {
.RunDetailsHeader-title .run-nav-container {
vertical-align: middle;
span:first-child {
margin-left: 10px;
}
}
.RunDetailsHeader-sources,

View File

@ -210,6 +210,8 @@ rundetail.header.tab.artifacts=Artifacts
rundetail.header.tab.changes=Changes
rundetail.header.tab.pipeline=Pipeline
rundetail.header.tab.tests=Tests
rundetail.header.next_run=Next Run
rundetail.header.prev_run=Previous Run
rundetail.input.cancel=Abort
rundetail.pipeline.description=Description
rundetail.pipeline.logs=Logs

View File

@ -0,0 +1,95 @@
import React from 'react';
import { assert } from 'chai';
import { shallow } from 'enzyme';
import { RunIdNavigation } from '../../main/js/components/RunDetailsHeader';
import { i18nTranslator } from '@jenkins-cd/blueocean-core-js';
const t = i18nTranslator('blueocean-dashboard');
const mockRuns = [
{
'id': '1',
'organization': 'jenkins',
'pipeline': 'mockPipeline',
'_links': {
'nextRun': {
'_class': 'io.jenkins.blueocean.rest.hal.Link',
'href': '/blue/rest/organizations/jenkins/pipelines/mockPipeline/runs/2/'
},
}
},
{
'id': '2',
'organization': 'jenkins',
'pipeline': 'mockPipeline',
'_links': {
'prevRun': {
'_class': 'io.jenkins.blueocean.rest.hal.Link',
'href': '/blue/rest/organizations/jenkins/pipelines/mockPipeline/runs/1/'
},
'nextRun': {
'_class': 'io.jenkins.blueocean.rest.hal.Link',
'href': '/blue/rest/organizations/jenkins/pipelines/mockPipeline/runs/3/'
},
}
},
{
'id': '3',
'organization': 'jenkins',
'pipeline': 'mockPipeline',
'_links': {
'prevRun': {
'_class': 'io.jenkins.blueocean.rest.hal.Link',
'href': '/blue/rest/organizations/jenkins/pipelines/mockPipeline/runs/2/'
},
}
}
];
const mockPipeline = {
'organization': 'jenkins',
'fullName': 'mockPipeline',
}
import { mockExtensionsForI18n } from './mock-extensions-i18n';
mockExtensionsForI18n();
describe('RunDetailsIdNavigation', () => {
beforeAll(() => mockExtensionsForI18n());
it('check next link on run with id = 1', () => {
let wrapper = shallow(<RunIdNavigation run={mockRuns[0]} pipeline={mockPipeline} branchName='branchName' t={t} />);
//check that there is only one link for first run (only next)
assert.equal(wrapper.find('Link').length, 1);
//check next link
assert.equal(wrapper.find('Link').first().props().title, 'Next Run');
assert.equal(wrapper.find('Link').first().props().to, '/organizations/jenkins/mockPipeline/detail/branchName/2/pipeline');
});
it('check next/prev links on run with id = 2', () => {
let wrapper = shallow(<RunIdNavigation run={mockRuns[1]} pipeline={mockPipeline} branchName='branchName' t={t} />);
//check prev link
assert.equal(wrapper.find('Link').first().props().title, 'Previous Run');
assert.equal(wrapper.find('Link').first().props().to, '/organizations/jenkins/mockPipeline/detail/branchName/1/pipeline');
//check next link
assert.equal(wrapper.find('Link').at(1).props().title, 'Next Run');
assert.equal(wrapper.find('Link').at(1).props().to, '/organizations/jenkins/mockPipeline/detail/branchName/3/pipeline');
});
it('check next link on run with id = 3', () => {
let wrapper = shallow(<RunIdNavigation run={mockRuns[2]} pipeline={mockPipeline} branchName='branchName' t={t} />);
//check that there is only one link for the last run (only prev)
assert.equal(wrapper.find('Link').length, 1);
//check prev link
assert.equal(wrapper.find('Link').first().props().title, 'Previous Run');
assert.equal(wrapper.find('Link').first().props().to, '/organizations/jenkins/mockPipeline/detail/branchName/2/pipeline');
});
});

View File

@ -238,6 +238,45 @@ public class AbstractRunImplTest extends PipelineBaseTest {
Assert.assertEquals("Waiting for next available executor", latestRun.get("causeOfBlockage"));
}
@Test
public void pipelineRunIncludesNextPrevLinks() throws Exception {
WorkflowJob p = createWorkflowJobWithJenkinsfile(getClass(),"latestRunIncludesQueued.jenkinsfile");
// Ensure null before first run
Map pipeline = request().get(String.format("/organizations/jenkins/pipelines/%s/", p.getName())).build(Map.class);
Assert.assertNull(pipeline.get("latestRun"));
// Run until completed
Run r = p.scheduleBuild2(0).waitForStart();
j.waitForCompletion(r);
// Make the next runs queue
j.jenkins.setNumExecutors(1);
// Schedule another run so it goes in the queue
WorkflowRun r2 = p.scheduleBuild2(0).waitForStart();
j.waitForCompletion(r2);
// Schedule another run so it goes in the queue
WorkflowRun r3 = p.scheduleBuild2(0).waitForStart();
j.waitForCompletion(r3);
// Get latest run for this pipeline
Map secondRun = request().get(String.format("/organizations/jenkins/pipelines/%s/runs/2", p.getName())).build(Map.class);
String prevRunUrl = ((Map)((Map)secondRun.get("_links")).get("prevRun")).get("href").toString();
String nextRunUrl = ((Map)((Map)secondRun.get("_links")).get("nextRun")).get("href").toString();
//check the run id of the second run
Assert.assertEquals("2", secondRun.get("id"));
//check that id in previous run url is 1
Assert.assertEquals("1", prevRunUrl.substring(prevRunUrl.length() - 2, prevRunUrl.length() - 1));
//check that id in next run url is 3
Assert.assertEquals("3", nextRunUrl.substring(nextRunUrl.length() - 2, nextRunUrl.length() - 1));
}
@Issue("JENKINS-44981")
@Test
public void queuedSingleNode() throws Exception {

View File

@ -13,6 +13,7 @@ import io.jenkins.blueocean.commons.ServiceException;
import io.jenkins.blueocean.rest.Reachable;
import io.jenkins.blueocean.rest.factory.BlueTestResultFactory;
import io.jenkins.blueocean.rest.hal.Link;
import io.jenkins.blueocean.rest.hal.LinkResolver;
import io.jenkins.blueocean.rest.hal.Links;
import io.jenkins.blueocean.rest.model.BlueActionProxy;
import io.jenkins.blueocean.rest.model.BlueArtifactContainer;
@ -326,7 +327,21 @@ public abstract class AbstractRunImpl<T extends Run> extends BlueRun {
@Override
public Links getLinks() {
return super.getLinks().add("parent", parent.getLink());
Links links = super.getLinks().add("parent", parent.getLink());
Run nextRun = run.getNextBuild();
Run prevRun = run.getPreviousBuild();
if(nextRun != null) {
Link nextRunLink = LinkResolver.resolveLink(nextRun);
links.add("nextRun", nextRunLink);
}
if(prevRun != null) {
Link prevRunLink = LinkResolver.resolveLink(prevRun);
links.add("prevRun", prevRunLink);
}
return links;
}
public static class BlueCauseImpl extends BlueCause {

View File

@ -1,6 +1,6 @@
.svg-icon {
display: 'inline-block';
vertical-align: 'middle';
user-select: 'none';
display: inline-block;
vertical-align: middle;
user-select: none;
fill: #ffffff;
}