This is an automated email from the ASF dual-hosted git repository.

yao pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/master by this push:
     new aa968ee5a088 [SPARK-55779][UI] Add tooltip helper utilities for Spark 
Web UI
aa968ee5a088 is described below

commit aa968ee5a0884a17828c5dd99163e3348d979e0f
Author: Kent Yao <[email protected]>
AuthorDate: Wed Mar 4 08:48:07 2026 +0800

    [SPARK-55779][UI] Add tooltip helper utilities for Spark Web UI
    
    ### What changes were proposed in this pull request?
    
    This PR introduces helper utilities for tooltip markup generation in the 
Spark Web UI:
    
    **Scala helpers in `UIUtils.scala`:**
    - `tooltipSpan(content, text, placement)` — wraps content in a `<span>` 
with BS5 tooltip attributes
    - `tooltipLink(content, text, placement)` — wraps content in an `<a>` with 
BS5 tooltip attributes
    
    **JS helper in `stagepage.js`:**
    - `setTooltip(selector, text)` — sets BS5 tooltip attributes on a jQuery 
element
    
    These helpers replace 21 inline tooltip patterns across 10 Scala files and 
12 `.attr()` call chains in stagepage.js, reducing boilerplate and ensuring 
consistent tooltip markup.
    
    ### Why are the changes needed?
    
    Part of the Bootstrap 5 migration 
([SPARK-55760](https://issues.apache.org/jira/browse/SPARK-55760)). Tooltip 
markup was duplicated across many files with slight variations. Centralizing it:
    1. Reduces code duplication
    2. Ensures consistent BS5 tooltip attributes (`data-bs-toggle`, 
`data-bs-placement`, `title`)
    3. Makes future tooltip-related changes easier (single point of change)
    
    ### Does this PR introduce _any_ user-facing change?
    
    No. The rendered HTML is functionally identical — tooltips now consistently 
include `data-bs-placement="top"`.
    
    ### How was this patch tested?
    
    - Updated `UIUtilsSuite` to match new tooltip markup
    - All existing tests pass
    - Scalastyle and JS lint checks pass
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Yes, GitHub Copilot CLI was used.
    
    Closes #54588 from yaooqinn/SPARK-55779.
    
    Authored-by: Kent Yao <[email protected]>
    Signed-off-by: Kent Yao <[email protected]>
---
 .../org/apache/spark/ui/static/stagepage.js        | 64 ++++++++--------------
 .../spark/deploy/master/ui/ApplicationPage.scala   |  8 +--
 .../scala/org/apache/spark/ui/PagedTable.scala     | 17 +++---
 .../main/scala/org/apache/spark/ui/UIUtils.scala   | 26 ++++++---
 .../spark/ui/exec/ExecutorThreadDumpPage.scala     |  6 +-
 .../org/apache/spark/ui/jobs/AllJobsPage.scala     |  4 +-
 .../scala/org/apache/spark/ui/jobs/JobPage.scala   |  4 +-
 .../scala/org/apache/spark/ui/jobs/PoolTable.scala |  9 ++-
 .../apache/spark/ui/jobs/TaskThreadDumpPage.scala  |  6 +-
 .../scala/org/apache/spark/ui/UIUtilsSuite.scala   |  2 +-
 .../apache/spark/ui/storage/StoragePageSuite.scala |  2 +-
 .../apache/spark/streaming/ui/StreamingPage.scala  |  5 +-
 12 files changed, 66 insertions(+), 87 deletions(-)

diff --git a/core/src/main/resources/org/apache/spark/ui/static/stagepage.js 
b/core/src/main/resources/org/apache/spark/ui/static/stagepage.js
index ac5397419356..1f2142f4f808 100644
--- a/core/src/main/resources/org/apache/spark/ui/static/stagepage.js
+++ b/core/src/main/resources/org/apache/spark/ui/static/stagepage.js
@@ -26,6 +26,12 @@ import {
 
 export {setTaskThreadDumpEnabled};
 
+function setTooltip(selector, text) {
+  $(selector).attr("data-bs-toggle", "tooltip")
+    .attr("data-bs-placement", "top")
+    .attr("title", text);
+}
+
 var shouldBlockUI = true;
 var taskThreadDumpEnabled = false;
 
@@ -356,35 +362,19 @@ $(document).ready(function () {
     "<div id='executor_direct_mapped_pool_memory' 
class='executor-jvm-metrics-checkbox-div'><input type='checkbox' 
class='toggle-vis' id='executor-box-18' data-column='18' 
data-metrics-type='executor'> Peak Pool Memory Direct / Mapped</div>" +
     "</div>");
 
-  $('#scheduler_delay').attr("data-bs-toggle", "tooltip")
-    .attr("data-bs-placement", "top")
-    .attr("title", "Scheduler delay includes time to ship the task from the 
scheduler to the executor, and time to send " +
-      "the task result from the executor to the scheduler. If scheduler delay 
is large, consider decreasing the size of tasks or decreasing the size of task 
results.");
-  $('#task_deserialization_time').attr("data-bs-toggle", "tooltip")
-    .attr("data-bs-placement", "top")
-    .attr("title", "Time spent deserializing the task closure on the executor, 
including the time to read the broadcasted task.");
-  $('#shuffle_read_fetch_wait_time').attr("data-bs-toggle", "tooltip")
-    .attr("data-bs-placement", "top")
-    .attr("title", "Time that the task spent blocked waiting for shuffle data 
to be read from remote machines.");
-  $('#shuffle_remote_reads').attr("data-bs-toggle", "tooltip")
-    .attr("data-bs-placement", "top")
-    .attr("title", "Total shuffle bytes read from remote executors. This is a 
subset of the shuffle read bytes; the remaining shuffle data is read locally. 
");
-  $('#shuffle_write_time').attr("data-bs-toggle", "tooltip")
-    .attr("data-bs-placement", "top")
-    .attr("title", "Time that the task spent writing shuffle data.");
-  $('#result_serialization_time').attr("data-bs-toggle", "tooltip")
-    .attr("data-bs-placement", "top")
-    .attr("title", "Time spent serializing the task result on the executor 
before sending it back to the driver.");
-  $('#getting_result_time').attr("data-bs-toggle", "tooltip")
-    .attr("data-bs-placement", "top")
-    .attr("title", "Time that the driver spends fetching task results from 
workers. If this is large, consider decreasing the amount of data returned from 
each task.");
-  $('#peak_execution_memory').attr("data-bs-toggle", "tooltip")
-    .attr("data-bs-placement", "top")
-    .attr("title", "Execution memory refers to the memory used by internal 
data structures created during " +
-      "shuffles, aggregations and joins when Tungsten is enabled. The value of 
this accumulator " +
-      "should be approximately the sum of the peak sizes across all such data 
structures created " +
-      "in this task. For SQL jobs, this only tracks all unsafe operators, 
broadcast joins, and " +
-      "external sort.");
+  setTooltip('#scheduler_delay', "Scheduler delay includes time to ship the 
task from the scheduler to the executor, and time to send " +
+    "the task result from the executor to the scheduler. If scheduler delay is 
large, consider decreasing the size of tasks or decreasing the size of task 
results.");
+  setTooltip('#task_deserialization_time', "Time spent deserializing the task 
closure on the executor, including the time to read the broadcasted task.");
+  setTooltip('#shuffle_read_fetch_wait_time', "Time that the task spent 
blocked waiting for shuffle data to be read from remote machines.");
+  setTooltip('#shuffle_remote_reads', "Total shuffle bytes read from remote 
executors. This is a subset of the shuffle read bytes; the remaining shuffle 
data is read locally. ");
+  setTooltip('#shuffle_write_time', "Time that the task spent writing shuffle 
data.");
+  setTooltip('#result_serialization_time', "Time spent serializing the task 
result on the executor before sending it back to the driver.");
+  setTooltip('#getting_result_time', "Time that the driver spends fetching 
task results from workers. If this is large, consider decreasing the amount of 
data returned from each task.");
+  setTooltip('#peak_execution_memory', "Execution memory refers to the memory 
used by internal data structures created during " +
+    "shuffles, aggregations and joins when Tungsten is enabled. The value of 
this accumulator " +
+    "should be approximately the sum of the peak sizes across all such data 
structures created " +
+    "in this task. For SQL jobs, this only tracks all unsafe operators, 
broadcast joins, and " +
+    "external sort.");
   var tasksSummary = $("#parent-container");
   getStandAloneAppId(function (appId) {
     // rendering the UI page
@@ -624,27 +614,19 @@ $(document).ready(function () {
   
             
executorSummaryTableSelector.column(9).visible(dataToShow.showInputData);
             if (dataToShow.showInputData) {
-              $('#executor-summary-input').attr("data-bs-toggle", "tooltip")
-                .attr("data-bs-placement", "top")
-                .attr("title", "Bytes and records read from Hadoop or from 
Spark storage.");
+              setTooltip('#executor-summary-input', "Bytes and records read 
from Hadoop or from Spark storage.");
             }
             
executorSummaryTableSelector.column(10).visible(dataToShow.showOutputData);
             if (dataToShow.showOutputData) {
-              $('#executor-summary-output').attr("data-bs-toggle", "tooltip")
-                .attr("data-bs-placement", "top")
-                .attr("title", "Bytes and records written to Hadoop.");
+              setTooltip('#executor-summary-output', "Bytes and records 
written to Hadoop.");
             }
             
executorSummaryTableSelector.column(11).visible(dataToShow.showShuffleReadData);
             if (dataToShow.showShuffleReadData) {
-              $('#executor-summary-shuffle-read').attr("data-bs-toggle", 
"tooltip")
-                .attr("data-bs-placement", "top")
-                .attr("title", "Total shuffle bytes and records read (includes 
both data read locally and data read from remote executors).");
+              setTooltip('#executor-summary-shuffle-read', "Total shuffle 
bytes and records read (includes both data read locally and data read from 
remote executors).");
             }
             
executorSummaryTableSelector.column(12).visible(dataToShow.showShuffleWriteData);
             if (dataToShow.showShuffleWriteData) {
-              $('#executor-summary-shuffle-write').attr("data-bs-toggle", 
"tooltip")
-                .attr("data-bs-placement", "top")
-                .attr("title", "Bytes and records written to disk in order to 
be read by a shuffle in a future stage.");
+              setTooltip('#executor-summary-shuffle-write', "Bytes and records 
written to disk in order to be read by a shuffle in a future stage.");
             }
             
executorSummaryTableSelector.column(13).visible(dataToShow.showBytesSpilledData);
             
executorSummaryTableSelector.column(14).visible(dataToShow.showBytesSpilledData);
diff --git 
a/core/src/main/scala/org/apache/spark/deploy/master/ui/ApplicationPage.scala 
b/core/src/main/scala/org/apache/spark/deploy/master/ui/ApplicationPage.scala
index 886cff3d6b39..3de530f1d252 100644
--- 
a/core/src/main/scala/org/apache/spark/deploy/master/ui/ApplicationPage.scala
+++ 
b/core/src/main/scala/org/apache/spark/deploy/master/ui/ApplicationPage.scala
@@ -72,14 +72,12 @@ private[ui] class ApplicationPage(parent: MasterWebUI) 
extends WebUIPage("app")
             }
             </li>
             <li>
-              <span data-bs-toggle="tooltip" 
title={ToolTips.APPLICATION_EXECUTOR_LIMIT}
-                    data-bs-placement="top">
-                <strong>Executor Limit: </strong>
+              {UIUtils.tooltipSpan(
+                <xml:group><strong>Executor Limit: </strong>
                 {
                   if (app.getExecutorLimit == Int.MaxValue) "Unlimited" else 
app.getExecutorLimit
                 }
-                ({app.executors.size} granted)
-              </span>
+                ({app.executors.size} granted)</xml:group>, 
ToolTips.APPLICATION_EXECUTOR_LIMIT)}
             </li>
             <li>
               <strong>Executor Memory - Default Resource Profile:</strong>
diff --git a/core/src/main/scala/org/apache/spark/ui/PagedTable.scala 
b/core/src/main/scala/org/apache/spark/ui/PagedTable.scala
index 5ee482ed4343..726c79edab86 100644
--- a/core/src/main/scala/org/apache/spark/ui/PagedTable.scala
+++ b/core/src/main/scala/org/apache/spark/ui/PagedTable.scala
@@ -364,9 +364,9 @@ private[spark] trait PagedTable[T] {
 
           <th>
             <a href={headerLink}>
-              <span data-bs-toggle="tooltip" data-bs-placement="top" 
title={tooltip.getOrElse("")}>
-                {header}&nbsp;{Unparsed(arrow)}
-              </span>
+              {UIUtils.tooltipSpan(
+                <xml:group>{header}&nbsp;{Unparsed(arrow)}</xml:group>,
+                tooltip.getOrElse(""))}
             </a>
           </th>
         } else {
@@ -379,17 +379,14 @@ private[spark] trait PagedTable[T] {
 
             <th>
               <a href={headerLink}>
-                <span data-bs-toggle="tooltip"
-                  data-bs-placement="top" title={tooltip.getOrElse("")}>
-                  {header}
-                </span>
+                {UIUtils.tooltipSpan(<xml:group>{header}</xml:group>,
+                  tooltip.getOrElse(""))}
               </a>
             </th>
           } else {
             <th>
-              <span data-bs-toggle="tooltip" data-bs-placement="top" 
title={tooltip.getOrElse("")}>
-                {header}
-              </span>
+              {UIUtils.tooltipSpan(<xml:group>{header}</xml:group>,
+                tooltip.getOrElse(""))}
             </th>
           }
         }
diff --git a/core/src/main/scala/org/apache/spark/ui/UIUtils.scala 
b/core/src/main/scala/org/apache/spark/ui/UIUtils.scala
index 74cd4791399e..229561bc5081 100644
--- a/core/src/main/scala/org/apache/spark/ui/UIUtils.scala
+++ b/core/src/main/scala/org/apache/spark/ui/UIUtils.scala
@@ -431,9 +431,7 @@ private[spark] object UIUtils extends Logging {
         getTooltip(x._2) match {
           case Some(tooltip) =>
             <th width={colWidthAttr} class={getClass(x._2)}>
-              <span data-bs-toggle="tooltip" title={tooltip}>
-                {getHeaderContent(x._1)}
-              </span>
+              {tooltipSpan(getHeaderContent(x._1), tooltip)}
             </th>
           case None => <th width={colWidthAttr} 
class={getClass(x._2)}>{getHeaderContent(x._1)}</th>
         }
@@ -517,10 +515,8 @@ private[spark] object UIUtils extends Logging {
       <span id={if (forJob) "job-dag-viz" else "stage-dag-viz"}
             class="expand-dag-viz" data-forjob={forJob.toString}>
         <span class="expand-dag-viz-arrow arrow-closed"></span>
-        <a data-bs-toggle="tooltip" title={if (forJob) ToolTips.JOB_DAG else 
ToolTips.STAGE_DAG}
-           data-bs-placement="top">
-          DAG Visualization
-        </a>
+        {tooltipLink(<xml:group>DAG Visualization</xml:group>,
+          if (forJob) ToolTips.JOB_DAG else ToolTips.STAGE_DAG)}
       </span>
       <div id="dag-viz-graph"></div>
       <div id="dag-viz-metadata" style="display:none">
@@ -556,6 +552,22 @@ private[spark] object UIUtils extends Logging {
     </sup>
   }
 
+  /** Wrap content in a span with a Bootstrap 5 tooltip. */
+  def tooltipSpan(content: Seq[Node], text: String,
+      placement: String = "top"): Seq[Node] = {
+    <span data-bs-toggle="tooltip" data-bs-placement={placement} title={text}>
+      {content}
+    </span>
+  }
+
+  /** Create a link with a Bootstrap 5 tooltip. */
+  def tooltipLink(content: Seq[Node], text: String,
+      placement: String = "top"): Seq[Node] = {
+    <a data-bs-toggle="tooltip" data-bs-placement={placement} title={text}>
+      {content}
+    </a>
+  }
+
   /**
    * Returns HTML rendering of a job or stage description. It will try to 
parse the string as HTML
    * and make sure that it only contains anchors with root-relative links. 
Otherwise,
diff --git 
a/core/src/main/scala/org/apache/spark/ui/exec/ExecutorThreadDumpPage.scala 
b/core/src/main/scala/org/apache/spark/ui/exec/ExecutorThreadDumpPage.scala
index b4a205e09fb8..fb971b2078ad 100644
--- a/core/src/main/scala/org/apache/spark/ui/exec/ExecutorThreadDumpPage.scala
+++ b/core/src/main/scala/org/apache/spark/ui/exec/ExecutorThreadDumpPage.scala
@@ -117,10 +117,8 @@ private[ui] class ExecutorThreadDumpPage(
               <th data-action="collapseAllThreadStackTrace" 
data-toggle-button="false">Thread Name</th>
               <th data-action="collapseAllThreadStackTrace" 
data-toggle-button="false">Thread State</th>
               <th data-action="collapseAllThreadStackTrace" 
data-toggle-button="false">
-                <span data-bs-toggle="tooltip" data-bs-placement="top"
-                      title="Objects whose lock the thread currently holds">
-                  Thread Locks
-                </span>
+                {UIUtils.tooltipSpan(<xml:group>Thread Locks</xml:group>,
+                  "Objects whose lock the thread currently holds")}
               </th>
             </thead>
             <tbody>{dumpRows}</tbody>
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 a270d4b71ac3..019bebcadeaa 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
@@ -199,9 +199,7 @@ private[ui] class AllJobsPage(parent: JobsTab, store: 
AppStatusStore) extends We
 
     <span class="expand-application-timeline">
       <span class="expand-application-timeline-arrow arrow-closed"></span>
-      <a data-bs-toggle="tooltip" title={ToolTips.JOB_TIMELINE} 
data-bs-placement="top">
-        Event Timeline
-      </a>
+      {UIUtils.tooltipLink(<xml:group>Event Timeline</xml:group>, 
ToolTips.JOB_TIMELINE)}
     </span> ++
     <div id="application-timeline" class="collapsed">
       {
diff --git a/core/src/main/scala/org/apache/spark/ui/jobs/JobPage.scala 
b/core/src/main/scala/org/apache/spark/ui/jobs/JobPage.scala
index ef9ce225d08a..77869163c8d0 100644
--- a/core/src/main/scala/org/apache/spark/ui/jobs/JobPage.scala
+++ b/core/src/main/scala/org/apache/spark/ui/jobs/JobPage.scala
@@ -179,9 +179,7 @@ private[ui] class JobPage(parent: JobsTab, store: 
AppStatusStore) extends WebUIP
 
     <span class="expand-job-timeline">
       <span class="expand-job-timeline-arrow arrow-closed"></span>
-      <a data-bs-toggle="tooltip" title={ToolTips.STAGE_TIMELINE} 
data-bs-placement="top">
-        Event Timeline
-      </a>
+      {UIUtils.tooltipLink(<xml:group>Event Timeline</xml:group>, 
ToolTips.STAGE_TIMELINE)}
     </span> ++
     <div id="job-timeline" class="collapsed">
       {
diff --git a/core/src/main/scala/org/apache/spark/ui/jobs/PoolTable.scala 
b/core/src/main/scala/org/apache/spark/ui/jobs/PoolTable.scala
index cb4ed7190ad7..64dc961f77a8 100644
--- a/core/src/main/scala/org/apache/spark/ui/jobs/PoolTable.scala
+++ b/core/src/main/scala/org/apache/spark/ui/jobs/PoolTable.scala
@@ -37,13 +37,12 @@ private[ui] class PoolTable(pools: Map[Schedulable, 
PoolData], parent: StagesTab
         <tr>
           <th>Pool Name</th>
           <th>
-            <span data-bs-toggle="tooltip" data-bs-placement="top"
-              title="Pool's minimum share of CPU
-             cores">Minimum Share</span>
+            {UIUtils.tooltipSpan(<xml:group>Minimum Share</xml:group>,
+              "Pool's minimum share of CPU cores")}
           </th>
           <th>
-            <span data-bs-toggle="tooltip" data-bs-placement="top" 
title="Pool's share of cluster
-             resources relative to others">Pool Weight</span>
+            {UIUtils.tooltipSpan(<xml:group>Pool Weight</xml:group>,
+              "Pool's share of cluster resources relative to others")}
           </th>
           <th>Active Stages</th>
           <th>Running Tasks</th>
diff --git 
a/core/src/main/scala/org/apache/spark/ui/jobs/TaskThreadDumpPage.scala 
b/core/src/main/scala/org/apache/spark/ui/jobs/TaskThreadDumpPage.scala
index ac595b810d62..60677216ed93 100644
--- a/core/src/main/scala/org/apache/spark/ui/jobs/TaskThreadDumpPage.scala
+++ b/core/src/main/scala/org/apache/spark/ui/jobs/TaskThreadDumpPage.scala
@@ -78,10 +78,8 @@ private[spark] class TaskThreadDumpPage(
               <th>Thread Name</th>
               <th>Thread State</th>
               <th>
-                <span data-bs-toggle="tooltip" data-bs-placement="top"
-                      title="Objects whose lock the thread currently holds">
-                  Thread Locks
-                </span>
+                {UIUtils.tooltipSpan(<xml:group>Thread Locks</xml:group>,
+                  "Objects whose lock the thread currently holds")}
               </th>
             </thead>
             <tbody>
diff --git a/core/src/test/scala/org/apache/spark/ui/UIUtilsSuite.scala 
b/core/src/test/scala/org/apache/spark/ui/UIUtilsSuite.scala
index ebb22de7c538..896e5b828017 100644
--- a/core/src/test/scala/org/apache/spark/ui/UIUtilsSuite.scala
+++ b/core/src/test/scala/org/apache/spark/ui/UIUtilsSuite.scala
@@ -165,7 +165,7 @@ class UIUtilsSuite extends SparkFunSuite {
         <thead>
           <th width="" class="">{header(0)}</th>
           <th width="" class="">
-              <span data-bs-toggle="tooltip" title="tooltip">
+              <span data-bs-toggle="tooltip" data-bs-placement="top" 
title="tooltip">
                 {header(1)}
               </span>
           </th>
diff --git 
a/core/src/test/scala/org/apache/spark/ui/storage/StoragePageSuite.scala 
b/core/src/test/scala/org/apache/spark/ui/storage/StoragePageSuite.scala
index ada799c863ee..26ed10fc69f5 100644
--- a/core/src/test/scala/org/apache/spark/ui/storage/StoragePageSuite.scala
+++ b/core/src/test/scala/org/apache/spark/ui/storage/StoragePageSuite.scala
@@ -89,7 +89,7 @@ class StoragePageSuite extends SparkFunSuite {
         }
       }
     }
-    assert((xmlNodes \\ "th").map(_.text) === headerRow.map(_.text))
+    assert((xmlNodes \\ "th").map(_.text.trim) === headerRow.map(_.text.trim))
 
     assert((xmlNodes \\ "tr").size === 3)
     assert(((xmlNodes \\ "tr")(0) \\ "td").map(_.text.trim) ===
diff --git 
a/streaming/src/main/scala/org/apache/spark/streaming/ui/StreamingPage.scala 
b/streaming/src/main/scala/org/apache/spark/streaming/ui/StreamingPage.scala
index e2caf5256226..11ab7ccc4e61 100644
--- a/streaming/src/main/scala/org/apache/spark/streaming/ui/StreamingPage.scala
+++ b/streaming/src/main/scala/org/apache/spark/streaming/ui/StreamingPage.scala
@@ -281,9 +281,8 @@ private[ui] class StreamingPage(parent: StreamingTab)
                 if (hasStream) {
                   <span class="expand-input-rate">
                     <span class="expand-input-rate-arrow arrow-closed"></span>
-                    <a data-bs-toggle="tooltip" title="Show/hide details of 
each receiver" data-bs-placement="top">
-                      <strong>Input Rate</strong>
-                    </a>
+                    {SparkUIUtils.tooltipLink(<xml:group><strong>Input 
Rate</strong></xml:group>,
+                      "Show/hide details of each receiver")}
                   </span>
                 } else {
                   <strong>Input Rate</strong>


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to