This is an automated email from the ASF dual-hosted git repository.
voidmatcha pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/zeppelin.git
The following commit(s) were added to refs/heads/master by this push:
new 2cc438b725 [ZEPPELIN-5235] Add Cancel all paragraphs button to the new
UI
2cc438b725 is described below
commit 2cc438b72501a202635f064f084d53cad9c04f5f
Author: HwangRock <[email protected]>
AuthorDate: Tue Aug 18 23:01:29 2026 +0900
[ZEPPELIN-5235] Add Cancel all paragraphs button to the new UI
### What is this PR for?
The note action bar has a run-all button but no way to stop a note from the
UI. Stopping a running
note is only reachable through `DELETE /api/notebook/job/{noteId}`, so a
user who starts a long note has
to leave the page and issue a REST call, or cancel each paragraph one by
one.
This adds a `Cancel all paragraphs` button next to run-all in the new UI.
The button sends a new
`CANCEL_ALL_PARAGRAPHS` websocket message, and `cancelAllParagraphs()` in
`NotebookService` aborts
every paragraph of the note that has not terminated yet.
The abort loop already existed inline in `stopNoteJobs()` in
`NotebookRestApi`. It is extracted to
`Note.abortAll()` so the REST endpoint and the websocket handler share one
implementation instead of
drifting apart. Permission handling follows the existing run path: the
service checks
`Permission.RUNNER` the same way `cancelParagraph()` does, and the note is
reached through
`notebook.processNote()` so the note lock is held while paragraphs are
aborted.
No new cancellation mechanism is introduced. `Note.abortAll()` calls the
existing `Paragraph.abort()`,
which delegates to `interpreter.cancel()` exactly as single-paragraph
cancel does today. The button is
disabled in revision view and while no paragraph is running.
### What type of PR is it?
Improvement
### Todos
* [x] - Extract `Note.abortAll()` and reuse it from `stopNoteJobs()`
* [x] - Add `CANCEL_ALL_PARAGRAPHS` op and
`NotebookService.cancelAllParagraphs()`
* [x] - Add the button to the new UI action bar and the message to the SDK
* [x] - Unit tests for the notebook, service and socket layers
### What is the Jira issue?
https://issues.apache.org/jira/browse/ZEPPELIN-5235
### How should this be tested?
`NotebookServiceTest` covers the success path, the forbidden path for a
non-runner, and an unknown
note id. `NotebookServerTest` covers the op routing. `NotebookTest` covers
`Note.abortAll()` leaving
terminated paragraphs alone.
Manually: create a note with six python paragraphs where the first two
finish within a few seconds and
the rest sleep for minutes, run all, then press the button while a
paragraph is running. The running
paragraph moves to ABORT, the finished ones keep FINISHED, and paragraphs
that never started stay
untouched.
### Screenshots (if appropriate)
https://github.com/user-attachments/assets/ebd5e205-ad18-4340-9995-2fcfd10f5d9b
### Questions:
* Does the license files need to update? No.
* Is there breaking changes for older versions? No. The REST behaviour is
unchanged and the new op is additive.
* Does this needs documentation? No.
Closes #5425 from HwangRock/ZEPPELIN-5235-cancel-all.
Signed-off-by: YONGJAE LEE <[email protected]>
---
.../java/org/apache/zeppelin/common/Message.java | 1 +
.../java/org/apache/zeppelin/notebook/Note.java | 11 ++++
.../org/apache/zeppelin/rest/NotebookRestApi.java | 6 +--
.../apache/zeppelin/service/NotebookService.java | 20 +++++++
.../org/apache/zeppelin/socket/NotebookServer.java | 8 +++
.../org/apache/zeppelin/notebook/NotebookTest.java | 30 +++++++++++
.../zeppelin/service/NotebookServiceTest.java | 61 ++++++++++++++++++++++
.../apache/zeppelin/socket/NotebookServerTest.java | 46 ++++++++++++++++
.../e2e/models/notebook-action-bar-page.ts | 2 +
.../action-bar/action-bar-functionality.spec.ts | 8 +++
.../interfaces/message-data-type-map.interface.ts | 2 +
.../src/interfaces/message-operator.interface.ts | 6 +++
.../src/interfaces/message-paragraph.interface.ts | 4 ++
.../projects/zeppelin-sdk/src/message.ts | 4 ++
.../notebook/action-bar/action-bar.component.html | 11 ++++
.../notebook/action-bar/action-bar.component.ts | 4 ++
.../src/app/services/message.service.ts | 4 ++
17 files changed, 223 insertions(+), 5 deletions(-)
diff --git
a/zeppelin-common/src/main/java/org/apache/zeppelin/common/Message.java
b/zeppelin-common/src/main/java/org/apache/zeppelin/common/Message.java
index 6ec66e63db..126855bcff 100644
--- a/zeppelin-common/src/main/java/org/apache/zeppelin/common/Message.java
+++ b/zeppelin-common/src/main/java/org/apache/zeppelin/common/Message.java
@@ -192,6 +192,7 @@ public class Message implements JsonSerializable {
PARAGRAPH_MOVED, // [s-c] paragraph moved
NOTE_UPDATED, // [s-c] paragraph updated(name, config)
RUN_ALL_PARAGRAPHS, // [c-s] run all paragraphs
+ CANCEL_ALL_PARAGRAPHS, // [c-s] cancel(abort) all paragraphs
PARAGRAPH_EXECUTED_BY_SPELL, // [c-s] paragraph was executed by spell
RUN_PARAGRAPH_USING_SPELL, // [s-c] run paragraph using spell
PARAS_INFO, // [s-c] paragraph runtime infos
diff --git
a/zeppelin-server/src/main/java/org/apache/zeppelin/notebook/Note.java
b/zeppelin-server/src/main/java/org/apache/zeppelin/notebook/Note.java
index 278fa43ecc..ccebad12dc 100644
--- a/zeppelin-server/src/main/java/org/apache/zeppelin/notebook/Note.java
+++ b/zeppelin-server/src/main/java/org/apache/zeppelin/notebook/Note.java
@@ -815,6 +815,17 @@ public class Note implements JsonSerializable {
return this.paragraphs;
}
+ /**
+ * Abort all the paragraphs which are not terminated yet.
+ */
+ public void abortAll() {
+ for (Paragraph p : getParagraphs()) {
+ if (!p.isTerminated()) {
+ p.abort();
+ }
+ }
+ }
+
// TODO(zjffdu) how does this used ?
private void snapshotAngularObjectRegistry(String user) {
angularObjects = new HashMap<>();
diff --git
a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/NotebookRestApi.java
b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/NotebookRestApi.java
index 192cd5056c..3c09a612f4 100644
---
a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/NotebookRestApi.java
+++
b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/NotebookRestApi.java
@@ -887,11 +887,7 @@ public class NotebookRestApi extends AbstractRestApi {
note -> {
checkIfNoteIsNotNull(note, noteId);
checkIfUserCanRun(noteId, "Insufficient privileges you cannot stop
this job for this note");
- for (Paragraph p : note.getParagraphs()) {
- if (!p.isTerminated()) {
- p.abort();
- }
- }
+ note.abortAll();
return new JsonResponse<>(Status.OK).build();
});
}
diff --git
a/zeppelin-server/src/main/java/org/apache/zeppelin/service/NotebookService.java
b/zeppelin-server/src/main/java/org/apache/zeppelin/service/NotebookService.java
index 38ce280ad6..9e5e31aa1c 100644
---
a/zeppelin-server/src/main/java/org/apache/zeppelin/service/NotebookService.java
+++
b/zeppelin-server/src/main/java/org/apache/zeppelin/service/NotebookService.java
@@ -602,6 +602,26 @@ public class NotebookService {
}
+ public void cancelAllParagraphs(String noteId,
+ ServiceContext context,
+ ServiceCallback<Paragraph> callback) throws
IOException {
+ if (!checkPermission(noteId, Permission.RUNNER,
Message.OP.CANCEL_ALL_PARAGRAPHS, context,
+ callback)) {
+ return;
+ }
+
+ notebook.processNote(noteId,
+ note -> {
+ if (note == null) {
+ throw new NoteNotFoundException(noteId);
+ }
+ note.abortAll();
+ callback.onSuccess(null, context);
+ return null;
+ });
+
+ }
+
public void moveParagraph(String noteId,
String paragraphId,
int newIndex,
diff --git
a/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java
b/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java
index 2d78ae7fb5..555d22ffd3 100644
---
a/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java
+++
b/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java
@@ -387,6 +387,9 @@ public class NotebookServer implements
AngularObjectRegistryListener,
case CANCEL_PARAGRAPH:
cancelParagraph(conn, context, receivedMessage);
break;
+ case CANCEL_ALL_PARAGRAPHS:
+ cancelAllParagraphs(conn, context, receivedMessage);
+ break;
case MOVE_PARAGRAPH:
moveParagraph(conn, context, receivedMessage);
break;
@@ -1482,6 +1485,11 @@ public class NotebookServer implements
AngularObjectRegistryListener,
getNotebookService().cancelParagraph(noteId, paragraphId, context, new
WebSocketServiceCallback<>(conn));
}
+ private void cancelAllParagraphs(NotebookSocket conn, ServiceContext
context, Message fromMessage) throws IOException {
+ final String noteId = (String) fromMessage.get("noteId");
+ getNotebookService().cancelAllParagraphs(noteId, context, new
WebSocketServiceCallback<>(conn));
+ }
+
private void runAllParagraphs(NotebookSocket conn,
ServiceContext context,
Message fromMessage) throws IOException {
diff --git
a/zeppelin-server/src/test/java/org/apache/zeppelin/notebook/NotebookTest.java
b/zeppelin-server/src/test/java/org/apache/zeppelin/notebook/NotebookTest.java
index 39e6ec70e3..a9a879795e 100644
---
a/zeppelin-server/src/test/java/org/apache/zeppelin/notebook/NotebookTest.java
+++
b/zeppelin-server/src/test/java/org/apache/zeppelin/notebook/NotebookTest.java
@@ -616,6 +616,36 @@ class NotebookTest extends AbstractInterpreterTest
implements ParagraphJobListen
notebook.removeNote(noteId, anonymous);
}
+ @Test
+ void testAbortAll() throws IOException {
+ String noteId = notebook.createNote("note1", anonymous);
+ notebook.processNote(noteId,
+ note -> {
+ Paragraph p1 = note.addNewParagraph(AuthenticationInfo.ANONYMOUS);
+ p1.setText("p1");
+ p1.setStatus(Status.RUNNING);
+
+ Paragraph p2 = note.addNewParagraph(AuthenticationInfo.ANONYMOUS);
+ p2.setText("p2");
+ p2.setStatus(Status.PENDING);
+
+ Paragraph p3 = note.addNewParagraph(AuthenticationInfo.ANONYMOUS);
+ p3.setText("p3");
+ p3.setStatus(Status.FINISHED);
+
+ // when
+ note.abortAll();
+
+ // then
+ assertTrue(p1.isAborted());
+ assertTrue(p2.isAborted());
+ assertFalse(p3.isAborted());
+ assertEquals(Status.FINISHED, p3.getStatus());
+ return null;
+ });
+ notebook.removeNote(noteId, anonymous);
+ }
+
@Test
void testSchedule() throws InterruptedException, IOException {
// create a note and a paragraph
diff --git
a/zeppelin-server/src/test/java/org/apache/zeppelin/service/NotebookServiceTest.java
b/zeppelin-server/src/test/java/org/apache/zeppelin/service/NotebookServiceTest.java
index 2d53f34dec..2eeb0f650c 100644
---
a/zeppelin-server/src/test/java/org/apache/zeppelin/service/NotebookServiceTest.java
+++
b/zeppelin-server/src/test/java/org/apache/zeppelin/service/NotebookServiceTest.java
@@ -19,8 +19,10 @@
package org.apache.zeppelin.service;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.any;
@@ -67,6 +69,9 @@ import
org.apache.zeppelin.notebook.exception.NotePathAlreadyExistsException;
import org.apache.zeppelin.notebook.repo.NotebookRepo;
import org.apache.zeppelin.notebook.repo.VFSNotebookRepo;
import org.apache.zeppelin.notebook.scheduler.QuartzSchedulerService;
+import org.apache.zeppelin.rest.exception.ForbiddenException;
+import org.apache.zeppelin.rest.exception.NoteNotFoundException;
+import org.apache.zeppelin.scheduler.Job.Status;
import org.apache.zeppelin.search.LuceneSearch;
import org.apache.zeppelin.search.SearchService;
import org.apache.zeppelin.storage.ConfigStorage;
@@ -671,6 +676,62 @@ class NotebookServiceTest {
});
}
+ @Test
+ void testCancelAllParagraphs() throws IOException {
+ String note1Id = notebookService.createNote("note_cancel_all", "python",
false, context, callback);
+ Paragraph p1 = notebook.processNote(note1Id,
+ note1 -> {
+ Paragraph p = note1.addNewParagraph(context.getAutheInfo());
+ p.setText("p1");
+ p.setStatus(Status.RUNNING);
+ return p;
+ });
+ Paragraph p2 = notebook.processNote(note1Id,
+ note1 -> {
+ Paragraph p = note1.addNewParagraph(context.getAutheInfo());
+ p.setText("p2");
+ p.setStatus(Status.FINISHED);
+ return p;
+ });
+
+ reset(callback);
+ notebookService.cancelAllParagraphs(note1Id, context, callback);
+
+ assertTrue(p1.isAborted());
+ assertFalse(p2.isAborted());
+ verify(callback).onSuccess(any(), eq(context));
+ }
+
+ @Test
+ void testCancelAllParagraphsForbidden() throws IOException {
+ String note1Id = notebookService.createNote("note_cancel_all_forbidden",
"python", false, context, callback);
+ Paragraph p1 = notebook.processNote(note1Id,
+ note1 -> {
+ Paragraph p = note1.addNewParagraph(context.getAutheInfo());
+ p.setText("p1");
+ p.setStatus(Status.RUNNING);
+ return p;
+ });
+
+ HashSet<String> otherUser = new HashSet<>();
+ otherUser.add("other_user");
+ authorizationService.setOwners(note1Id, otherUser);
+ authorizationService.setWriters(note1Id, otherUser);
+ authorizationService.setRunners(note1Id, otherUser);
+
+ reset(callback);
+ notebookService.cancelAllParagraphs(note1Id, context, callback);
+
+ assertFalse(p1.isAborted());
+ verify(callback).onFailure(any(ForbiddenException.class), eq(context));
+ }
+
+ @Test
+ void testCancelAllParagraphsNoteNotFound() {
+ assertThrows(NoteNotFoundException.class,
+ () -> notebookService.cancelAllParagraphs("non_existing_note_id",
context, callback));
+ }
+
@Test
void testNormalizeNotePath() throws IOException {
assertEquals("/Untitled Note", notebookService.normalizeNotePath(" "));
diff --git
a/zeppelin-server/src/test/java/org/apache/zeppelin/socket/NotebookServerTest.java
b/zeppelin-server/src/test/java/org/apache/zeppelin/socket/NotebookServerTest.java
index d982d46a33..ef6eb6d5ec 100644
---
a/zeppelin-server/src/test/java/org/apache/zeppelin/socket/NotebookServerTest.java
+++
b/zeppelin-server/src/test/java/org/apache/zeppelin/socket/NotebookServerTest.java
@@ -237,6 +237,52 @@ class NotebookServerTest extends AbstractTestRestApi {
}
}
+ @Test
+ void testCancelAllParagraphsNotDisabledForRunningNotes() {
+ assertFalse(Message.isDisabledForRunningNotes(OP.CANCEL_ALL_PARAGRAPHS));
+ }
+
+ @Test
+ void testCancelAllParagraphsWebSocket() throws IOException {
+ NotebookSocket sock1 = createWebSocket();
+
+ String noteName = "Note with millis " + System.currentTimeMillis();
+ notebookServer.onMessage(sock1, new Message(OP.NEW_NOTE).put("name",
noteName).toJson());
+ NoteInfo createdNoteInfo = null;
+ for (NoteInfo noteInfo : notebook.getNotesInfo()) {
+ if (notebook.processNote(noteInfo.getId(),
Note::getName).equals(noteName)) {
+ createdNoteInfo = noteInfo;
+ break;
+ }
+ }
+ String noteId = createdNoteInfo.getId();
+
+ notebookServer.onMessage(sock1, new Message(OP.GET_NOTE).put("id",
noteId).toJson());
+
+ Paragraph paragraph = notebook.processNote(noteId,
+ note -> {
+ Paragraph p = note.getParagraphs().get(0);
+ p.setStatus(Status.RUNNING);
+ // simulate a sequential run in progress
+ note.setRunning(true);
+ return p;
+ });
+
+ try {
+ notebookServer.onMessage(sock1,
+ new Message(OP.CANCEL_ALL_PARAGRAPHS).put("noteId",
noteId).toJson());
+
+ assertTrue(paragraph.isAborted());
+ } finally {
+ notebook.processNote(noteId,
+ note -> {
+ note.setRunning(false);
+ return null;
+ });
+ notebook.removeNote(noteId, anonymous);
+ }
+ }
+
@Test
void testCollaborativeEditing() throws IOException {
if
(!zepServer.getZeppelinConfiguration().isZeppelinNotebookCollaborativeModeEnable())
{
diff --git a/zeppelin-web-angular/e2e/models/notebook-action-bar-page.ts
b/zeppelin-web-angular/e2e/models/notebook-action-bar-page.ts
index 1ecc33bd2c..3ffe207727 100644
--- a/zeppelin-web-angular/e2e/models/notebook-action-bar-page.ts
+++ b/zeppelin-web-angular/e2e/models/notebook-action-bar-page.ts
@@ -16,6 +16,7 @@ import { BasePage } from './base-page';
export class NotebookActionBarPage extends BasePage {
readonly titleEditor: Locator;
readonly runAllButton: Locator;
+ readonly cancelAllButton: Locator;
readonly showHideCodeButton: Locator;
readonly showHideOutputButton: Locator;
readonly clearOutputButton: Locator;
@@ -40,6 +41,7 @@ export class NotebookActionBarPage extends BasePage {
super(page);
this.titleEditor = page.locator('zeppelin-elastic-input');
this.runAllButton = page.locator('button[nzTooltipTitle="Run all
paragraphs"]');
+ this.cancelAllButton = page.locator('button[nzTooltipTitle="Cancel all
paragraphs"]');
this.showHideCodeButton = page.locator('button[nzTooltipTitle="Show/hide
the code"]');
this.showHideOutputButton = page.locator('button[nzTooltipTitle="Show/hide
the output"]');
this.clearOutputButton = page.locator('button[nzTooltipTitle="Clear all
output"]');
diff --git
a/zeppelin-web-angular/e2e/tests/notebook/action-bar/action-bar-functionality.spec.ts
b/zeppelin-web-angular/e2e/tests/notebook/action-bar/action-bar-functionality.spec.ts
index 5825e1d31b..0ef046d3c1 100644
---
a/zeppelin-web-angular/e2e/tests/notebook/action-bar/action-bar-functionality.spec.ts
+++
b/zeppelin-web-angular/e2e/tests/notebook/action-bar/action-bar-functionality.spec.ts
@@ -68,6 +68,14 @@ test.describe('Notebook Action Bar Functionality', () => {
await expect(confirmButton).not.toBeVisible();
});
+ test('should display cancel all button as disabled when note is idle', async
() => {
+ await expect(actionBarPage.cancelAllButton).toBeVisible();
+
+ // Given: an idle note (no paragraph running), Cancel all is disabled and
Run all is enabled — the two buttons are mutually exclusive
+ await expect(actionBarPage.cancelAllButton).toBeDisabled();
+ await expect(actionBarPage.runAllButton).toBeEnabled();
+ });
+
test('should toggle code visibility', async () => {
await expect(actionBarPage.showHideCodeButton).toBeVisible();
await expect(actionBarPage.showHideCodeButton).toBeEnabled();
diff --git
a/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-data-type-map.interface.ts
b/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-data-type-map.interface.ts
index 6c6088c73a..5fba20a132 100644
---
a/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-data-type-map.interface.ts
+++
b/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-data-type-map.interface.ts
@@ -59,6 +59,7 @@ import {
AngularObjectRemove,
AngularObjectUpdate,
AngularObjectUpdated,
+ CancelAllParagraphs,
CancelParagraph,
CommitParagraph,
Completion,
@@ -154,6 +155,7 @@ export interface MessageSendDataTypeMap {
[OP.PARAGRAPH_EXECUTED_BY_SPELL]: ParagraphExecutedBySpell;
[OP.RUN_PARAGRAPH]: RunParagraph;
[OP.RUN_ALL_PARAGRAPHS]: RunAllParagraphs;
+ [OP.CANCEL_ALL_PARAGRAPHS]: CancelAllParagraphs;
[OP.PARAGRAPH_REMOVE]: ParagraphRemove;
[OP.PARAGRAPH_CLEAR_OUTPUT]: ParagraphClearOutput;
[OP.PARAGRAPH_CLEAR_ALL_OUTPUT]: ParagraphClearAllOutput;
diff --git
a/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-operator.interface.ts
b/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-operator.interface.ts
index 1f8036b393..322fb8f388 100644
---
a/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-operator.interface.ts
+++
b/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-operator.interface.ts
@@ -443,6 +443,12 @@ export enum OP {
*/
RUN_ALL_PARAGRAPHS = 'RUN_ALL_PARAGRAPHS',
+ /**
+ * [c-s]
+ * cancel all paragraphs
+ */
+ CANCEL_ALL_PARAGRAPHS = 'CANCEL_ALL_PARAGRAPHS',
+
/**
* [c-s]
* paragraph was executed by spell
diff --git
a/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-paragraph.interface.ts
b/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-paragraph.interface.ts
index 2ea3916ea1..f75cd1f5f3 100644
---
a/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-paragraph.interface.ts
+++
b/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-paragraph.interface.ts
@@ -178,6 +178,10 @@ export interface RunAllParagraphs {
paragraphs: string;
}
+export interface CancelAllParagraphs {
+ noteId: string;
+}
+
export interface InsertParagraph {
index: number;
}
diff --git a/zeppelin-web-angular/projects/zeppelin-sdk/src/message.ts
b/zeppelin-web-angular/projects/zeppelin-sdk/src/message.ts
index 6262bff26a..110af36f27 100644
--- a/zeppelin-web-angular/projects/zeppelin-sdk/src/message.ts
+++ b/zeppelin-web-angular/projects/zeppelin-sdk/src/message.ts
@@ -415,6 +415,10 @@ export class Message {
});
}
+ cancelAllParagraphs(noteId: string): void {
+ this.send<OP.CANCEL_ALL_PARAGRAPHS>(OP.CANCEL_ALL_PARAGRAPHS, { noteId });
+ }
+
paragraphRemove(paragraphId: string): void {
this.send<OP.PARAGRAPH_REMOVE>(OP.PARAGRAPH_REMOVE, { id: paragraphId });
}
diff --git
a/zeppelin-web-angular/src/app/pages/workspace/notebook/action-bar/action-bar.component.html
b/zeppelin-web-angular/src/app/pages/workspace/notebook/action-bar/action-bar.component.html
index f53f5292da..c45438e872 100644
---
a/zeppelin-web-angular/src/app/pages/workspace/notebook/action-bar/action-bar.component.html
+++
b/zeppelin-web-angular/src/app/pages/workspace/notebook/action-bar/action-bar.component.html
@@ -33,6 +33,17 @@
>
<i nz-icon nzType="play-circle" nzTheme="outline"></i>
</button>
+ <button
+ nz-button
+ nz-popconfirm
+ nzPopconfirmTitle="Cancel all paragraphs?"
+ nz-tooltip
+ nzTooltipTitle="Cancel all paragraphs"
+ (nzOnConfirm)="cancelAllParagraphs()"
+ [disabled]="revisionView || !isNoteParagraphRunning"
+ >
+ <i nz-icon nzType="pause-circle" nzTheme="outline"></i>
+ </button>
@if (!viewOnly) {
<button
nz-button
diff --git
a/zeppelin-web-angular/src/app/pages/workspace/notebook/action-bar/action-bar.component.ts
b/zeppelin-web-angular/src/app/pages/workspace/notebook/action-bar/action-bar.component.ts
index cab6895b9b..a0b92d2c72 100644
---
a/zeppelin-web-angular/src/app/pages/workspace/notebook/action-bar/action-bar.component.ts
+++
b/zeppelin-web-angular/src/app/pages/workspace/notebook/action-bar/action-bar.component.ts
@@ -154,6 +154,10 @@ export class NotebookActionBarComponent extends
MessageListenersManager implemen
);
}
+ cancelAllParagraphs() {
+ this.messageService.cancelAllParagraphs(this.note.id);
+ }
+
clearAllParagraphOutput() {
this.messageService.paragraphClearAllOutput(this.note.id);
}
diff --git a/zeppelin-web-angular/src/app/services/message.service.ts
b/zeppelin-web-angular/src/app/services/message.service.ts
index 9b86a7d42d..1c61fb052b 100644
--- a/zeppelin-web-angular/src/app/services/message.service.ts
+++ b/zeppelin-web-angular/src/app/services/message.service.ts
@@ -277,6 +277,10 @@ export class MessageService extends Message implements
OnDestroy {
super.runAllParagraphs(noteId, paragraphs);
}
+ cancelAllParagraphs(noteId: string): void {
+ super.cancelAllParagraphs(noteId);
+ }
+
paragraphRemove(paragraphId: string): void {
super.paragraphRemove(paragraphId);
}