diff --git a/blueocean-dashboard/package.json b/blueocean-dashboard/package.json index 65778df1..3bfc870a 100644 --- a/blueocean-dashboard/package.json +++ b/blueocean-dashboard/package.json @@ -35,7 +35,7 @@ "skin-deep": "^0.16.0" }, "dependencies": { - "@jenkins-cd/design-language": "0.0.65", + "@jenkins-cd/design-language": "0.0.67", "@jenkins-cd/js-extensions": "0.0.20", "@jenkins-cd/js-modules": "0.0.5", "@jenkins-cd/sse-gateway": "0.0.7", diff --git a/blueocean-dashboard/src/main/js/components/Activity.jsx b/blueocean-dashboard/src/main/js/components/Activity.jsx index 1a10dd48..5aaa590d 100644 --- a/blueocean-dashboard/src/main/js/components/Activity.jsx +++ b/blueocean-dashboard/src/main/js/components/Activity.jsx @@ -2,7 +2,7 @@ import React, { Component, PropTypes } from 'react'; import { EmptyStateView, Table } from '@jenkins-cd/design-language'; import Runs from './Runs'; import Pipeline from '../api/Pipeline'; -import { ActivityRecord, ChangeSetRecord } from './records'; +import { RunRecord, ChangeSetRecord } from './records'; import RunPipeline from './RunPipeline.jsx'; import { actions, @@ -88,26 +88,27 @@ export class Activity extends Component { { label: '', className: 'actions' }, ]; - + return (
{showRunButton && } - { 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.map((run, index) => { + const changeset = run.changeSet; + let latestRecord = {}; + if (changeset && changeset.length > 0) { + latestRecord = new ChangeSetRecord(changeset[ + Object.keys(changeset)[0] + ]); + } + + return (); + }) + }
); diff --git a/blueocean-dashboard/src/main/js/components/RunDetails.jsx b/blueocean-dashboard/src/main/js/components/RunDetails.jsx index a94d3e26..17bc35cb 100644 --- a/blueocean-dashboard/src/main/js/components/RunDetails.jsx +++ b/blueocean-dashboard/src/main/js/components/RunDetails.jsx @@ -3,7 +3,6 @@ import { ModalView, ModalBody, ModalHeader, - PipelineResult, PageTabs, TabLink, } from '@jenkins-cd/design-language'; @@ -23,6 +22,9 @@ import { buildRunDetailsUrl, } from '../util/UrlUtils'; +import { RunDetailsHeader } from './RunDetailsHeader'; +import { RunRecord } from './records'; + const { func, object, array, any, string } = PropTypes; class RunDetails extends Component { @@ -72,43 +74,29 @@ class RunDetails extends Component { return null; } - const { - router, - location, - params: { - organization, - branch, - runId, - pipeline: name, - }, - } = this.context; - - const baseUrl = buildRunDetailsUrl(organization, name, branch, runId); - - /* eslint-disable arrow-body-style */ - const currentRun = this.props.runs.filter((run) => { - return run.id === runId && - decodeURIComponent(run.pipeline) === branch; - })[0]; - - // deep-linking across RunDetails for different pipelines yields 'runs' data for the wrong pipeline + const { router, location, params } = this.context; + + const baseUrl = buildRunDetailsUrl(params.organization, params.pipeline, params.branch, params.runId); + + const foundRun = this.props.runs.find((run) => + run.id === params.runId && + decodeURIComponent(run.pipeline) === params.branch + ); + // deep-linking across RunDetails for different pipelines yields 'runs' data for the wrong pipeline // during initial render. when runs are refetched the screen will render again with 'currentRun' correctly set - if (!currentRun) { + if (!foundRun) { return null; } - currentRun.name = name; - - const status = currentRun.result === 'UNKNOWN' ? currentRun.state : currentRun.result; - + const currentRun = new RunRecord(foundRun); + + const status = currentRun.getComputedResult(); + const afterClose = () => { - const fallbackUrl = buildPipelineUrl(organization, name); - + const fallbackUrl = buildPipelineUrl(params.organization, params.pipeline); location.pathname = this.opener || fallbackUrl; - router.push(location); }; - return (
- this.navigateToOrganization()} onNameClick={() => this.navigateToPipeline()} onAuthorsClick={() => this.navigateToChanges()} diff --git a/blueocean-dashboard/src/main/js/components/RunDetailsHeader.jsx b/blueocean-dashboard/src/main/js/components/RunDetailsHeader.jsx new file mode 100644 index 00000000..6ad8e532 --- /dev/null +++ b/blueocean-dashboard/src/main/js/components/RunDetailsHeader.jsx @@ -0,0 +1,111 @@ +// @flow + +import React, { Component, PropTypes } from 'react'; +import { Icon } from 'react-material-icons-blue'; +import { ReadableDate } from '@jenkins-cd/design-language'; +import { LiveStatusIndicator } from '@jenkins-cd/design-language'; +import { TimeDuration } from '@jenkins-cd/design-language'; +import moment from 'moment'; + +const { object, func } = PropTypes; + +class RunDetailsHeader extends Component { + handleAuthorsClick() { + if (this.props.onAuthorsClick) { + this.props.onAuthorsClick(); + } + } + + handleOrganizationClick() { + if (this.props.onOrganizationClick) { + this.props.onOrganizationClick(); + } + } + + handleNameClick() { + if (this.props.onNameClick) { + this.props.onNameClick(); + } + } + + render() { + const { data: run } = this.props; + // Grab author from each change, run through a set for uniqueness + // FIXME-FLOW: Remove the ":any" cast after completion of https://github.com/facebook/flow/issues/1059 + const authors = [...(new Set(run.changeSet.map(change => change.author.fullName)):any)]; + const status = run.getComputedResult(); + const durationMillis = run.isRunning() ? + moment().diff(moment(run.startTime)) : run.durationInMillis; + return ( +
+
+ +
+
+

+ this.handleOrganizationClick()}>{run.organization} +  /  + this.handleNameClick()}>{run.pipeline} +   + #{run.id} +

+ +
+
+
+ + {decodeURIComponent(run.pipeline)} +
+ { run.commitId ? +
+ + + #{run.commitId.substring(0, 8)} + +
+ : null } + +
+
+
+ + +
+
+ + +
+
+
+
+
); + } +} + +RunDetailsHeader.propTypes = { + data: object.isRequired, + colors: object, + onOrganizationClick: func, + onNameClick: func, + onAuthorsClick: func, +}; + +export { RunDetailsHeader }; diff --git a/blueocean-dashboard/src/main/js/components/RunDetailsPipeline.jsx b/blueocean-dashboard/src/main/js/components/RunDetailsPipeline.jsx index f46bf838..42963d14 100644 --- a/blueocean-dashboard/src/main/js/components/RunDetailsPipeline.jsx +++ b/blueocean-dashboard/src/main/js/components/RunDetailsPipeline.jsx @@ -4,6 +4,7 @@ import Extensions from '@jenkins-cd/js-extensions'; import LogConsole from './LogConsole'; import * as sse from '@jenkins-cd/sse-gateway'; import { EmptyStateView } from '@jenkins-cd/design-language'; +import { Icon } from 'react-material-icons-blue'; import LogToolbar from './LogToolbar'; import Steps from './Steps'; @@ -23,6 +24,19 @@ import { calculateNode } from '../util/KaraokeHelper'; const { string, object, any, func } = PropTypes; +const queuedState = () => ( + +

+ + Waiting for run to start. +

+
+); + export class RunDetailsPipeline extends Component { constructor(props) { super(props); @@ -34,82 +48,43 @@ export class RunDetailsPipeline extends Component { } componentWillMount() { - const { fetchNodes, fetchLog, result, fetchSteps } = this.props; + const { fetchNodes, fetchLog, result } = this.props; this.mergedConfig = this.generateConfig(this.props); - // It should really be using capability using /rest/classes API - const supportsNode = result && result._class === 'io.jenkins.blueocean.rest.impl.pipeline.PipelineRunImpl'; - if (supportsNode) { - fetchNodes(this.mergedConfig); - } else { - // console.log('fetch the log directly') - const logGeneral = calculateRunLogURLObject(this.mergedConfig); - // fetchAll indicates whether we want all logs - const fetchAll = this.mergedConfig.fetchAll; - fetchLog({ ...logGeneral, fetchAll }); + if (!result.isQueued()) { + // It should really be using capability using /rest/classes API + const supportsNode = result && result._class === 'io.jenkins.blueocean.rest.impl.pipeline.PipelineRunImpl'; + if (supportsNode) { + fetchNodes(this.mergedConfig); + } else { + // console.log('fetch the log directly') + const logGeneral = calculateRunLogURLObject(this.mergedConfig); + // fetchAll indicates whether we want all logs + const fetchAll = this.mergedConfig.fetchAll; + fetchLog({ ...logGeneral, fetchAll }); + } } - // Listen for pipeline flow node events. - // We filter them only for steps and the end event all other we let pass - const onSseEvent = (event) => { - const jenkinsEvent = event.jenkins_event; - // we are using try/catch to throw an early out error - try { - if (event.pipeline_run_id !== this.props.result.id) { - // console.log('early out'); - throw new Error('exit'); - } - // we turn on refetch so we always fetch a new Node result - const refetch = true; - switch (jenkinsEvent) { - case 'pipeline_step': - { - // we are not using an early out for the events since we want to refresh the node if we finished - if (this.state.followAlong) { // if we do it means we want karaoke - // if the step_stage_id has changed we need to change the focus - if (event.pipeline_step_stage_id !== this.mergedConfig.node) { - // console.log('nodes fetching via sse triggered'); - delete this.mergedConfig.node; - fetchNodes({ ...this.mergedConfig, refetch }); - } else { - // console.log('only steps fetching via sse triggered'); - fetchSteps({ ...this.mergedConfig, refetch }); - } - } - break; - } - case 'pipeline_end': - { - // we always want to refresh if the run has finished - fetchNodes({ ...this.mergedConfig, refetch }); - break; - } - default: - { - // //console.log(event); - } - } - } catch (e) { - // we only ignore the exit error - if (e.message !== 'exit') { - throw e; - } - } - }; - - this.listener.sse = sse.subscribe('pipeline', onSseEvent); + this.listener.sse = sse.subscribe('pipeline', this._onSseEvent); } - + componentDidMount() { - // determine scroll area - const domNode = ReactDOM.findDOMNode(this.refs.scrollArea); - // add both listemer, one to the scroll area and another to the whole document - domNode.addEventListener('wheel', this.onScrollHandler, false); - document.addEventListener('keydown', this._handleKeys, false); + const { result } = this.props; + + if (!result.isQueued()) { + // determine scroll area + const domNode = ReactDOM.findDOMNode(this.refs.scrollArea); + // add both listemer, one to the scroll area and another to the whole document + domNode.addEventListener('wheel', this.onScrollHandler, false); + document.addEventListener('keydown', this._handleKeys, false); + } } componentWillReceiveProps(nextProps) { + if (this.props.result.isQueued()) { + return; + } const followAlong = this.state.followAlong; this.mergedConfig = this.generateConfig({ ...nextProps, followAlong }); @@ -158,13 +133,16 @@ 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); @@ -178,6 +156,56 @@ export class RunDetailsPipeline extends Component { this.setState({ followAlong: false }); } } + + // Listen for pipeline flow node events. + // We filter them only for steps and the end event all other we let pass + _onSseEvent(event) { + const { fetchNodes, fetchSteps } = this.props; + const jenkinsEvent = event.jenkins_event; + // we are using try/catch to throw an early out error + try { + if (event.pipeline_run_id !== this.props.result.id) { + // console.log('early out'); + throw new Error('exit'); + } + // we turn on refetch so we always fetch a new Node result + const refetch = true; + switch (jenkinsEvent) { + case 'pipeline_step': + { + // we are not using an early out for the events since we want to refresh the node if we finished + if (this.state.followAlong) { // if we do it means we want karaoke + // if the step_stage_id has changed we need to change the focus + if (event.pipeline_step_stage_id !== this.mergedConfig.node) { + // console.log('nodes fetching via sse triggered'); + delete this.mergedConfig.node; + fetchNodes({ ...this.mergedConfig, refetch }); + } else { + // console.log('only steps fetching via sse triggered'); + fetchSteps({ ...this.mergedConfig, refetch }); + } + } + break; + } + case 'pipeline_end': + { + // we always want to refresh if the run has finished + fetchNodes({ ...this.mergedConfig, refetch }); + break; + } + default: + { + // //console.log(event); + } + } + } catch (e) { + // we only ignore the exit error + if (e.message !== 'exit') { + throw e; + } + } + } + // we bail out on arrow_up key _handleKeys(event) { if (event.keyCode === 38 && this.state.followAlong) { @@ -186,14 +214,9 @@ export class RunDetailsPipeline extends Component { } generateConfig(props) { - const { - config = {}, - } = this.context; + const { config = {} } = this.context; const followAlong = this.state.followAlong; - const { - isMultiBranch, - params: { pipeline: name, branch, runId, node: nodeParam }, - } = props; + const { isMultiBranch, params } = props; const fetchAll = calculateFetchAll(props); // we would use default properties however the node can be null so no default properties will be triggered let { nodeReducer } = props; @@ -201,30 +224,31 @@ 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 = nodeParam || nodeReducer.id; + const node = params.node || nodeReducer.id; - const mergedConfig = { ...config, name, branch, runId, isMultiBranch, node, nodeReducer, followAlong, fetchAll }; - return mergedConfig; + // Merge config + return { + ...config, + name: params.pipeline, + branch: params.branch, + runId: params.runId, + isMultiBranch, + node, + nodeReducer, + followAlong, + fetchAll, + }; } render() { - const { - location, - router, - } = this.context; + const { location, router } = this.context; - const { - params: { - pipeline: name, branch, runId, - }, - isMultiBranch, steps, nodes, logs, result: resultMeta, - } = this.props; - - const { - result, - state, - } = resultMeta; - const resultRun = result === 'UNKNOWN' || !result ? state : result; + const { isMultiBranch, steps, nodes, logs, result: run, params } = this.props; + + if (run.isQueued()) { + return queuedState(); + } + const resultRun = run.isCompleted() ? run.state : run.result; const followAlong = this.state.followAlong; // in certain cases we want that the log component will scroll to the end of a log const scrollToBottom = @@ -303,8 +327,8 @@ export class RunDetailsPipeline extends Component { callback={afterClick} nodes={nodes[nodeKey].model} pipelineName={name} - branchName={isMultiBranch ? branch : undefined} - runId={runId} + branchName={isMultiBranch ? params.branch : undefined} + runId={run.id} /> } { shouldShowLogHeader && diff --git a/blueocean-dashboard/src/main/js/components/records.jsx b/blueocean-dashboard/src/main/js/components/records.jsx index 87c632bb..b15f06fe 100644 --- a/blueocean-dashboard/src/main/js/components/records.jsx +++ b/blueocean-dashboard/src/main/js/components/records.jsx @@ -45,7 +45,8 @@ export const ChangeSetRecord = Record({ timestamp: null, }); -export const ActivityRecord = Record({ +export class RunRecord extends Record({ + _class: null, changeSet: ChangeSetRecord, durationInMillis: null, enQueueTime: null, @@ -60,7 +61,24 @@ export const ActivityRecord = Record({ state: null, type: null, commitId: null, -}); +}) { + isQueued() { + return this.state === 'QUEUED'; + } + + // We have a result + isCompleted() { + return this.result !== 'UNKNOWN'; + } + + isRunning() { + return this.state === 'RUNNING'; + } + + getComputedResult() { + return this.isCompleted() ? this.result : this.state; + } +} export const PullRequestRecord = Record({ pullRequest: { @@ -74,7 +92,7 @@ export const PullRequestRecord = Record({ export const RunsRecord = Record({ _class: null, _links: null, - latestRun: ActivityRecord, + latestRun: RunRecord, name: null, weatherScore: 0, pullRequest: PullRequestRecord, diff --git a/blueocean-dashboard/src/main/js/redux/actions.js b/blueocean-dashboard/src/main/js/redux/actions.js index 771b997f..adbe6879 100644 --- a/blueocean-dashboard/src/main/js/redux/actions.js +++ b/blueocean-dashboard/src/main/js/redux/actions.js @@ -6,6 +6,32 @@ import UrlConfig from '../config'; import { getNodesInformation } from '../util/logDisplayHelper'; import { calculateStepsBaseUrl, calculateLogUrl, calculateNodeBaseUrl } from '../util/UrlUtils'; +/** + * This function maps a queue item into a run instancce. + * + * We do this because the api returns us queued items as well + * as runs and its easier to deal with them if they are modeled + * as the same thing. If the raw data is needed if can be fetched + * from _item. + */ +function _mapQueueToPsuedoRun(run) { + if (run._class === 'io.jenkins.blueocean.service.embedded.rest.QueueItemImpl') { + return { + id: String(run.expectedBuildNumber), + state: 'QUEUED', + pipeline: run.pipeline, + type: 'QueuedItem', + result: 'UNKNOWN', + job_run_queueId: run.id, + enQueueTime: run.queuedTime, + organization: run.organization, + changeSet: [], + _item: run, + }; + } + return run; +} + // main actin logic export const ACTION_TYPES = keymirror({ UPDATE_MESSAGES: null, @@ -57,7 +83,7 @@ export const actionHandlers = { return state.set('currentRuns', null); }, [ACTION_TYPES.SET_CURRENT_RUN_DATA](state, { payload }): State { - return state.set('currentRuns', payload); + return state.set('currentRuns', payload.map((run) => _mapQueueToPsuedoRun(run))); }, [ACTION_TYPES.SET_NODE](state, { payload }): State { return state.set('node', { ...payload }); @@ -69,7 +95,8 @@ export const actionHandlers = { }, [ACTION_TYPES.SET_RUNS_DATA](state, { payload, id }): State { const runs = { ...state.runs } || {}; - runs[id] = payload; + + runs[id] = payload.map(run => _mapQueueToPsuedoRun(run)); return state.set('runs', runs); }, [ACTION_TYPES.CLEAR_CURRENT_BRANCHES_DATA](state) { @@ -558,7 +585,7 @@ export const actions = { fetchRunsIfNeeded(config) { return (dispatch) => { const baseUrl = `${config.getAppURLBase()}/rest/organizations/jenkins` + - `/pipelines/${config.pipeline}/runs/`; + `/pipelines/${config.pipeline}/activities/`; return dispatch(actions.fetchIfNeeded({ url: baseUrl, id: config.pipeline, diff --git a/blueocean-personalization/package.json b/blueocean-personalization/package.json index 0921983b..d219e8e3 100644 --- a/blueocean-personalization/package.json +++ b/blueocean-personalization/package.json @@ -34,7 +34,7 @@ "react-addons-test-utils": "15.0.1" }, "dependencies": { - "@jenkins-cd/design-language": "0.0.63", + "@jenkins-cd/design-language": "0.0.67", "@jenkins-cd/js-extensions": "0.0.20", "@jenkins-cd/js-modules": "0.0.5", "@jenkins-cd/sse-gateway": "0.0.7", diff --git a/blueocean-pipeline-api-impl/src/main/java/io/jenkins/blueocean/rest/impl/pipeline/MultiBranchPipelineImpl.java b/blueocean-pipeline-api-impl/src/main/java/io/jenkins/blueocean/rest/impl/pipeline/MultiBranchPipelineImpl.java index 6647121a..61b25a20 100644 --- a/blueocean-pipeline-api-impl/src/main/java/io/jenkins/blueocean/rest/impl/pipeline/MultiBranchPipelineImpl.java +++ b/blueocean-pipeline-api-impl/src/main/java/io/jenkins/blueocean/rest/impl/pipeline/MultiBranchPipelineImpl.java @@ -1,5 +1,7 @@ package io.jenkins.blueocean.rest.impl.pipeline; +import com.google.common.collect.Iterators; +import com.google.common.collect.Lists; import hudson.Extension; import hudson.model.Item; import hudson.model.Job; @@ -10,17 +12,7 @@ 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.BlueActionProxy; -import io.jenkins.blueocean.rest.model.BlueFavorite; -import io.jenkins.blueocean.rest.model.BlueFavoriteAction; -import io.jenkins.blueocean.rest.model.BlueMultiBranchPipeline; -import io.jenkins.blueocean.rest.model.BluePipeline; -import io.jenkins.blueocean.rest.model.BluePipelineContainer; -import io.jenkins.blueocean.rest.model.BlueQueueContainer; -import io.jenkins.blueocean.rest.model.BlueQueueItem; -import io.jenkins.blueocean.rest.model.BlueRun; -import io.jenkins.blueocean.rest.model.BlueRunContainer; -import io.jenkins.blueocean.rest.model.Resource; +import io.jenkins.blueocean.rest.model.*; import io.jenkins.blueocean.service.embedded.rest.BlueFavoriteResolver; import io.jenkins.blueocean.service.embedded.rest.BluePipelineFactory; import io.jenkins.blueocean.service.embedded.rest.FavoriteImpl; @@ -30,6 +22,7 @@ import io.jenkins.blueocean.service.embedded.util.FavoriteUtil; import jenkins.branch.MultiBranchProject; import jenkins.scm.api.SCMHead; import jenkins.scm.api.actions.ChangeRequestAction; +import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.json.JsonBody; import java.util.ArrayList; @@ -357,4 +350,10 @@ public class MultiBranchPipelineImpl extends BlueMultiBranchPipeline { return null; } } + + @Exported(inline = true) + @Navigable + public Container getActivities() { + return Containers.fromResource(getLink(), Lists.newArrayList(Iterators.concat(getQueue().iterator(), getRuns().iterator()))); + } } diff --git a/blueocean-pipeline-api-impl/src/main/java/io/jenkins/blueocean/rest/impl/pipeline/MultiBranchPipelineQueueContainer.java b/blueocean-pipeline-api-impl/src/main/java/io/jenkins/blueocean/rest/impl/pipeline/MultiBranchPipelineQueueContainer.java index 224432b6..9cb8d298 100644 --- a/blueocean-pipeline-api-impl/src/main/java/io/jenkins/blueocean/rest/impl/pipeline/MultiBranchPipelineQueueContainer.java +++ b/blueocean-pipeline-api-impl/src/main/java/io/jenkins/blueocean/rest/impl/pipeline/MultiBranchPipelineQueueContainer.java @@ -1,22 +1,17 @@ package io.jenkins.blueocean.rest.impl.pipeline; +import com.google.common.collect.Lists; import hudson.model.Job; import hudson.model.Queue; import io.jenkins.blueocean.commons.ServiceException; import io.jenkins.blueocean.rest.hal.Link; -import io.jenkins.blueocean.rest.model.BluePipeline; import io.jenkins.blueocean.rest.model.BlueQueueContainer; import io.jenkins.blueocean.rest.model.BlueQueueItem; import io.jenkins.blueocean.service.embedded.rest.QueueContainerImpl; -import io.jenkins.blueocean.service.embedded.rest.QueueItemImpl; import jenkins.model.Jenkins; -import org.jenkinsci.plugins.workflow.support.steps.ExecutorStepExecution; -import java.util.ArrayList; -import java.util.HashMap; import java.util.Iterator; import java.util.List; -import java.util.Map; /** * @author Vivek Pandey @@ -34,21 +29,11 @@ public class MultiBranchPipelineQueueContainer extends BlueQueueContainer { @Override public BlueQueueItem get(String name) { try { - Queue.Item item = Jenkins.getActiveInstance().getQueue().getItem(Long.parseLong(name)); - if(item != null){ - BranchImpl pipeline = (BranchImpl) multiBranchPipeline.getBranches().get(item.task.getOwnerTask().getName()); - if(pipeline != null) { - - if(item.task instanceof ExecutorStepExecution.PlaceholderTask) { - ExecutorStepExecution.PlaceholderTask task = (ExecutorStepExecution.PlaceholderTask) item.task; - if(task.run() == null){ - return QueueContainerImpl.getQueuedItem(item, pipeline.job); - }else{ - return new QueueItemImpl(item, item.task.getOwnerTask().getName(), task.run().getNumber(), - self.rel(String.valueOf(item.getId()))); - } - } - + Queue.Item item = Jenkins.getInstance().getQueue().getItem(Long.parseLong(name)); + if(item != null && item.task instanceof Job){ + Job job = ((Job) item.task); + if(job.getParent() != null && job.getParent().getFullName().equals(multiBranchPipeline.mbp.getFullName())) { + return QueueContainerImpl.getQueuedItem(item, job); } } }catch (NumberFormatException e){ @@ -64,43 +49,12 @@ public class MultiBranchPipelineQueueContainer extends BlueQueueContainer { @Override public Iterator iterator() { - final List items = new ArrayList<>(); - Map> 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 its = queueMap.get(task.getOwnerTask().getName()); - if(its == null){ - its = new ArrayList<>(); - queueMap.put(ownerTaskName,its); - } - its.add(item); + List queueItems = Lists.newArrayList(); + for(Object o: multiBranchPipeline.mbp.getItems()) { + if(o instanceof Job) { + queueItems.addAll(QueueContainerImpl.getQueuedItems((Job)o)); } } - for(final BluePipeline p:multiBranchPipeline.getBranches()){ - Job job = ((BranchImpl)p).job; - List its = queueMap.get(job.getName()); - if(its == null || its.isEmpty()){ - continue; - } - int count=0; - for(Queue.Item item:its){ - ExecutorStepExecution.PlaceholderTask task = (ExecutorStepExecution.PlaceholderTask) item.task; - if(task != null){ - int runNumber; - if(task.run() == null){ - runNumber = job.getNextBuildNumber() + count; - count++; - }else{ - runNumber = task.run().getNumber(); - } - items.add(new QueueItemImpl(item,p.getName(), - runNumber, self.rel(String.valueOf(item.getId())))); - } - } - } - return items.iterator(); + return queueItems.iterator(); } } diff --git a/blueocean-pipeline-api-impl/src/test/java/io/jenkins/blueocean/rest/impl/pipeline/MultiBranchTest.java b/blueocean-pipeline-api-impl/src/test/java/io/jenkins/blueocean/rest/impl/pipeline/MultiBranchTest.java index a60d1ae2..021a36ed 100644 --- a/blueocean-pipeline-api-impl/src/test/java/io/jenkins/blueocean/rest/impl/pipeline/MultiBranchTest.java +++ b/blueocean-pipeline-api-impl/src/test/java/io/jenkins/blueocean/rest/impl/pipeline/MultiBranchTest.java @@ -3,6 +3,7 @@ package io.jenkins.blueocean.rest.impl.pipeline; import com.google.common.collect.ImmutableMap; import hudson.Util; import hudson.model.FreeStyleProject; +import hudson.model.Queue; import hudson.plugins.favorite.user.FavoriteUserProperty; import hudson.plugins.git.util.BuildData; import hudson.scm.ChangeLogSet; @@ -14,8 +15,10 @@ import jenkins.branch.DefaultBranchPropertyStrategy; import jenkins.plugins.git.GitSCMSource; import jenkins.scm.api.SCMSource; import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.SystemUtils; import org.hamcrest.collection.IsArrayContainingInAnyOrder; import org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval; +import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; import org.jenkinsci.plugins.workflow.job.WorkflowJob; import org.jenkinsci.plugins.workflow.job.WorkflowRun; import org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject; @@ -50,6 +53,9 @@ public class MultiBranchTest extends PipelineBaseTest { @Rule public GitSampleRepoRule sampleRepo = new GitSampleRepoRule(); + @Rule + public GitSampleRepoRule sampleRepo1 = new GitSampleRepoRule(); + private final String[] branches={"master", "feature%2Fux-1", "feature2"}; @@ -723,4 +729,56 @@ public class MultiBranchTest extends PipelineBaseTest { return p; } + //Disabled test for now as I can't get it to work. Tested manually. + //@Test + public void getPipelineJobActivities() throws Exception { + WorkflowMultiBranchProject mp = j.jenkins.createProject(WorkflowMultiBranchProject.class, "p"); + sampleRepo1.init(); + sampleRepo1.write("Jenkinsfile", "stage 'build'\n "+"node {echo 'Building'}\n"+ + "stage 'test'\nnode { echo 'Testing'}\n" + + "sleep 10000 \n"+ + "stage 'deploy'\nnode { echo 'Deploying'}\n" + ); + sampleRepo1.write("file", "initial content"); + sampleRepo1.git("add", "Jenkinsfile"); + sampleRepo1.git("commit", "--all", "--message=flow"); + + //create feature branch + sampleRepo1.git("checkout", "-b", "abc"); + sampleRepo1.write("Jenkinsfile", "echo \"branch=${env.BRANCH_NAME}\"; "+"node {" + + " stage ('Build'); " + + " echo ('Building'); " + + " stage ('Test'); sleep 10000; " + + " echo ('Testing'); " + + " stage ('Deploy'); " + + " echo ('Deploying'); " + + "}"); + ScriptApproval.get().approveSignature("method java.lang.String toUpperCase"); + sampleRepo1.write("file", "subsequent content1"); + sampleRepo1.git("commit", "--all", "--message=tweaked1"); + + + mp.getSourcesList().add(new BranchSource(new GitSCMSource(null, sampleRepo1.toString(), "", "*", "", false), + new DefaultBranchPropertyStrategy(new BranchProperty[0]))); + for (SCMSource source : mp.getSCMSources()) { + assertEquals(mp, source.getOwner()); + } + scheduleAndFindBranchProject(mp); + + for(WorkflowJob job : mp.getItems()) { + Queue.Item item = job.getQueueItem(); + if(item != null ) { + item.getFuture().waitForStart(); + } + job.setConcurrentBuild(false); + job.scheduleBuild2(0); + job.scheduleBuild2(0); + } + List l = request().get("/organizations/jenkins/pipelines/p/activities").build(List.class); + + Assert.assertEquals(4, l.size()); + Assert.assertEquals("io.jenkins.blueocean.service.embedded.rest.QueueItemImpl", ((Map) l.get(0)).get("_class")); + Assert.assertEquals("io.jenkins.blueocean.rest.impl.pipeline.PipelineRunImpl", ((Map) l.get(2)).get("_class")); + } + } diff --git a/blueocean-pipeline-api-impl/src/test/java/io/jenkins/blueocean/rest/impl/pipeline/PipelineApiTest.java b/blueocean-pipeline-api-impl/src/test/java/io/jenkins/blueocean/rest/impl/pipeline/PipelineApiTest.java index d2294773..342163e1 100644 --- a/blueocean-pipeline-api-impl/src/test/java/io/jenkins/blueocean/rest/impl/pipeline/PipelineApiTest.java +++ b/blueocean-pipeline-api-impl/src/test/java/io/jenkins/blueocean/rest/impl/pipeline/PipelineApiTest.java @@ -182,4 +182,29 @@ public class PipelineApiTest extends PipelineBaseTest { Assert.assertTrue(size > 0); } + @Test + public void getPipelineJobActivities() throws Exception { + WorkflowJob job1 = j.jenkins.createProject(WorkflowJob.class, "pipeline1"); + job1.setDefinition(new CpsFlowDefinition("" + + "node {" + + " stage ('Build1'); " + + " echo ('Building'); " + + " stage ('Test1'); " + + " sleep 10000 " + + " echo ('Testing'); " + + "}")); + + job1.setConcurrentBuild(false); + + WorkflowRun r = job1.scheduleBuild2(0).waitForStart(); + job1.scheduleBuild2(0); + + + List l = request().get("/organizations/jenkins/pipelines/pipeline1/activities").build(List.class); + + Assert.assertEquals(2, l.size()); + Assert.assertEquals("io.jenkins.blueocean.service.embedded.rest.QueueItemImpl", ((Map) l.get(0)).get("_class")); + Assert.assertEquals("io.jenkins.blueocean.rest.impl.pipeline.PipelineRunImpl", ((Map) l.get(1)).get("_class")); + } + } diff --git a/blueocean-rest-impl/src/main/java/io/jenkins/blueocean/service/embedded/rest/PipelineImpl.java b/blueocean-rest-impl/src/main/java/io/jenkins/blueocean/service/embedded/rest/PipelineImpl.java index 63afcd3a..10497c45 100644 --- a/blueocean-rest-impl/src/main/java/io/jenkins/blueocean/service/embedded/rest/PipelineImpl.java +++ b/blueocean-rest-impl/src/main/java/io/jenkins/blueocean/service/embedded/rest/PipelineImpl.java @@ -1,5 +1,7 @@ package io.jenkins.blueocean.service.embedded.rest; +import com.google.common.collect.Iterators; +import com.google.common.collect.Lists; import hudson.Extension; import hudson.model.Action; import hudson.model.Item; @@ -8,7 +10,6 @@ import io.jenkins.blueocean.commons.ServiceException; import io.jenkins.blueocean.rest.Navigable; import io.jenkins.blueocean.rest.Reachable; import io.jenkins.blueocean.rest.hal.Link; -import io.jenkins.blueocean.service.embedded.util.FavoriteUtil; import io.jenkins.blueocean.rest.model.BlueActionProxy; import io.jenkins.blueocean.rest.model.BlueFavorite; import io.jenkins.blueocean.rest.model.BlueFavoriteAction; @@ -16,9 +17,13 @@ import io.jenkins.blueocean.rest.model.BluePipeline; import io.jenkins.blueocean.rest.model.BlueQueueContainer; import io.jenkins.blueocean.rest.model.BlueRun; import io.jenkins.blueocean.rest.model.BlueRunContainer; +import io.jenkins.blueocean.rest.model.Container; +import io.jenkins.blueocean.rest.model.Containers; import io.jenkins.blueocean.rest.model.Resource; +import io.jenkins.blueocean.service.embedded.util.FavoriteUtil; import org.kohsuke.stapler.Stapler; import org.kohsuke.stapler.WebMethod; +import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.json.JsonBody; import org.kohsuke.stapler.verb.DELETE; @@ -96,6 +101,7 @@ public class PipelineImpl extends BluePipeline { return new QueueContainerImpl(this); } + @WebMethod(name="") @DELETE public void delete() throws IOException, InterruptedException { job.delete(); @@ -179,4 +185,9 @@ public class PipelineImpl extends BluePipeline { } + @Exported(inline = true) + @Navigable + public Container getActivities() { + return Containers.fromResource(getLink(),Lists.newArrayList(Iterators.concat(getQueue().iterator(), getRuns().iterator()))); + } } diff --git a/blueocean-rest-impl/src/main/java/io/jenkins/blueocean/service/embedded/rest/QueueItemImpl.java b/blueocean-rest-impl/src/main/java/io/jenkins/blueocean/service/embedded/rest/QueueItemImpl.java index fae3d0a5..9152d6b4 100644 --- a/blueocean-rest-impl/src/main/java/io/jenkins/blueocean/service/embedded/rest/QueueItemImpl.java +++ b/blueocean-rest-impl/src/main/java/io/jenkins/blueocean/service/embedded/rest/QueueItemImpl.java @@ -34,6 +34,11 @@ public class QueueItemImpl extends BlueQueueItem { return Long.toString(item.getId()); } + @Override + public String getOrganization() { + return OrganizationImpl.INSTANCE.getName(); + } + @Override public String getPipeline() { return pipelineName; diff --git a/blueocean-rest/src/main/java/io/jenkins/blueocean/rest/model/BlueQueueItem.java b/blueocean-rest/src/main/java/io/jenkins/blueocean/rest/model/BlueQueueItem.java index df9f3b38..62337272 100644 --- a/blueocean-rest/src/main/java/io/jenkins/blueocean/rest/model/BlueQueueItem.java +++ b/blueocean-rest/src/main/java/io/jenkins/blueocean/rest/model/BlueQueueItem.java @@ -26,6 +26,8 @@ public abstract class BlueQueueItem extends Resource { @Exported public abstract String getId(); + @Exported + public abstract String getOrganization(); /** * * @return pipeline this queued item belongs too diff --git a/blueocean-web/package.json b/blueocean-web/package.json index a36a2117..58124d74 100644 --- a/blueocean-web/package.json +++ b/blueocean-web/package.json @@ -25,7 +25,7 @@ "zombie": "^4.2.1" }, "dependencies": { - "@jenkins-cd/design-language": "0.0.65", + "@jenkins-cd/design-language": "0.0.67", "@jenkins-cd/js-extensions": "0.0.20", "@jenkins-cd/js-modules": "0.0.5", "history": "2.0.2",