Skip to content
9 changes: 9 additions & 0 deletions src/main/frontend/common/RestClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ export interface RunStatus {
complete: boolean;
}

export interface InputStep {
message: string;
cancel: string;
id: string;
ok: string;
parameters: boolean;
}

/**
* StageInfo is the input, in the form of an Array<StageInfo> of the top-level stages of a pipeline
*/
Expand All @@ -17,6 +25,7 @@ export interface StepInfo {
title: string;
state: Result;
completePercent: number;
inputStep?: InputStep;
id: string;
type: string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are the values of type? Could it be set to input instead of a new field?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its STEP, it could be but I need a number of fields anyway

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I missed deleting this comment after a further look through the rest of the code

stageId: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,6 @@ export const ConsoleLine = memo(function ConsoleLine(props: ConsoleLineProps) {
useEffect(() => {
const height = ref.current ? ref.current.getBoundingClientRect().height : 0;
props.heightCallback(height);

// apply any behaviour selectors to the new content, e.g. for input step
window.Behaviour.applySubtree(
document.getElementById(`${props.stepId}-${props.lineNumber}`),
);
}, []);

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,39 +41,6 @@ export default function ConsoleLogCard(props: ConsoleLogCardProps) {
props.onStepToggle(props.step.id);
};

const showMoreLogs = () => {
let startByte = props.stepBuffer.startByte - LOG_FETCH_SIZE;
if (startByte < 0) startByte = 0;
props.onMoreConsoleClick(props.step.id, startByte);
};

const getTruncatedLogWarning = () => {
if (props.stepBuffer.lines && props.stepBuffer.startByte > 0) {
return (
<button
onClick={showMoreLogs}
className={
"pgv-show-more-logs jenkins-button jenkins-!-warning-color"
}
>
There’s more to see - {prettySizeString(props.stepBuffer.startByte)}{" "}
of logs hidden
</button>
);
}
return undefined;
};

const prettySizeString = (size: number) => {
const kib = 1024;
const mib = 1024 * 1024;
const gib = 1024 * 1024 * 1024;
if (size < kib) return `${size}B`;
if (size < mib) return `${(size / kib).toFixed(2)}KiB`;
if (size < gib) return `${(size / mib).toFixed(2)}MiB`;
return `${(size / gib).toFixed(2)}GiB`;
};

const messages = useMessages();

return (
Expand Down Expand Up @@ -162,18 +129,117 @@ export default function ConsoleLogCard(props: ConsoleLogCardProps) {
</div>

{props.isExpanded && (
<ConsoleLogBody
step={props.step}
stepBuffer={props.stepBuffer}
onMoreConsoleClick={props.onMoreConsoleClick}
isExpanded={false}
onStepToggle={props.onStepToggle}
/>
)}
</div>
);
}

function ConsoleLogBody(props: ConsoleLogCardProps) {
const inputStep = props.step.inputStep;
if (inputStep && !inputStep.parameters) {
function handler(id: string, action: string) {
fetch(`../input/${id}/${action}`, {
method: "POST",
headers: (window as any).crumb.wrap({}),
})
.then((res) => {
console.log(res);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To remove

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that better now?

return true;
})
.catch((err) => {
console.error(err);
});
}

const ok = () => {
return handler(inputStep.id, "proceedEmpty");
};

const abort = () => {
return handler(inputStep.id, "abort");
};

return (
<>
<div style={{ paddingTop: "0.5rem" }}>
{getTruncatedLogWarning()}
<Suspense>
<ConsoleLogStream
logBuffer={props.stepBuffer}
onMoreConsoleClick={props.onMoreConsoleClick}
step={props.step}
maxHeightScale={0.65}
/>
</Suspense>
<div className={"console-output-line"}>
<a style={{ width: "30px" }} className={"console-line-number"} />
<p style={{ fontWeight: "var(--font-bold-weight)" }}>
{inputStep.message}
</p>
</div>
</div>
)}
<div className={"console-output-line"}>
<a style={{ width: "30px" }} className={"console-line-number"} />
<div
className={"jenkins-buttons-row jenkins-buttons-row--equal-width"}
>
<button
onClick={ok}
className={"jenkins-button jenkins-button--primary"}
>
{inputStep.ok}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know much about the plugin but are there alternatives to just abort/proceed? I feel like I remember in a previous project we had a text input at some point

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes those are parameters, the existing behaviour provides a regular link that takes you to the page with parameters. (the one you made prettier recently)

image

Copy link
Member Author

@timja timja Jun 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(we could do the parameters page inline like in blueocean but that's a bigger change and may have the risk of re-renders occurring breaking things when new polls happen)

</button>
<button onClick={abort} className={"jenkins-button"}>
{inputStep.cancel}
</button>
</div>
</div>
</>
);
}

const prettySizeString = (size: number) => {
const kib = 1024;
const mib = 1024 * 1024;
const gib = 1024 * 1024 * 1024;
if (size < kib) return `${size}B`;
if (size < mib) return `${(size / kib).toFixed(2)}KiB`;
if (size < gib) return `${(size / mib).toFixed(2)}MiB`;
return `${(size / gib).toFixed(2)}GiB`;
};

const showMoreLogs = () => {
let startByte = props.stepBuffer.startByte - LOG_FETCH_SIZE;
if (startByte < 0) startByte = 0;
props.onMoreConsoleClick(props.step.id, startByte);
};

const getTruncatedLogWarning = () => {
if (props.stepBuffer.lines && props.stepBuffer.startByte > 0) {
return (
<button
onClick={showMoreLogs}
className={
"pgv-show-more-logs jenkins-button jenkins-!-warning-color"
}
>
There’s more to see - {prettySizeString(props.stepBuffer.startByte)}{" "}
of logs hidden
</button>
);
}
return undefined;
};

return (
<div style={{ paddingTop: "0.5rem" }}>
{getTruncatedLogWarning()}
<Suspense>
<ConsoleLogStream
logBuffer={props.stepBuffer}
onMoreConsoleClick={props.onMoreConsoleClick}
step={props.step}
maxHeightScale={0.65}
/>
</Suspense>
</div>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,28 @@
import io.jenkins.plugins.pipelinegraphview.utils.FlowNodeWrapper;
import io.jenkins.plugins.pipelinegraphview.utils.NodeRunStatus;
import io.jenkins.plugins.pipelinegraphview.utils.PipelineNodeUtil;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import org.jenkinsci.plugins.pipeline.modeldefinition.actions.ExecutionModelAction;
import org.jenkinsci.plugins.workflow.actions.ErrorAction;
import org.jenkinsci.plugins.workflow.cps.nodes.StepAtomNode;
import org.jenkinsci.plugins.workflow.flow.FlowExecution;
import org.jenkinsci.plugins.workflow.graph.AtomNode;
import org.jenkinsci.plugins.workflow.graph.BlockEndNode;
import org.jenkinsci.plugins.workflow.graph.BlockStartNode;
import org.jenkinsci.plugins.workflow.graph.FlowNode;
import org.jenkinsci.plugins.workflow.graphanalysis.DepthFirstScanner;
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
import org.jenkinsci.plugins.workflow.support.steps.input.InputAction;
import org.jenkinsci.plugins.workflow.support.steps.input.InputStep;
import org.jenkinsci.plugins.workflow.support.steps.input.InputStepExecution;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -167,6 +174,7 @@
// FlowNodeWrapper rootStage = null;

private final Logger logger = LoggerFactory.getLogger(GraphBuilder.class);
private final InputAction inputAction;
private boolean isDebugEnabled = logger.isDebugEnabled();

/*
Expand All @@ -181,6 +189,7 @@
this.nodeMap = nodeMap;
this.relationships = relationships;
this.run = run;
this.inputAction = run.getAction(InputAction.class);
this.execution = execution;
buildGraph();
}
Expand Down Expand Up @@ -491,7 +500,24 @@
timing = relationship.getTimingInfo(this.run);
status = relationship.getStatus(this.run);
}
return new FlowNodeWrapper(node, status, timing, this.run);

InputStep inputStep = null;
if (node instanceof AtomNode atomNode
&& PipelineNodeUtil.isPausedForInputStep((StepAtomNode) atomNode, inputAction)) {
try {
for (InputStepExecution execution : inputAction.getExecutions()) {

Check warning on line 508 in src/main/java/io/jenkins/plugins/pipelinegraphview/treescanner/PipelineNodeTreeScanner.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 508 is only partially covered, one branch is missing
FlowNode theNode = execution.getContext().get(FlowNode.class);
if (theNode != null && theNode.equals(atomNode)) {

Check warning on line 510 in src/main/java/io/jenkins/plugins/pipelinegraphview/treescanner/PipelineNodeTreeScanner.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 510 is only partially covered, 2 branches are missing
inputStep = execution.getInput();
break;
}
}
} catch (IOException | InterruptedException | TimeoutException e) {
logger.error("Error getting FlowNode from execution context: {}", e.getMessage(), e);

Check warning on line 516 in src/main/java/io/jenkins/plugins/pipelinegraphview/treescanner/PipelineNodeTreeScanner.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 514-516 are not covered by tests
}
}

return new FlowNodeWrapper(node, status, timing, inputStep, this.run);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package io.jenkins.plugins.pipelinegraphview.utils;

public record PipelineInputStep(String message, String cancel, String id, String ok, boolean parameters) {}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jackson with our current config can't serialise the parameters object (if not more) so just map what we need

Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import io.jenkins.plugins.pipelinegraphview.analysis.TimingInfo;

public class PipelineStep extends AbstractPipelineNode {
private String stageId;
private final String stageId;
private final PipelineInputStep inputStep;

public PipelineStep(
String id,
Expand All @@ -12,12 +13,18 @@ public PipelineStep(
String type,
String title,
String stageId,
PipelineInputStep inputStep,
TimingInfo timingInfo) {
super(id, name, state, type, title, timingInfo);
this.stageId = stageId;
this.inputStep = inputStep;
}

public String getStageId() {
return stageId;
}

public PipelineInputStep getInputStep() {
return inputStep;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.util.Map;
import java.util.stream.Collectors;
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
import org.jenkinsci.plugins.workflow.support.steps.input.InputStep;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -58,11 +59,24 @@ private List<PipelineStep> parseSteps(List<FlowNodeWrapper> stepNodes, String st
flowNodeWrapper.getType().name(),
title,
stageId,
mapInputStep(flowNodeWrapper.getInputStep()),
flowNodeWrapper.getTiming());
})
.collect(Collectors.toList());
}

private PipelineInputStep mapInputStep(InputStep inputStep) {
if (inputStep == null) {
return null;
}
return new PipelineInputStep(
inputStep.getMessage(),
inputStep.getCancel(),
inputStep.getId(),
inputStep.getOk(),
!inputStep.getParameters().isEmpty());
}

static String cleanTextContent(String text) {
// strips off all ANSI color codes
text = text.replaceAll("\\e\\[(\\d+[;:]?)+m", "");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
<l:layout title="${it.buildDisplayName} - ${it.fullProjectDisplayName}">
<l:main-panel>
<link rel="stylesheet" href="${resURL}/plugin/pipeline-graph-view/js/style.css" type="text/css" />
<j:out value="${h.generateConsoleAnnotationScriptAndStylesheet()}"/>

<j:set var="controls">
<j:if test="${it.buildInProgress}">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,9 @@
import io.jenkins.plugins.casc.misc.JenkinsConfiguredWithCodeRule;
import io.jenkins.plugins.casc.misc.junit.jupiter.WithJenkinsConfiguredWithCode;
import io.jenkins.plugins.pipelinegraphview.playwright.PipelineJobPage;
import io.jenkins.plugins.pipelinegraphview.playwright.PipelineOverviewPage;
import io.jenkins.plugins.pipelinegraphview.playwright.PlaywrightConfig;
import io.jenkins.plugins.pipelinegraphview.utils.TestUtils;
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

@WithJenkinsConfiguredWithCode
Expand All @@ -38,8 +36,6 @@ void inputWithParametersSucceeds(Page p, JenkinsConfiguredWithCodeRule j) throws

@Test
@ConfiguredWithCode("../configure-appearance.yml")
// Times out on slower systems as it doesn't click the input quick enough
@Disabled("https://github.com/jenkinsci/pipeline-graph-view-plugin/issues/568")
void inputSucceeds(Page p, JenkinsConfiguredWithCodeRule j) throws Exception {
WorkflowRun run =
TestUtils.createAndRunJobNoWait(j, "input", "input.jenkinsfile").waitForStart();
Expand All @@ -54,25 +50,4 @@ void inputSucceeds(Page p, JenkinsConfiguredWithCodeRule j) throws Exception {

j.assertBuildStatus(Result.SUCCESS, j.waitForCompletion(run));
}

@Test
@ConfiguredWithCode("../configure-appearance.yml")
@Disabled("https://github.com/jenkinsci/pipeline-graph-view-plugin/issues/568")
void inputSucceedsWithDelay(Page p, JenkinsConfiguredWithCodeRule j) throws Exception {
WorkflowRun run =
TestUtils.createAndRunJobNoWait(j, "input", "input.jenkinsfile").waitForStart();

PipelineOverviewPage pipelineOverviewPage = new PipelineJobPage(p, run.getParent())
.goTo()
.hasBuilds(1)
.nthBuild(0)
.goToBuild()
.goToPipelineOverview();

// Fails if you don't click proceed immediately currently
p.waitForTimeout(1000D);

pipelineOverviewPage.clickProceed();
j.assertBuildStatus(Result.SUCCESS, j.waitForCompletion(run));
}
}