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]

Reply via email to