diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/StageNodeLink.tsx b/src/main/frontend/pipeline-console-view/pipeline-console/main/StageNodeLink.tsx
new file mode 100644
index 000000000..6c5a6b02f
--- /dev/null
+++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/StageNodeLink.tsx
@@ -0,0 +1,22 @@
+import React from "react";
+
+export interface StageNodeLinkProps {
+ agent: string;
+}
+
+function getAgentUrl(name: string) {
+ // Wrap built-in in brackets
+ const id = name == "built-in" ? "(built-in)" : name;
+ const rootPath = document.head.dataset.rooturl
+ return `${rootPath}/computer/${id}/`;
+}
+
+const StageNodeLink = ({agent}: StageNodeLinkProps) => {
+ const agentName = agent == "built-in" ? "Jenkins" : agent;
+ const href = getAgentUrl(agent);
+ return <>
+ Running on {agentName}
+ >
+};
+
+export default StageNodeLink;
diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/StageView.tsx b/src/main/frontend/pipeline-console-view/pipeline-console/main/StageView.tsx
index da4e2c506..d1033e696 100644
--- a/src/main/frontend/pipeline-console-view/pipeline-console/main/StageView.tsx
+++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/StageView.tsx
@@ -6,6 +6,7 @@ import ScheduleIcon from "@mui/icons-material/Schedule";
import TimerIcon from "@mui/icons-material/Timer";
import InfoIcon from "@mui/icons-material/Info";
import LinkIcon from "@mui/icons-material/Link";
+import ComputerIcon from "@mui/icons-material/Computer";
import {
StepInfo,
@@ -14,6 +15,7 @@ import {
LOG_FETCH_SIZE,
} from "./PipelineConsoleModel";
import { ConsoleLogCard } from "./ConsoleLogCard";
+import StageNodeLink from "./StageNodeLink";
export interface StageSummaryProps {
stage: StageInfo;
@@ -81,6 +83,22 @@ const StageSummary = (props: StageSummaryProps) => (
{props.stage.state}
+ {props.stage.agent && (
+
+
+
+
+
+
+ )}
{props.failedSteps.map((value: StepInfo) => {
console.debug(`Found failed step ${value}`);
return (
diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/TestData.tsx b/src/main/frontend/pipeline-console-view/pipeline-console/main/TestData.tsx
index 66bc0942c..f7e7d1ce0 100644
--- a/src/main/frontend/pipeline-console-view/pipeline-console/main/TestData.tsx
+++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/TestData.tsx
@@ -1,5 +1,5 @@
import { Result, StageType, StageInfo, StepInfo } from "./PipelineConsoleModel";
-export const defaultStagesList = [
+export const defaultStagesList: StageInfo[] = [
{
name: "Stage A",
title: "",
@@ -11,6 +11,7 @@ export const defaultStagesList = [
pauseDurationMillis: "",
startTimeMillis: "",
totalDurationMillis: "",
+ agent: "built-in",
},
{
name: "Stage B",
@@ -23,6 +24,7 @@ export const defaultStagesList = [
pauseDurationMillis: "",
startTimeMillis: "",
totalDurationMillis: "",
+ agent: "not-built-in",
},
{
name: "Parent C",
@@ -43,11 +45,13 @@ export const defaultStagesList = [
pauseDurationMillis: "",
startTimeMillis: "",
totalDurationMillis: "",
+ agent: "not-built-in",
},
],
pauseDurationMillis: "",
startTimeMillis: "",
totalDurationMillis: "",
+ agent: "built-in",
},
];
diff --git a/src/main/frontend/pipeline-graph-view/pipeline-graph/main/PipelineGraphLayout.spec.ts b/src/main/frontend/pipeline-graph-view/pipeline-graph/main/PipelineGraphLayout.spec.ts
index 44f150214..c17adf588 100644
--- a/src/main/frontend/pipeline-graph-view/pipeline-graph/main/PipelineGraphLayout.spec.ts
+++ b/src/main/frontend/pipeline-graph-view/pipeline-graph/main/PipelineGraphLayout.spec.ts
@@ -14,6 +14,7 @@ describe("PipelineGraphLayout", () => {
pauseDurationMillis: "",
startTimeMillis: "",
totalDurationMillis: "",
+ agent: "built-in",
};
const makeStage = (
diff --git a/src/main/frontend/pipeline-graph-view/pipeline-graph/main/PipelineGraphModel.tsx b/src/main/frontend/pipeline-graph-view/pipeline-graph/main/PipelineGraphModel.tsx
index 3140846fa..7ae5b288b 100644
--- a/src/main/frontend/pipeline-graph-view/pipeline-graph/main/PipelineGraphModel.tsx
+++ b/src/main/frontend/pipeline-graph-view/pipeline-graph/main/PipelineGraphModel.tsx
@@ -62,6 +62,7 @@ export interface StageInfo {
pauseDurationMillis: string;
startTimeMillis: string;
totalDurationMillis: string;
+ agent: string;
}
interface BaseNodeInfo {
diff --git a/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineGraphApi.java b/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineGraphApi.java
index 80fb90d1a..7f2539e09 100644
--- a/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineGraphApi.java
+++ b/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineGraphApi.java
@@ -8,18 +8,16 @@
import hudson.model.Queue;
import io.jenkins.plugins.pipelinegraphview.treescanner.PipelineNodeGraphAdapter;
import io.jenkins.plugins.pipelinegraphview.utils.legacy.PipelineNodeGraphVisitor;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
+import java.io.IOException;
+import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import java.util.function.Function;
import java.util.stream.Collectors;
+import org.jenkinsci.plugins.workflow.actions.WorkspaceAction;
import org.jenkinsci.plugins.workflow.flow.FlowExecution;
import org.jenkinsci.plugins.workflow.graph.FlowNode;
+import org.jenkinsci.plugins.workflow.graphanalysis.DepthFirstScanner;
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -70,7 +68,8 @@ private List getPipelineNodes(PipelineGraphBuilderApi bui
flowNodeWrapper.getType().name(),
flowNodeWrapper.getDisplayName(), // TODO blue ocean uses timing information: "Passed in 0s"
flowNodeWrapper.isSynthetic(),
- flowNodeWrapper.getTiming());
+ flowNodeWrapper.getTiming(),
+ getStageNode(flowNodeWrapper));
})
.collect(Collectors.toList());
}
@@ -197,7 +196,7 @@ private PipelineGraph createShallowTree(PipelineGraphBuilderApi builder) {
stageToChildrenMap.put(stage.getId(), new ArrayList<>());
topLevelStageIds.add(stage.getId());
}
- } catch (java.io.IOException ex) {
+ } catch (IOException ex) {
logger.error("Caught a "
+ ex.getClass().getSimpleName()
+ " when trying to find parent of stage '"
@@ -233,6 +232,49 @@ private List getAncestors(PipelineStageInternal stage, Map getChildren() {
public boolean isSynthetic() {
return synthetic;
}
+
+ public String getAgent() {
+ return agent;
+ }
}
diff --git a/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStageInternal.java b/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStageInternal.java
index 7e3aa83ef..99ba5f185 100644
--- a/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStageInternal.java
+++ b/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStageInternal.java
@@ -19,6 +19,7 @@ public class PipelineStageInternal {
private boolean sequential;
private boolean synthetic;
private TimingInfo timingInfo;
+ private String agent;
public PipelineStageInternal(
String id,
@@ -29,7 +30,8 @@ public PipelineStageInternal(
String type,
String title,
boolean synthetic,
- TimingInfo times) {
+ TimingInfo times,
+ String agent) {
this.id = id;
this.name = name;
this.parents = parents;
@@ -39,6 +41,7 @@ public PipelineStageInternal(
this.title = title;
this.synthetic = synthetic;
this.timingInfo = times;
+ this.agent = agent;
}
public boolean isSequential() {
@@ -121,6 +124,14 @@ public void setSynthetic(boolean synthetic) {
this.synthetic = synthetic;
}
+ public String getAgent() {
+ return agent;
+ }
+
+ public void setAgent(String aAgent) {
+ this.agent = aAgent;
+ }
+
public PipelineStage toPipelineStage(List children) {
return new PipelineStage(
id,
@@ -134,6 +145,7 @@ public PipelineStage toPipelineStage(List children) {
nextSibling != null ? nextSibling.toPipelineStage(Collections.emptyList()) : null,
sequential,
synthetic,
- timingInfo);
+ timingInfo,
+ agent);
}
}
diff --git a/src/test/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineGraphApiTest.java b/src/test/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineGraphApiTest.java
index b82e8aaa3..f8849c135 100644
--- a/src/test/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineGraphApiTest.java
+++ b/src/test/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineGraphApiTest.java
@@ -4,7 +4,9 @@
import static org.hamcrest.Matchers.*;
import hudson.model.Result;
+import hudson.model.labels.LabelAtom;
import hudson.model.queue.QueueTaskFuture;
+import hudson.slaves.DumbSlave;
import io.jenkins.plugins.pipelinegraphview.treescanner.NodeRelationshipFinder;
import io.jenkins.plugins.pipelinegraphview.treescanner.PipelineNodeGraphAdapter;
import io.jenkins.plugins.pipelinegraphview.treescanner.PipelineNodeTreeScanner;
@@ -408,4 +410,76 @@ public void gh_358_parallelStagesMarkedAsSkipped() throws Exception {
equalTo(
"foo{success},first-parallel{failure}[bar{skipped},baz{failure}],second-parallel{skipped},Post Actions{success}"));
}
+
+ @Test
+ public void getAgentForSingleStagePipeline() throws Exception {
+ WorkflowRun run = TestUtils.createAndRunJob(
+ j, "getAgentForSingleStagePipeline", "singleStagePipeline.jenkinsfile", Result.SUCCESS);
+
+ List stages = new PipelineGraphApi(run).createTree().getStages();
+
+ assertThat(stages.size(), equalTo(1));
+ assertThat(stages.get(0).getAgent(), equalTo("built-in"));
+ }
+
+ @Test
+ public void getAgentForSingleStagePipelineWithExternalAgent() throws Exception {
+ var testingLabel = new LabelAtom("external");
+ DumbSlave agent = j.createSlave(testingLabel);
+ j.waitOnline(agent);
+
+ WorkflowRun run = TestUtils.createAndRunJob(
+ j,
+ "getAgentForSingleStagePipelineWithExternalAgent",
+ "singleStagePipelineWithExternalAgent.jenkinsfile",
+ Result.SUCCESS);
+
+ List stages = new PipelineGraphApi(run).createTree().getStages();
+
+ assertThat(stages.size(), equalTo(1));
+ assertThat(stages.get(0).getAgent(), equalTo(agent.getNodeName()));
+ }
+
+ @Test
+ public void getAgentForParallelPipelineWithExternalAgent() throws Exception {
+ var testingLabel = new LabelAtom("external");
+ DumbSlave agent = j.createSlave(testingLabel);
+ j.waitOnline(agent);
+
+ WorkflowRun run = TestUtils.createAndRunJob(
+ j,
+ "getAgentForParallelPipelineWithExternalAgent",
+ "parallelPipelineWithExternalAgent.jenkinsfile",
+ Result.SUCCESS);
+
+ List stages = new PipelineGraphApi(run).createTree().getStages();
+
+ // Parallel pipeline structure:
+ // name: Parallel, type: STAGE
+ // name: Parallel, type: PARALLEL_BLOCK
+
+ // name: Builtin, type: PARALLEL
+ // name: Stage : Start, type: STEPS_BLOCK
+ // name: Builtin, type: STAGE
+ // name: Allocate node : Start, type: STEPS_BLOCK
+
+ assertThat(stages.size(), equalTo(1));
+ assertThat(stages.get(0).getType(), equalTo("STAGE"));
+ assertThat(stages.get(0).getName(), equalTo("Parallel"));
+ assertThat(stages.get(0).getAgent(), equalTo(null));
+
+ List children = stages.get(0).getChildren();
+
+ assertThat(children.size(), equalTo(2));
+
+ PipelineStage builtinStage = children.get(0);
+ assertThat(builtinStage.getType(), equalTo("PARALLEL"));
+ assertThat(builtinStage.getName(), equalTo("Builtin"));
+ assertThat(builtinStage.getAgent(), equalTo("built-in"));
+
+ PipelineStage externalStage = children.get(1);
+ assertThat(externalStage.getType(), equalTo("PARALLEL"));
+ assertThat(externalStage.getName(), equalTo("External"));
+ assertThat(externalStage.getAgent(), equalTo(agent.getNodeName()));
+ }
}
diff --git a/src/test/resources/io/jenkins/plugins/pipelinegraphview/utils/parallelPipelineWithExternalAgent.jenkinsfile b/src/test/resources/io/jenkins/plugins/pipelinegraphview/utils/parallelPipelineWithExternalAgent.jenkinsfile
new file mode 100644
index 000000000..5acc3e7bc
--- /dev/null
+++ b/src/test/resources/io/jenkins/plugins/pipelinegraphview/utils/parallelPipelineWithExternalAgent.jenkinsfile
@@ -0,0 +1,21 @@
+pipeline {
+ agent none
+ stages {
+ stage('Parallel') {
+ parallel {
+ stage('Builtin') {
+ agent { label 'built-in' }
+ steps {
+ echo "Hello, from home"
+ }
+ }
+ stage('External') {
+ agent { label 'external' }
+ steps {
+ echo "Hello, from far away"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/test/resources/io/jenkins/plugins/pipelinegraphview/utils/singleStagePipeline.jenkinsfile b/src/test/resources/io/jenkins/plugins/pipelinegraphview/utils/singleStagePipeline.jenkinsfile
new file mode 100644
index 000000000..b3bcb5501
--- /dev/null
+++ b/src/test/resources/io/jenkins/plugins/pipelinegraphview/utils/singleStagePipeline.jenkinsfile
@@ -0,0 +1,10 @@
+pipeline {
+ agent any
+ stages {
+ stage('Hello') {
+ steps {
+ echo "Hello, world"
+ }
+ }
+ }
+}
diff --git a/src/test/resources/io/jenkins/plugins/pipelinegraphview/utils/singleStagePipelineWithExternalAgent.jenkinsfile b/src/test/resources/io/jenkins/plugins/pipelinegraphview/utils/singleStagePipelineWithExternalAgent.jenkinsfile
new file mode 100644
index 000000000..a477fe488
--- /dev/null
+++ b/src/test/resources/io/jenkins/plugins/pipelinegraphview/utils/singleStagePipelineWithExternalAgent.jenkinsfile
@@ -0,0 +1,11 @@
+pipeline {
+ agent any
+ stages {
+ stage('Hello') {
+ agent { label 'external' }
+ steps {
+ echo "Hello, world"
+ }
+ }
+ }
+}