Skip to content

Commit 1efd7e2

Browse files
timjajanfaracik
andauthored
Use custom rendering for input step (#866)
Co-authored-by: Jan Faracik <[email protected]>
1 parent 69ea6bb commit 1efd7e2

File tree

12 files changed

+221
-93
lines changed

12 files changed

+221
-93
lines changed

src/main/frontend/common/RestClient.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ export interface RunStatus {
99
complete: boolean;
1010
}
1111

12+
export interface InputStep {
13+
message: string;
14+
cancel: string;
15+
id: string;
16+
ok: string;
17+
parameters: boolean;
18+
}
19+
1220
/**
1321
* StageInfo is the input, in the form of an Array<StageInfo> of the top-level stages of a pipeline
1422
*/
@@ -17,6 +25,7 @@ export interface StepInfo {
1725
title: string;
1826
state: Result;
1927
completePercent: number;
28+
inputStep?: InputStep;
2029
id: string;
2130
type: string;
2231
stageId: string;

src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLine.tsx

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,12 @@ export interface ConsoleLineProps {
1111
heightCallback: (height: number) => void;
1212
}
1313

14-
declare global {
15-
interface Window {
16-
Behaviour: any;
17-
}
18-
}
19-
2014
// Console output line
2115
export const ConsoleLine = memo(function ConsoleLine(props: ConsoleLineProps) {
2216
const ref = useRef<HTMLDivElement>(null);
2317
useEffect(() => {
2418
const height = ref.current ? ref.current.getBoundingClientRect().height : 0;
2519
props.heightCallback(height);
26-
27-
// apply any behaviour selectors to the new content, e.g. for input step
28-
window.Behaviour.applySubtree(
29-
document.getElementById(`${props.stepId}-${props.lineNumber}`),
30-
);
3120
}, []);
3221

3322
return (

src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLogCard.tsx

Lines changed: 62 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
StepInfo,
1919
StepLogBufferInfo,
2020
} from "./PipelineConsoleModel.tsx";
21+
import InputStep from "./steps/InputStep.tsx";
2122

2223
const ConsoleLogStream = lazy(() => import("./ConsoleLogStream.tsx"));
2324

@@ -41,41 +42,13 @@ export default function ConsoleLogCard(props: ConsoleLogCardProps) {
4142
props.onStepToggle(props.step.id);
4243
};
4344

44-
const showMoreLogs = () => {
45-
let startByte = props.stepBuffer.startByte - LOG_FETCH_SIZE;
46-
if (startByte < 0) startByte = 0;
47-
props.onMoreConsoleClick(props.step.id, startByte);
48-
};
49-
50-
const getTruncatedLogWarning = () => {
51-
if (props.stepBuffer.lines && props.stepBuffer.startByte > 0) {
52-
return (
53-
<button
54-
onClick={showMoreLogs}
55-
className={
56-
"pgv-show-more-logs jenkins-button jenkins-!-warning-color"
57-
}
58-
>
59-
There’s more to see - {prettySizeString(props.stepBuffer.startByte)}{" "}
60-
of logs hidden
61-
</button>
62-
);
63-
}
64-
return undefined;
65-
};
66-
67-
const prettySizeString = (size: number) => {
68-
const kib = 1024;
69-
const mib = 1024 * 1024;
70-
const gib = 1024 * 1024 * 1024;
71-
if (size < kib) return `${size}B`;
72-
if (size < mib) return `${(size / kib).toFixed(2)}KiB`;
73-
if (size < gib) return `${(size / mib).toFixed(2)}MiB`;
74-
return `${(size / gib).toFixed(2)}GiB`;
75-
};
76-
7745
const messages = useMessages();
7846

47+
const inputStep = props.step.inputStep;
48+
if (inputStep && !inputStep.parameters) {
49+
return <InputStep {...props} />;
50+
}
51+
7952
return (
8053
<div className={"pgv-step-detail-group"} key={`step-card-${props.step.id}`}>
8154
<div
@@ -162,22 +135,67 @@ export default function ConsoleLogCard(props: ConsoleLogCardProps) {
162135
</div>
163136

164137
{props.isExpanded && (
165-
<div style={{ paddingTop: "0.5rem" }}>
166-
{getTruncatedLogWarning()}
167-
<Suspense>
168-
<ConsoleLogStream
169-
logBuffer={props.stepBuffer}
170-
onMoreConsoleClick={props.onMoreConsoleClick}
171-
step={props.step}
172-
maxHeightScale={0.65}
173-
/>
174-
</Suspense>
175-
</div>
138+
<ConsoleLogBody
139+
step={props.step}
140+
stepBuffer={props.stepBuffer}
141+
onMoreConsoleClick={props.onMoreConsoleClick}
142+
isExpanded={false}
143+
onStepToggle={props.onStepToggle}
144+
/>
176145
)}
177146
</div>
178147
);
179148
}
180149

150+
function ConsoleLogBody(props: ConsoleLogCardProps) {
151+
const prettySizeString = (size: number) => {
152+
const kib = 1024;
153+
const mib = 1024 * 1024;
154+
const gib = 1024 * 1024 * 1024;
155+
if (size < kib) return `${size}B`;
156+
if (size < mib) return `${(size / kib).toFixed(2)}KiB`;
157+
if (size < gib) return `${(size / mib).toFixed(2)}MiB`;
158+
return `${(size / gib).toFixed(2)}GiB`;
159+
};
160+
161+
const showMoreLogs = () => {
162+
let startByte = props.stepBuffer.startByte - LOG_FETCH_SIZE;
163+
if (startByte < 0) startByte = 0;
164+
props.onMoreConsoleClick(props.step.id, startByte);
165+
};
166+
167+
const getTruncatedLogWarning = () => {
168+
if (props.stepBuffer.lines && props.stepBuffer.startByte > 0) {
169+
return (
170+
<button
171+
onClick={showMoreLogs}
172+
className={
173+
"pgv-show-more-logs jenkins-button jenkins-!-warning-color"
174+
}
175+
>
176+
There’s more to see - {prettySizeString(props.stepBuffer.startByte)}{" "}
177+
of logs hidden
178+
</button>
179+
);
180+
}
181+
return undefined;
182+
};
183+
184+
return (
185+
<div style={{ paddingTop: "0.5rem" }}>
186+
{getTruncatedLogWarning()}
187+
<Suspense>
188+
<ConsoleLogStream
189+
logBuffer={props.stepBuffer}
190+
onMoreConsoleClick={props.onMoreConsoleClick}
191+
step={props.step}
192+
maxHeightScale={0.65}
193+
/>
194+
</Suspense>
195+
</div>
196+
);
197+
}
198+
181199
export type ConsoleLogCardProps = {
182200
step: StepInfo;
183201
stepBuffer: StepLogBufferInfo;

src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLogStream.spec.tsx

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,6 @@ import {
1212
StepLogBufferInfo,
1313
} from "./PipelineConsoleModel.tsx";
1414

15-
beforeAll(() => {
16-
window.Behaviour = {
17-
applySubtree: vi.fn(),
18-
};
19-
});
20-
21-
afterAll(() => {
22-
delete window.Behaviour;
23-
});
24-
2515
const TestComponent = (props: ConsoleLogStreamProps) => {
2616
return (
2717
<div id="test-parent">

src/main/frontend/pipeline-console-view/pipeline-console/main/console-log-card.scss

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,38 @@ a.console-line-number {
138138
}
139139
}
140140

141+
.pgv-input-step {
142+
position: relative;
143+
z-index: 0;
144+
padding: 0.875rem;
145+
margin-bottom: -0.375rem;
146+
margin-left: -0.375rem;
147+
margin-right: -0.375rem;
148+
// TODO - var fallback can removed after baseline is moved >= 2.496
149+
border-top: var(
150+
--jenkins-border,
151+
2px solid color-mix(in srgb, var(--text-color-secondary) 10%, transparent)
152+
);
153+
background-color: color-mix(in srgb, var(--accent-color) 2.5%, transparent);
154+
border-bottom-left-radius: 0.375rem;
155+
border-bottom-right-radius: 0.375rem;
156+
157+
&:first-of-type {
158+
margin-top: -0.375rem;
159+
border-top: none;
160+
border-radius: 0.375rem;
161+
}
162+
163+
.jenkins-button {
164+
min-width: 7.5rem;
165+
}
166+
167+
&__controls {
168+
padding-left: 2.275rem;
169+
margin-top: 0.75rem;
170+
}
171+
}
172+
141173
div.console-output-line {
142174
position: relative;
143175
display: flex;
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import StatusIcon from "../../../../common/components/status-icon.tsx";
2+
import { ConsoleLogCardProps } from "../ConsoleLogCard.tsx";
3+
4+
declare global {
5+
interface Window {
6+
crumb: Crumb;
7+
}
8+
9+
interface Crumb {
10+
wrap: (headers: Record<string, string>) => Record<string, string>;
11+
}
12+
}
13+
14+
export default function InputStep(props: ConsoleLogCardProps) {
15+
const inputStep = props.step.inputStep!;
16+
function handler(id: string, action: string) {
17+
fetch(`../input/${id}/${action}`, {
18+
method: "POST",
19+
headers: window.crumb.wrap({}),
20+
})
21+
.then((res) => {
22+
if (!res.ok) {
23+
console.error(res);
24+
}
25+
return true;
26+
})
27+
.catch((err) => {
28+
console.error(err);
29+
});
30+
}
31+
32+
const ok = () => {
33+
return handler(inputStep.id, "proceedEmpty");
34+
};
35+
36+
const abort = () => {
37+
return handler(inputStep.id, "abort");
38+
};
39+
40+
return (
41+
<div className="pgv-input-step">
42+
<div className="pgv-step-detail-header__content">
43+
<StatusIcon
44+
status={props.step.state}
45+
percentage={props.step.completePercent}
46+
/>
47+
<span>{inputStep.message}</span>
48+
</div>
49+
<div
50+
className={
51+
"jenkins-buttons-row jenkins-buttons-row--equal-width pgv-input-step__controls"
52+
}
53+
>
54+
<button
55+
onClick={ok}
56+
className={"jenkins-button jenkins-button--primary"}
57+
>
58+
{inputStep.ok}
59+
</button>
60+
<button onClick={abort} className={"jenkins-button"}>
61+
{inputStep.cancel}
62+
</button>
63+
</div>
64+
</div>
65+
);
66+
}

src/main/java/io/jenkins/plugins/pipelinegraphview/treescanner/PipelineNodeTreeScanner.java

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,28 @@
66
import io.jenkins.plugins.pipelinegraphview.utils.FlowNodeWrapper;
77
import io.jenkins.plugins.pipelinegraphview.utils.NodeRunStatus;
88
import io.jenkins.plugins.pipelinegraphview.utils.PipelineNodeUtil;
9+
import java.io.IOException;
910
import java.util.ArrayList;
1011
import java.util.Collections;
1112
import java.util.LinkedHashMap;
1213
import java.util.List;
1314
import java.util.Map;
1415
import java.util.Set;
16+
import java.util.concurrent.TimeoutException;
1517
import java.util.stream.Collectors;
1618
import org.jenkinsci.plugins.pipeline.modeldefinition.actions.ExecutionModelAction;
1719
import org.jenkinsci.plugins.workflow.actions.ErrorAction;
20+
import org.jenkinsci.plugins.workflow.cps.nodes.StepAtomNode;
1821
import org.jenkinsci.plugins.workflow.flow.FlowExecution;
22+
import org.jenkinsci.plugins.workflow.graph.AtomNode;
1923
import org.jenkinsci.plugins.workflow.graph.BlockEndNode;
2024
import org.jenkinsci.plugins.workflow.graph.BlockStartNode;
2125
import org.jenkinsci.plugins.workflow.graph.FlowNode;
2226
import org.jenkinsci.plugins.workflow.graphanalysis.DepthFirstScanner;
2327
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
28+
import org.jenkinsci.plugins.workflow.support.steps.input.InputAction;
29+
import org.jenkinsci.plugins.workflow.support.steps.input.InputStep;
30+
import org.jenkinsci.plugins.workflow.support.steps.input.InputStepExecution;
2431
import org.slf4j.Logger;
2532
import org.slf4j.LoggerFactory;
2633

@@ -167,6 +174,7 @@ private static class GraphBuilder {
167174
// FlowNodeWrapper rootStage = null;
168175

169176
private final Logger logger = LoggerFactory.getLogger(GraphBuilder.class);
177+
private final InputAction inputAction;
170178
private boolean isDebugEnabled = logger.isDebugEnabled();
171179

172180
/*
@@ -181,6 +189,7 @@ public GraphBuilder(
181189
this.nodeMap = nodeMap;
182190
this.relationships = relationships;
183191
this.run = run;
192+
this.inputAction = run.getAction(InputAction.class);
184193
this.execution = execution;
185194
buildGraph();
186195
}
@@ -491,7 +500,24 @@ private void assignParent(@NonNull FlowNodeWrapper wrappedNode, @CheckForNull Fl
491500
timing = relationship.getTimingInfo(this.run);
492501
status = relationship.getStatus(this.run);
493502
}
494-
return new FlowNodeWrapper(node, status, timing, this.run);
503+
504+
InputStep inputStep = null;
505+
if (node instanceof AtomNode atomNode
506+
&& PipelineNodeUtil.isPausedForInputStep((StepAtomNode) atomNode, inputAction)) {
507+
try {
508+
for (InputStepExecution execution : inputAction.getExecutions()) {
509+
FlowNode theNode = execution.getContext().get(FlowNode.class);
510+
if (theNode != null && theNode.equals(atomNode)) {
511+
inputStep = execution.getInput();
512+
break;
513+
}
514+
}
515+
} catch (IOException | InterruptedException | TimeoutException e) {
516+
logger.error("Error getting FlowNode from execution context: {}", e.getMessage(), e);
517+
}
518+
}
519+
520+
return new FlowNodeWrapper(node, status, timing, inputStep, this.run);
495521
}
496522
}
497523
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package io.jenkins.plugins.pipelinegraphview.utils;
2+
3+
public record PipelineInputStep(String message, String cancel, String id, String ok, boolean parameters) {}

0 commit comments

Comments
 (0)