Skip to content

Commit 1cf0f01

Browse files
stuartrowetimja
andauthored
Add pause/resume button to the pipeline overview page (#1345)
Co-authored-by: Tim Jacomb <timjacomb1@gmail.com>
1 parent f15977b commit 1cf0f01

8 files changed

Lines changed: 401 additions & 13 deletions

File tree

src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/PipelineConsoleViewAction.java

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import jenkins.model.Tab;
3636
import net.sf.json.JSONObject;
3737
import org.jenkinsci.plugins.pipeline.modeldefinition.actions.RestartDeclarativePipelineAction;
38+
import org.jenkinsci.plugins.workflow.cps.CpsFlowExecution;
3839
import org.jenkinsci.plugins.workflow.cps.replay.ReplayAction;
3940
import org.jenkinsci.plugins.workflow.flow.FlowExecution;
4041
import org.jenkinsci.plugins.workflow.graph.FlowNode;
@@ -374,6 +375,97 @@ public HttpResponse doCancel() {
374375
return HttpResponses.errorJSON(message);
375376
}
376377

378+
/**
379+
* Handles the pause request.
380+
*/
381+
@RequirePOST
382+
@JavaScriptMethod
383+
public HttpResponse doPause() {
384+
run.checkPermission(getCancelPermission());
385+
386+
if (!run.isBuilding()) {
387+
return HttpResponses.errorJSON(Messages.run_isFinished());
388+
}
389+
390+
FlowExecution execution = run.getExecution();
391+
if (execution == null) {
392+
return HttpResponses.errorJSON("No execution found");
393+
}
394+
395+
// Pause is specific to CpsFlowExecution
396+
if (execution instanceof CpsFlowExecution) {
397+
CpsFlowExecution cpsExecution = (CpsFlowExecution) execution;
398+
399+
try {
400+
cpsExecution.pause(true);
401+
return HttpResponses.okJSON();
402+
} catch (IOException e) {
403+
String pauseFailedMessage = Messages.run_pauseFailed();
404+
logger.error(pauseFailedMessage, e);
405+
return HttpResponses.errorJSON(pauseFailedMessage + ": " + e.getMessage());
406+
}
407+
}
408+
409+
return HttpResponses.errorJSON(Messages.run_noPauseSupport());
410+
}
411+
412+
/**
413+
* Handles the resume request.
414+
*/
415+
@RequirePOST
416+
@JavaScriptMethod
417+
public HttpResponse doResume() {
418+
run.checkPermission(getCancelPermission());
419+
420+
if (!run.isBuilding()) {
421+
return HttpResponses.errorJSON(Messages.run_isFinished());
422+
}
423+
424+
FlowExecution execution = run.getExecution();
425+
if (execution == null) {
426+
return HttpResponses.errorJSON("No execution found");
427+
}
428+
429+
// Resume is specific to CpsFlowExecution
430+
if (execution instanceof CpsFlowExecution) {
431+
CpsFlowExecution cpsExecution = (CpsFlowExecution) execution;
432+
433+
try {
434+
cpsExecution.pause(false);
435+
return HttpResponses.okJSON();
436+
} catch (IOException e) {
437+
String resumeFailedMessage = Messages.run_resumeFailed();
438+
logger.error(resumeFailedMessage, e);
439+
return HttpResponses.errorJSON(resumeFailedMessage + ": " + e.getMessage());
440+
}
441+
}
442+
443+
return HttpResponses.errorJSON(Messages.run_noPauseSupport());
444+
}
445+
446+
/**
447+
* Returns the current pause state of the pipeline.
448+
*/
449+
@GET
450+
@WebMethod(name = "pauseState")
451+
public HttpResponse getPauseState(StaplerRequest2 req, StaplerResponse2 rsp) {
452+
run.checkPermission(Item.READ);
453+
454+
FlowExecution execution = run.getExecution();
455+
JSONObject obj = new JSONObject();
456+
457+
if (execution instanceof CpsFlowExecution) {
458+
CpsFlowExecution cpsExecution = (CpsFlowExecution) execution;
459+
obj.put("paused", cpsExecution.isPaused());
460+
obj.put("building", run.isBuilding());
461+
} else {
462+
obj.put("paused", false);
463+
obj.put("building", run.isBuilding());
464+
}
465+
466+
return HttpResponses.okJSON(obj);
467+
}
468+
377469
public String getFullProjectDisplayName() {
378470
return run.getParent().getFullDisplayName();
379471
}

src/main/resources/components/temporary-wrapper.jelly

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,24 +22,63 @@
2222
<j:set var="controls">
2323
<j:if test="${it.buildInProgress}">
2424
<l:hasPermission permission="${it.cancelPermission}">
25-
<j:set var="proxyId" value="${h.generateId()}" />
26-
<st:bind value="${it}" var="cancelAction${proxyId}"/>
27-
<button id="pgv-cancel" data-confirm="${%Confirm(it.buildFullDisplayName)}"
28-
data-success-message="${%Build cancelled}"
29-
data-proxy-name="cancelAction${proxyId}"
30-
class="jenkins-button jenkins-!-error-color">
31-
<l:icon src="symbol-stop-circle-outline plugin-ionicons-api"/>
32-
${%Cancel}
33-
</button>
25+
<j:set var="cancelProxyId" value="${h.generateId()}" />
26+
<st:bind value="${it}" var="cancelAction${cancelProxyId}"/>
27+
<j:set var="pauseProxyId" value="${h.generateId()}" />
28+
<st:bind value="${it}" var="pauseAction${pauseProxyId}"/>
29+
<j:set var="resumeProxyId" value="${h.generateId()}" />
30+
<st:bind value="${it}" var="resumeAction${resumeProxyId}"/>
31+
<div class="jenkins-split-button">
32+
<button id="pgv-cancel" data-confirm="${%Confirm(it.buildFullDisplayName)}"
33+
data-success-message="${%Build cancelled}"
34+
data-proxy-name="cancelAction${cancelProxyId}"
35+
class="jenkins-button jenkins-!-error-color">
36+
<div class="jenkins-dropdown__item__icon">
37+
<l:icon src="symbol-stop-circle-outline plugin-ionicons-api"/>
38+
</div>
39+
${%Cancel}
40+
</button>
41+
<l:overflowButton id="pgv-cancel-overflow" tooltip="${null}"
42+
icon="symbol-chevron-down"
43+
clazz="jenkins-button jenkins-!-error-color">
44+
<dd:custom>
45+
<a id="pgv-pause" href="#" class="jenkins-dropdown__item pgv-dropdown-item jenkins-!-skipped-color"
46+
data-success-message="${%Build paused}"
47+
data-proxy-name="pauseAction${pauseProxyId}">
48+
<div class="jenkins-dropdown__item__icon">
49+
<l:icon src="symbol-pause-circle-outline plugin-ionicons-api"/>
50+
</div>
51+
<span>${%Pause}</span>
52+
<div class="pgv-dropdown-item__description">
53+
${%Pause the running build}
54+
</div>
55+
</a>
56+
</dd:custom>
57+
<dd:custom>
58+
<a id="pgv-resume" href="#" class="jenkins-dropdown__item pgv-dropdown-item jenkins-!-accent-color"
59+
data-success-message="${%Build resumed}"
60+
data-proxy-name="resumeAction${resumeProxyId}"
61+
style="display: none;">
62+
<div class="jenkins-dropdown__item__icon">
63+
<l:icon src="symbol-play-circle-outline plugin-ionicons-api"/>
64+
</div>
65+
<span>${%Resume}</span>
66+
<div class="pgv-dropdown-item__description">
67+
${%Resume the paused build}
68+
</div>
69+
</a>
70+
</dd:custom>
71+
</l:overflowButton>
72+
</div>
3473
</l:hasPermission>
3574
</j:if>
3675
<j:if test="${it.buildable}">
3776
<l:hasPermission permission="${it.permission}">
38-
<j:set var="proxyId" value="${h.generateId()}" />
39-
<st:bind value="${it}" var="rerunAction${proxyId}"/>
77+
<j:set var="rerunProxyId" value="${h.generateId()}" />
78+
<st:bind value="${it}" var="rerunAction${rerunProxyId}"/>
4079
<div class="jenkins-split-button">
4180
<button id="pgv-rerun"
42-
data-proxy-name="rerunAction${proxyId}"
81+
data-proxy-name="rerunAction${rerunProxyId}"
4382
class="jenkins-button jenkins-!-build-color">
4483
<div class="jenkins-dropdown__item__icon">
4584
<l:icon src="symbol-refresh-outline plugin-ionicons-api"/>
@@ -125,6 +164,10 @@
125164
</l:details-bar>
126165
</div>
127166

167+
<div id="pgv-paused-banner" class="jenkins-alert jenkins-alert-warning" style="display: none;">
168+
${%Build Execution is Paused}
169+
</div>
170+
128171
<j:if test="${!empty(it.run.description)}">
129172
<div class="pgv-description">
130173
<l:icon src="symbol-information-circle-outline plugin-ionicons-api" />
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
Build\ cancelled=Build cancelled
2+
Build\ Execution\ is\ Paused=Build Execution is Paused
3+
Build\ paused=Build paused
4+
Build\ resumed=Build resumed
25
Cancel=Cancel
36
Configure=Configure
47
Confirm=Are you sure you want to abort {0}?
58
Edit\ parameters\ and\ rebuild=Edit parameters and rebuild
69
Edit\ Pipeline\ and\ replay=Edit Pipeline and replay
10+
Pause=Pause
11+
Pause\ the\ running\ build=Pause the running build
712
Rerun=Rerun
813
Rerun\ now=Rerun now
914
Replay=Replay
15+
Resume=Resume
16+
Resume\ the\ paused\ build=Resume the paused build
1017
Rebuild=Rebuild
1118
Restart\ from\ Stage=Restart from Stage
1219
Restart\ Pipeline\ from\ a\ specific\ Stage=Restart Pipeline from a specific Stage

src/main/resources/io/jenkins/plugins/pipelinegraphview/Messages.properties

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ FlowNodeWrapper.noStage=System Generated
2727

2828
run.alreadyCancelled=Run was already cancelled
2929
run.isFinished=Run is already finished
30+
run.pauseFailed=Failed to pause the pipeline
31+
run.resumeFailed=Failed to resume the pipeline
32+
run.noPauseSupport=This pipeline does not support pause/resume
3033

3134
scheduled.success=Build scheduled
3235
scheduled.failure=Could not schedule a build

src/main/resources/io/jenkins/plugins/pipelinegraphview/consoleview/PipelineConsoleViewAction/index.jelly

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
</l:details-bar>
1212
</l:app-bar>
1313

14+
<div id="pgv-paused-banner" class="jenkins-alert jenkins-alert-warning" style="display: none;">
15+
${%Build Execution is Paused}
16+
</div>
17+
1418
<j:if test="${!empty(it.run.description)}">
1519
<div class="pgv-description">
1620
<l:icon src="symbol-information-circle-outline plugin-ionicons-api" />

src/main/webapp/js/build.js

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,13 @@ if (cancelButton) {
9595
if (isBuilding === "true") {
9696
setTimeout(updateCancelButton, 5000);
9797
} else {
98-
cancelButton.style.display = "none";
98+
// Hide the entire split-button when build finishes
99+
const splitButton = cancelButton.closest(".jenkins-split-button");
100+
if (splitButton) {
101+
splitButton.style.display = "none";
102+
} else {
103+
cancelButton.style.display = "none";
104+
}
99105
}
100106
}
101107
return null;
@@ -106,3 +112,119 @@ if (cancelButton) {
106112
}
107113
setTimeout(updateCancelButton, 5000);
108114
}
115+
116+
// Use event delegation for pause/resume dropdown items
117+
document.addEventListener("click", function (event) {
118+
const target = event.target.closest("#pgv-pause, #pgv-resume");
119+
if (!target) return;
120+
121+
event.preventDefault();
122+
123+
const proxyName = target.dataset.proxyName;
124+
if (!proxyName) return;
125+
126+
const actionProxy = window[proxyName];
127+
if (!actionProxy) {
128+
console.error("Failed to execute action: proxy not found");
129+
return;
130+
}
131+
132+
if (target.id === "pgv-pause") {
133+
if (typeof actionProxy.doPause !== "function") {
134+
console.error("Failed to pause: method not found");
135+
return;
136+
}
137+
138+
actionProxy.doPause(function (response) {
139+
const result = response.responseJSON;
140+
if (result.status === "ok") {
141+
notificationBar.show(target.dataset.successMessage);
142+
// Hide pause, show resume
143+
const pauseItem = document.getElementById("pgv-pause");
144+
const resumeItem = document.getElementById("pgv-resume");
145+
if (pauseItem) pauseItem.style.display = "none";
146+
if (resumeItem) resumeItem.style.display = "";
147+
} else {
148+
notificationBar.show(result.message, notificationBar.WARNING);
149+
}
150+
});
151+
} else if (target.id === "pgv-resume") {
152+
if (typeof actionProxy.doResume !== "function") {
153+
console.error("Failed to resume: method not found");
154+
return;
155+
}
156+
157+
actionProxy.doResume(function (response) {
158+
const result = response.responseJSON;
159+
if (result.status === "ok") {
160+
notificationBar.show(target.dataset.successMessage);
161+
// Hide resume, show pause
162+
const pauseItem = document.getElementById("pgv-pause");
163+
const resumeItem = document.getElementById("pgv-resume");
164+
if (resumeItem) resumeItem.style.display = "none";
165+
if (pauseItem) pauseItem.style.display = "";
166+
} else {
167+
notificationBar.show(result.message, notificationBar.WARNING);
168+
}
169+
});
170+
}
171+
});
172+
173+
function updatePauseElements() {
174+
const pausedBanner = document.getElementById("pgv-paused-banner");
175+
const pauseMenuItem = document.getElementById("pgv-pause");
176+
const resumeMenuItem = document.getElementById("pgv-resume");
177+
178+
fetch("pauseState")
179+
.then((rsp) => {
180+
if (!rsp.ok) {
181+
throw new Error(
182+
`Failed to fetch pause state: ${rsp.status} - ${rsp.statusText}`,
183+
);
184+
}
185+
return rsp.json();
186+
})
187+
.then((result) => {
188+
if (result.status === "ok") {
189+
const isPaused = result.data.paused;
190+
const isBuilding = result.data.building;
191+
192+
if (isBuilding) {
193+
if (isPaused) {
194+
pausedBanner.style.display = "";
195+
if (pauseMenuItem) {
196+
pauseMenuItem.style.display = "none";
197+
}
198+
if (resumeMenuItem) {
199+
resumeMenuItem.style.display = "";
200+
}
201+
} else {
202+
pausedBanner.style.display = "none";
203+
if (pauseMenuItem) {
204+
pauseMenuItem.style.display = "";
205+
}
206+
if (resumeMenuItem) {
207+
resumeMenuItem.style.display = "none";
208+
}
209+
}
210+
211+
let updateInterval = 5000;
212+
// update more frequently when the pause/resume menu items are visible
213+
if (pauseMenuItem || resumeMenuItem) {
214+
updateInterval = 1000;
215+
}
216+
setTimeout(updatePauseElements(), updateInterval);
217+
} else {
218+
pausedBanner.style.display = "none";
219+
pauseMenuItem.style.display = "none";
220+
resumeMenuItem.style.display = "none";
221+
}
222+
}
223+
return null;
224+
})
225+
.catch((error) => {
226+
console.error("Error fetching pause state:", error);
227+
});
228+
}
229+
230+
updatePauseElements();

0 commit comments

Comments
 (0)