This is an automated email from the ASF dual-hosted git repository.
dongjoon-hyun pushed a commit to branch branch-4.x
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/branch-4.x by this push:
new d3509396c65e [SPARK-59085][CORE] Render the web UI's inline `(kill)`,
`(hold)`, and `(resume)` links as buttons
d3509396c65e is described below
commit d3509396c65eafda14eddda1c6f11ce4bf6445de
Author: Dongjoon Hyun <[email protected]>
AuthorDate: Fri Aug 28 11:50:29 2026 -0700
[SPARK-59085][CORE] Render the web UI's inline `(kill)`, `(hold)`, and
`(resume)` links as buttons
### What changes were proposed in this pull request?
This PR renders the web UI's inline `(kill)`, `(hold)`, and `(resume)` text
links as buttons, using the `btn btn-sm btn-outline-*` style already used
elsewhere in the UI.
**BEFORE (Apache Spark 5.0.0-SNAPSHOT)**
<img width="410" height="115" alt="Screenshot 2026-08-28 at 09 48 51"
src="https://github.com/user-attachments/assets/b0516f14-e7a8-45a7-a0ff-e5b1e71fe1c2"
/>
**AFTER (This PR)**
<img width="421" height="128" alt="Screenshot 2026-08-28 at 09 47 20"
src="https://github.com/user-attachments/assets/3383ca74-87db-4b40-8d30-454d26a04327"
/>
- Master UI application and driver tables: these controls are already
wrapped in `POST` forms, so their anchors become `<button type="submit">`.
- Stage and job tables, and the jobs page summary: these must stay `GET`
links (SPARK-6846: the YARN AM proxy does not forward `POST`), so they remain
anchors and gain `role="button"`.
- `kill` uses `btn-outline-danger` and `hold`/`resume` use
`btn-outline-secondary`, separating the destructive control from the reversible
one.
- Labels drop the parentheses that marked them as links: `(kill)` ->
`Kill`, `(hold)` -> `Hold`, `(resume)` -> `Resume`.
- Because a submit button posts its own form, `webui.js` no longer has to
submit the form on the anchor's behalf. The two otherwise identical
confirmation handlers collapse into one that works for both links and buttons
(22 lines -> 8).
- `kill-link` and `confirm-link` remain as JS and test hooks but no longer
carry colors; the Bootstrap button variant owns those now.
- `docs/web-ui.md` and `docs/spark-standalone.md` are updated to match the
new labels.
### Why are the changes needed?
These controls perform actions -- killing an application, stage, or job,
and holding or resuming an application -- but render as small parenthesized
secondary-colored text, which reads as body copy rather than as something
clickable, and gives a hit target only as wide as the word.
This also aligns them with the button style the UI has been converging on.
SPARK-59066 and SPARK-59067 recently moved the History Server's `Download`
control and the SQL execution page's `Download` control to `btn btn-sm
btn-outline-secondary`; this PR applies the same style to the remaining inline
controls.
Converting the Master UI's `POST` controls to real submit buttons also
makes them reachable by keyboard, which `<a href="#">` was not, and removes the
JS workaround that submitted the form for them.
### Does this PR introduce _any_ user-facing change?
Yes, this is a visual change to the web UI. The controls listed above now
render as buttons and their labels lose the surrounding parentheses.
One behavior change is worth calling out: the Master UI's kill, hold, and
resume controls are now native submit buttons, so if `webui.js` fails to load,
clicking one posts the form without showing the confirmation prompt. Previously
the `<a href="#">` did nothing at all without JS. This matches the driver UI's
kill and hold controls, which are `GET` links and have always navigated without
a prompt when JS is unavailable.
### How was this patch tested?
- Existing coverage is unaffected: `UISeleniumSuite` locates these controls
by the `kill-link` and `confirm-link` class names, which are preserved, and
`MasterWebUISuite` asserts on the rendered `app/kill/`, `app/hold/`, and
`app/resume/` form actions, which are unchanged.
- `core/compile`, `core/scalastyle`, and `dev/lint-js` pass.
- Verified manually
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Opus 5
Closes #58387 from dongjoon-hyun/SPARK-59085.
Authored-by: Dongjoon Hyun <[email protected]>
Signed-off-by: Dongjoon Hyun <[email protected]>
(cherry picked from commit 598ca22c14ea9c8fcbaf2478abd0e5e31fa1ac30)
Signed-off-by: Dongjoon Hyun <[email protected]>
---
.../resources/org/apache/spark/ui/static/webui.css | 6 ++---
.../resources/org/apache/spark/ui/static/webui.js | 22 ++++--------------
.../apache/spark/deploy/master/ui/MasterPage.scala | 27 +++++++++++-----------
.../org/apache/spark/ui/jobs/AllJobsPage.scala | 9 ++++----
.../org/apache/spark/ui/jobs/StageTable.scala | 4 ++--
docs/spark-standalone.md | 8 +++----
docs/web-ui.md | 8 +++----
7 files changed, 36 insertions(+), 48 deletions(-)
diff --git a/core/src/main/resources/org/apache/spark/ui/static/webui.css
b/core/src/main/resources/org/apache/spark/ui/static/webui.css
index 5a1d41c3d9bd..591f807bcff4 100755
--- a/core/src/main/resources/org/apache/spark/ui/static/webui.css
+++ b/core/src/main/resources/org/apache/spark/ui/static/webui.css
@@ -137,15 +137,13 @@ table.sortable td {
background: linear-gradient(to bottom, var(--spark-progress-completed-from),
var(--spark-progress-completed-to));
}
-a.kill-link {
+.kill-link {
margin-right: 2px;
margin-left: 20px;
- color: var(--bs-secondary-color);
}
-a.confirm-link {
+.confirm-link {
margin-left: 4px;
- color: var(--bs-secondary-color);
}
a.name-link {
diff --git a/core/src/main/resources/org/apache/spark/ui/static/webui.js
b/core/src/main/resources/org/apache/spark/ui/static/webui.js
index 58b530919a77..1f1bf8cf45d7 100644
--- a/core/src/main/resources/org/apache/spark/ui/static/webui.js
+++ b/core/src/main/resources/org/apache/spark/ui/static/webui.js
@@ -121,24 +121,12 @@ $(function() {
$(this).closest("tr").nextAll("tr.sub-execution-list").first().toggleClass("collapsed");
});
- // kill links with confirmation
- $(document).on("click", "a.kill-link[data-kill-message]", function(e) {
- if (!window.confirm($(this).data("kill-message"))) {
+ // controls guarded by a confirmation prompt before they act: a link
navigates and a submit
+ // button posts its form. "data-kill-message" is the older spelling of
"data-confirm-message".
+ $(document).on("click", "[data-kill-message], [data-confirm-message]",
function(e) {
+ var message = $(this).data("kill-message") ||
$(this).data("confirm-message");
+ if (!window.confirm(message)) {
e.preventDefault();
- } else if ($(this).closest("form").length > 0) {
- e.preventDefault();
- $(this).closest("form").submit();
- }
- });
-
- // generic links guarded by a confirmation prompt before navigating, or
before submitting the
- // form they are wrapped in
- $(document).on("click", "a.confirm-link[data-confirm-message]", function(e) {
- if (!window.confirm($(this).data("confirm-message"))) {
- e.preventDefault();
- } else if ($(this).closest("form").length > 0) {
- e.preventDefault();
- $(this).closest("form").submit();
}
});
diff --git
a/core/src/main/scala/org/apache/spark/deploy/master/ui/MasterPage.scala
b/core/src/main/scala/org/apache/spark/deploy/master/ui/MasterPage.scala
index 637109cec46b..cd7787623423 100644
--- a/core/src/main/scala/org/apache/spark/deploy/master/ui/MasterPage.scala
+++ b/core/src/main/scala/org/apache/spark/deploy/master/ui/MasterPage.scala
@@ -335,18 +335,18 @@ private[ui] class MasterPage(parent: MasterWebUI) extends
WebUIPage("") {
private def appRow(app: ApplicationInfo): Seq[Node] = {
val killLink = if (parent.killEnabled &&
(app.state == ApplicationState.RUNNING || app.state ==
ApplicationState.WAITING)) {
- <form action="app/kill/" method="POST" class="d-inline">
+ <form action="app/kill/" method="POST" class="d-inline float-end">
<input type="hidden" name="id" value={app.id}/>
<input type="hidden" name="terminate" value="true"/>
- <a href="#"
- data-kill-message={s"Are you sure you want to kill application
${app.id} ?"}
- class="kill-link float-end">(kill)</a>
+ <button type="submit"
+ data-kill-message={s"Are you sure you want to kill application
${app.id} ?"}
+ class="btn btn-sm btn-outline-danger kill-link">Kill</button>
</form>
}
// Offered only for an application whose driver reported it as holdable
and which did not
// disable the controls itself through spark.ui.holdEnabled. Rendered
before the kill link:
- // floated links stack right to left, so this keeps the two controls
adjacent and leaves the
- // kill link's left margin between the application id and the pair.
+ // floated controls stack right to left, so this keeps the two adjacent
and leaves the kill
+ // button's left margin between the application id and the pair.
val holdLink = if (parent.holdEnabled && app.desc.holdEnabled &&
app.holdSupported &&
!app.isFinished) {
val (action, message) = if (app.isHeld) {
@@ -355,10 +355,11 @@ private[ui] class MasterPage(parent: MasterWebUI) extends
WebUIPage("") {
("hold", s"Are you sure you want to hold application ${app.id}? All
executors will " +
"be decommissioned after finishing their running tasks, and cached
blocks are lost.")
}
- <form action={s"app/$action/"} method="POST" class="d-inline">
+ val label = action.capitalize
+ <form action={s"app/$action/"} method="POST" class="d-inline float-end">
<input type="hidden" name="id" value={app.id}/>
- <a href="#" data-confirm-message={message}
- class="confirm-link float-end">{s"($action)"}</a>
+ <button type="submit" data-confirm-message={message}
+ class="btn btn-sm btn-outline-secondary
confirm-link">{label}</button>
</form>
}
<tr>
@@ -403,12 +404,12 @@ private[ui] class MasterPage(parent: MasterWebUI) extends
WebUIPage("") {
val killLink = if (parent.killEnabled &&
(driver.state == DriverState.RUNNING ||
driver.state == DriverState.SUBMITTED)) {
- <form action="driver/kill/" method="POST" class="d-inline">
+ <form action="driver/kill/" method="POST" class="d-inline float-end">
<input type="hidden" name="id" value={driver.id}/>
<input type="hidden" name="terminate" value="true"/>
- <a href="#"
- data-kill-message={s"Are you sure you want to kill driver
${driver.id} ?"}
- class="kill-link float-end">(kill)</a>
+ <button type="submit"
+ data-kill-message={s"Are you sure you want to kill driver
${driver.id} ?"}
+ class="btn btn-sm btn-outline-danger kill-link">Kill</button>
</form>
}
<tr>
diff --git a/core/src/main/scala/org/apache/spark/ui/jobs/AllJobsPage.scala
b/core/src/main/scala/org/apache/spark/ui/jobs/AllJobsPage.scala
index a0f9e52f2fb8..daf04085fc02 100644
--- a/core/src/main/scala/org/apache/spark/ui/jobs/AllJobsPage.scala
+++ b/core/src/main/scala/org/apache/spark/ui/jobs/AllJobsPage.scala
@@ -369,12 +369,13 @@ private[ui] class AllJobsPage(parent: JobsTab, store:
AppStatusStore) extends We
("Running", "hold", "Are you sure you want to hold this
application? All " +
"executors will be decommissioned after finishing their
running tasks.")
}
+ val label = action.capitalize
<li>
<strong>Application:</strong>
{status}
- <a href={s"$basePathUri/jobs/$action/"}
+ <a href={s"$basePathUri/jobs/$action/"} role="button"
data-confirm-message={confirm}
- class="confirm-link">{s"($action)"}</a>
+ class="btn btn-sm btn-outline-secondary
confirm-link">{label}</a>
{parent.lastHoldRequestStatus.getOrElse("")}
</li>
}
@@ -614,9 +615,9 @@ private[ui] class JobPagedTable(
val killLink = if (killEnabled) {
// SPARK-6846 this should be POST-only but YARN AM won't proxy POST
val killLinkUri = s"$basePath/jobs/job/kill/?id=${job.jobId}"
- <a href={killLinkUri}
+ <a href={killLinkUri} role="button"
data-kill-message={s"Are you sure you want to kill job ${job.jobId}
?"}
- class="kill-link float-end">(kill)</a>
+ class="btn btn-sm btn-outline-danger kill-link float-end">Kill</a>
} else {
Seq.empty
}
diff --git a/core/src/main/scala/org/apache/spark/ui/jobs/StageTable.scala
b/core/src/main/scala/org/apache/spark/ui/jobs/StageTable.scala
index 217bfff3b64a..aa5720bbf240 100644
--- a/core/src/main/scala/org/apache/spark/ui/jobs/StageTable.scala
+++ b/core/src/main/scala/org/apache/spark/ui/jobs/StageTable.scala
@@ -223,9 +223,9 @@ private[ui] class StagePagedTable(
val killLink = if (killEnabled) {
// SPARK-6846 this should be POST-only but YARN AM won't proxy POST
val killLinkUri = s"$basePathUri/stages/stage/kill/?id=${s.stageId}"
- <a href={killLinkUri}
+ <a href={killLinkUri} role="button"
data-kill-message={s"Are you sure you want to kill stage ${s.stageId}
?"}
- class="kill-link float-end">(kill)</a>
+ class="btn btn-sm btn-outline-danger kill-link float-end">Kill</a>
} else {
Seq.empty
}
diff --git a/docs/spark-standalone.md b/docs/spark-standalone.md
index ca31146be853..149c79105a58 100644
--- a/docs/spark-standalone.md
+++ b/docs/spark-standalone.md
@@ -776,13 +776,13 @@ once no executor is left. The Master's `/json/` endpoint
reports the same in the
Only applications whose driver reports that it can be held are annotated, per
the preconditions
described in [Web UI](web-ui.html#jobs-tab).
-Such an application also gets a **(hold)** link next to its **(kill)** link,
and a **(resume)**
-link while it is held. Holding stops requesting new executors for the
application and gracefully
+Such an application also gets a **Hold** button next to its **Kill** button,
and a **Resume**
+button while it is held. Holding stops requesting new executors for the
application and gracefully
decommissions the running ones; the application keeps its driver and the
shuffle output already
written, while cached blocks are lost and recomputed after resuming. Holding
and resuming require
-modify permissions, like killing. The links are gated by
`spark.ui.holdEnabled` twice: on the
+modify permissions, like killing. The buttons are gated by
`spark.ui.holdEnabled` twice: on the
Master, where setting it to false hides them for every application, and on
each application,
-whose own setting travels with its registration -- an application that
disabled it gets no links
+whose own setting travels with its registration -- an application that
disabled it gets no buttons
and its hold requests are rejected, while its hold status stays visible. The
same controls remain
available on the driver web UI.
diff --git a/docs/web-ui.md b/docs/web-ui.md
index 085e8be75537..47eac42971d4 100644
--- a/docs/web-ui.md
+++ b/docs/web-ui.md
@@ -70,11 +70,11 @@ The information displayed at the top of the page includes:
The current user, application start time, and total uptime are shown in the
footer at the
bottom of every page.
-When the application can be held, the summary shows an **Application** line
with a **(hold)**
-link; clicking it stops requesting new executors and gracefully decommissions
the running ones,
+When the application can be held, the summary shows an **Application** line
with a **Hold**
+button; clicking it stops requesting new executors and gracefully
decommissions the running ones,
so each finishes its tasks and then exits (unless
`spark.executor.decommission.forceKillTimeout`
is set, which kills a still-busy executor after that timeout). The line then
reads `Held` with a
-**(resume)** link that restores the executor requirement. Shuffle output
written before the hold
+**Resume** button that restores the executor requirement. Shuffle output
written before the hold
stays available, but cached blocks are recomputed after resuming, and an RDD
created while held
is sized against a default parallelism of 2 (no executors are alive) and keeps
that partition
count afterwards. Pipelined-shuffle jobs and workloads with long-running tasks
(streaming
@@ -117,7 +117,7 @@ completed, skipped, and failed). In [Fair scheduling
mode](job-scheduling.html#s
a table of [pool properties](job-scheduling.html#configuring-pool-properties)
is also shown.
Below the summary are the stages, grouped by status (active, pending,
completed, skipped, failed).
-An active stage shows a small **(kill)** link next to its description;
clicking it asks Spark
+An active stage shows a small **Kill** button next to its description;
clicking it asks Spark
to cancel that stage. Only failed stages show the failure reason. Click a
stage's description
to open its [Stage detail](#stage-detail) page.
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]