diff --git a/src/main/frontend/common/RestClient.tsx b/src/main/frontend/common/RestClient.tsx index 0fdc74d2e..dcd22e2e5 100644 --- a/src/main/frontend/common/RestClient.tsx +++ b/src/main/frontend/common/RestClient.tsx @@ -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 of the top-level stages of a pipeline */ @@ -17,6 +25,7 @@ export interface StepInfo { title: string; state: Result; completePercent: number; + inputStep?: InputStep; id: string; type: string; stageId: string; diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLine.tsx b/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLine.tsx index d8d619021..3d2a7ec52 100644 --- a/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLine.tsx +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLine.tsx @@ -11,23 +11,12 @@ export interface ConsoleLineProps { heightCallback: (height: number) => void; } -declare global { - interface Window { - Behaviour: any; - } -} - // Console output line export const ConsoleLine = memo(function ConsoleLine(props: ConsoleLineProps) { const ref = useRef(null); 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 ( diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLogCard.tsx b/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLogCard.tsx index 520ecd291..9c82dae12 100644 --- a/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLogCard.tsx +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLogCard.tsx @@ -18,6 +18,7 @@ import { StepInfo, StepLogBufferInfo, } from "./PipelineConsoleModel.tsx"; +import InputStep from "./steps/InputStep.tsx"; const ConsoleLogStream = lazy(() => import("./ConsoleLogStream.tsx")); @@ -41,41 +42,13 @@ 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 ( - - ); - } - 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(); + const inputStep = props.step.inputStep; + if (inputStep && !inputStep.parameters) { + return ; + } + return (
{props.isExpanded && ( -
- {getTruncatedLogWarning()} - - - -
+ )}
); } +function ConsoleLogBody(props: ConsoleLogCardProps) { + 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 ( + + ); + } + return undefined; + }; + + return ( +
+ {getTruncatedLogWarning()} + + + +
+ ); +} + export type ConsoleLogCardProps = { step: StepInfo; stepBuffer: StepLogBufferInfo; diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLogStream.spec.tsx b/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLogStream.spec.tsx index f08e7e751..00cf11cd4 100644 --- a/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLogStream.spec.tsx +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLogStream.spec.tsx @@ -12,16 +12,6 @@ import { StepLogBufferInfo, } from "./PipelineConsoleModel.tsx"; -beforeAll(() => { - window.Behaviour = { - applySubtree: vi.fn(), - }; -}); - -afterAll(() => { - delete window.Behaviour; -}); - const TestComponent = (props: ConsoleLogStreamProps) => { return (
diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/console-log-card.scss b/src/main/frontend/pipeline-console-view/pipeline-console/main/console-log-card.scss index a2e8c3905..5315c0826 100644 --- a/src/main/frontend/pipeline-console-view/pipeline-console/main/console-log-card.scss +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/console-log-card.scss @@ -138,6 +138,38 @@ a.console-line-number { } } +.pgv-input-step { + position: relative; + z-index: 0; + padding: 0.875rem; + margin-bottom: -0.375rem; + margin-left: -0.375rem; + margin-right: -0.375rem; + // TODO - var fallback can removed after baseline is moved >= 2.496 + border-top: var( + --jenkins-border, + 2px solid color-mix(in srgb, var(--text-color-secondary) 10%, transparent) + ); + background-color: color-mix(in srgb, var(--accent-color) 2.5%, transparent); + border-bottom-left-radius: 0.375rem; + border-bottom-right-radius: 0.375rem; + + &:first-of-type { + margin-top: -0.375rem; + border-top: none; + border-radius: 0.375rem; + } + + .jenkins-button { + min-width: 7.5rem; + } + + &__controls { + padding-left: 2.275rem; + margin-top: 0.75rem; + } +} + div.console-output-line { position: relative; display: flex; diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/steps/InputStep.tsx b/src/main/frontend/pipeline-console-view/pipeline-console/main/steps/InputStep.tsx new file mode 100644 index 000000000..47a46743d --- /dev/null +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/steps/InputStep.tsx @@ -0,0 +1,66 @@ +import StatusIcon from "../../../../common/components/status-icon.tsx"; +import { ConsoleLogCardProps } from "../ConsoleLogCard.tsx"; + +declare global { + interface Window { + crumb: Crumb; + } + + interface Crumb { + wrap: (headers: Record) => Record; + } +} + +export default function InputStep(props: ConsoleLogCardProps) { + const inputStep = props.step.inputStep!; + function handler(id: string, action: string) { + fetch(`../input/${id}/${action}`, { + method: "POST", + headers: window.crumb.wrap({}), + }) + .then((res) => { + if (!res.ok) { + console.error(res); + } + return true; + }) + .catch((err) => { + console.error(err); + }); + } + + const ok = () => { + return handler(inputStep.id, "proceedEmpty"); + }; + + const abort = () => { + return handler(inputStep.id, "abort"); + }; + + return ( +
+
+ + {inputStep.message} +
+
+ + +
+
+ ); +} diff --git a/src/main/java/io/jenkins/plugins/pipelinegraphview/treescanner/PipelineNodeTreeScanner.java b/src/main/java/io/jenkins/plugins/pipelinegraphview/treescanner/PipelineNodeTreeScanner.java index f53469a9e..2c8324bc7 100644 --- a/src/main/java/io/jenkins/plugins/pipelinegraphview/treescanner/PipelineNodeTreeScanner.java +++ b/src/main/java/io/jenkins/plugins/pipelinegraphview/treescanner/PipelineNodeTreeScanner.java @@ -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; @@ -167,6 +174,7 @@ private static class GraphBuilder { // FlowNodeWrapper rootStage = null; private final Logger logger = LoggerFactory.getLogger(GraphBuilder.class); + private final InputAction inputAction; private boolean isDebugEnabled = logger.isDebugEnabled(); /* @@ -181,6 +189,7 @@ public GraphBuilder( this.nodeMap = nodeMap; this.relationships = relationships; this.run = run; + this.inputAction = run.getAction(InputAction.class); this.execution = execution; buildGraph(); } @@ -491,7 +500,24 @@ private void assignParent(@NonNull FlowNodeWrapper wrappedNode, @CheckForNull Fl 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()) { + FlowNode theNode = execution.getContext().get(FlowNode.class); + if (theNode != null && theNode.equals(atomNode)) { + inputStep = execution.getInput(); + break; + } + } + } catch (IOException | InterruptedException | TimeoutException e) { + logger.error("Error getting FlowNode from execution context: {}", e.getMessage(), e); + } + } + + return new FlowNodeWrapper(node, status, timing, inputStep, this.run); } } } diff --git a/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineInputStep.java b/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineInputStep.java new file mode 100644 index 000000000..ab6f7d414 --- /dev/null +++ b/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineInputStep.java @@ -0,0 +1,3 @@ +package io.jenkins.plugins.pipelinegraphview.utils; + +public record PipelineInputStep(String message, String cancel, String id, String ok, boolean parameters) {} diff --git a/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStep.java b/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStep.java index 3fb3d2c01..4fc36b840 100644 --- a/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStep.java +++ b/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStep.java @@ -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, @@ -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; + } } diff --git a/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStepApi.java b/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStepApi.java index a1f033122..a46e528d8 100644 --- a/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStepApi.java +++ b/src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStepApi.java @@ -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; @@ -58,11 +59,24 @@ private List parseSteps(List 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", ""); diff --git a/src/main/resources/io/jenkins/plugins/pipelinegraphview/consoleview/PipelineConsoleViewAction/index.jelly b/src/main/resources/io/jenkins/plugins/pipelinegraphview/consoleview/PipelineConsoleViewAction/index.jelly index cc485eca3..190264660 100644 --- a/src/main/resources/io/jenkins/plugins/pipelinegraphview/consoleview/PipelineConsoleViewAction/index.jelly +++ b/src/main/resources/io/jenkins/plugins/pipelinegraphview/consoleview/PipelineConsoleViewAction/index.jelly @@ -5,7 +5,6 @@ - diff --git a/src/test/java/io/jenkins/plugins/pipelinegraphview/consoleview/PipelineConsoleViewInputTest.java b/src/test/java/io/jenkins/plugins/pipelinegraphview/consoleview/PipelineConsoleViewInputTest.java index a046154d6..ef9ee5656 100644 --- a/src/test/java/io/jenkins/plugins/pipelinegraphview/consoleview/PipelineConsoleViewInputTest.java +++ b/src/test/java/io/jenkins/plugins/pipelinegraphview/consoleview/PipelineConsoleViewInputTest.java @@ -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 @@ -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(); @@ -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)); - } }