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 f2ff4a2f434d [SPARK-55764][UI] Use delegated event listener for 
Bootstrap 5 Tooltip lazy initialization
f2ff4a2f434d is described below

commit f2ff4a2f434de82cbb7c07f323bd57392ce21812
Author: Kent Yao <[email protected]>
AuthorDate: Mon Mar 2 23:37:23 2026 +0800

    [SPARK-55764][UI] Use delegated event listener for Bootstrap 5 Tooltip lazy 
initialization
    
    ### What changes were proposed in this pull request?
    
    Replace eager tooltip initialization on `DOMContentLoaded` with a single 
delegated `mouseover` event listener that lazily creates `bootstrap.Tooltip` 
instances on first hover.
    
    Changes:
    - `initialize-tooltips.js`: Replace `DOMContentLoaded` + `querySelectorAll` 
eager init with a single delegated `mouseover` listener on `document`
    - `stagepage.js`: Remove 6 redundant `new bootstrap.Tooltip()` calls
    - `historypage.js`: Remove 1 redundant tooltip init after DataTable render
    - `executorspage.js`: Remove 2 redundant tooltip inits after DataTable 
render
    - `timeline-view.js`: Use `getOrCreateInstance` for `show()` calls so 
programmatic tooltip display works without prior initialization
    
    ### Why are the changes needed?
    
    After the Bootstrap 5 upgrade (SPARK-55753), tooltips are eagerly 
initialized on `DOMContentLoaded` via `initialize-tooltips.js`, plus scattered 
`new bootstrap.Tooltip()` re-initialization calls in 5+ JS files after dynamic 
content renders (DataTables, timeline, etc.). This is fragile and error-prone — 
dynamically rendered elements (e.g., vis.js timeline items) are not present at 
`DOMContentLoaded`, so their tooltips silently fail.
    
    The delegated listener approach:
    - **Single source of truth** — no per-page boilerplate or re-init calls
    - **Handles dynamic content automatically** — DataTables, DAG viz, timeline 
items all work without explicit re-initialization
    - **Better performance** — only creates Tooltip for elements actually 
hovered
    - **Simpler code** — removed 9 scattered tooltip init calls
    - **Fixes timeline tooltips** — `getOrCreateInstance` ensures tooltips work 
on vis.js items that were not present at page load
    
    ### Does this PR introduce _any_ user-facing change?
    
    No. Tooltips behave identically — they appear on hover with the same 
content and positioning.
    
    ### How was this patch tested?
    
    - ESLint passes
    - 77 Scala UI tests pass (`core/testOnly org.apache.spark.ui.*`)
    - Manual verification via `spark-shell`: all pages (Jobs, Stages, Storage, 
Executors, SQL) serve correct BS5 tooltip markup and `initialize-tooltips.js` 
contains the lazy listener
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Yes, GitHub Copilot was used.
    
    Closes #54560 from yaooqinn/SPARK-55764.
    
    Authored-by: Kent Yao <[email protected]>
    Signed-off-by: Kent Yao <[email protected]>
---
 .../resources/org/apache/spark/ui/static/executorspage.js  |  2 --
 .../resources/org/apache/spark/ui/static/historypage.js    |  1 -
 .../org/apache/spark/ui/static/initialize-tooltips.js      | 14 +++++++++-----
 .../main/resources/org/apache/spark/ui/static/stagepage.js |  6 ------
 .../resources/org/apache/spark/ui/static/timeline-view.js  |  6 +++---
 5 files changed, 12 insertions(+), 17 deletions(-)

diff --git 
a/core/src/main/resources/org/apache/spark/ui/static/executorspage.js 
b/core/src/main/resources/org/apache/spark/ui/static/executorspage.js
index 12f3d0a5f6a4..9f7ff6db7494 100644
--- a/core/src/main/resources/org/apache/spark/ui/static/executorspage.js
+++ b/core/src/main/resources/org/apache/spark/ui/static/executorspage.js
@@ -623,7 +623,6 @@ $(document).ready(function () {
         
execDataTable.column('executorLogsCol:name').visible(logsExist(response));
         
execDataTable.column('threadDumpCol:name').visible(getThreadDumpEnabled());
         
execDataTable.column('heapHistogramCol:name').visible(getHeapHistogramEnabled());
-        $('#active-executors [data-bs-toggle="tooltip"]').each(function() { 
new bootstrap.Tooltip(this); });
     
         // This section should be visible once API gives the response.
         $('.active-process-container').hide();
@@ -751,7 +750,6 @@ $(document).ready(function () {
         };
 
         sumDataTable = $(sumSelector).DataTable(sumConf);
-        $('#execSummary [data-bs-toggle="tooltip"]').each(function() { new 
bootstrap.Tooltip(this); });
   
         $("#showAdditionalMetrics").append(
           "<div><a id='additionalMetrics' class='collapse-table'>" +
diff --git a/core/src/main/resources/org/apache/spark/ui/static/historypage.js 
b/core/src/main/resources/org/apache/spark/ui/static/historypage.js
index c5f3a0bf5d3e..20734eb69e57 100644
--- a/core/src/main/resources/org/apache/spark/ui/static/historypage.js
+++ b/core/src/main/resources/org/apache/spark/ui/static/historypage.js
@@ -265,7 +265,6 @@ $(document).ready(function() {
       historySummary.append(apps);
       apps.DataTable(conf);
       sibling.after(historySummary);
-      $('#history-summary [data-bs-toggle="tooltip"]').each(function() { new 
bootstrap.Tooltip(this); });
     });
   });
 });
diff --git 
a/core/src/main/resources/org/apache/spark/ui/static/initialize-tooltips.js 
b/core/src/main/resources/org/apache/spark/ui/static/initialize-tooltips.js
index 6533787415e0..592724f90182 100644
--- a/core/src/main/resources/org/apache/spark/ui/static/initialize-tooltips.js
+++ b/core/src/main/resources/org/apache/spark/ui/static/initialize-tooltips.js
@@ -15,9 +15,13 @@
  * limitations under the License.
  */
 
-document.addEventListener("DOMContentLoaded", function() {
-  document.querySelectorAll("[data-bs-toggle=tooltip]").forEach(function(el) {
-    new bootstrap.Tooltip(el, {container: 'body'});
-  });
-});
+// Lazily initialize Bootstrap 5 Tooltips on first hover via event delegation.
+// This single listener replaces per-page boilerplate and handles dynamic 
content.
+document.addEventListener("mouseover", function(e) {
+  var el = e.target.closest("[data-bs-toggle=tooltip]");
+  if (el && !bootstrap.Tooltip.getInstance(el)) {
+    var tt = new bootstrap.Tooltip(el, {container: "body"});
+    tt.show();
+  }
+}, {passive: true});
 
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 ba573c0535f3..ac5397419356 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
@@ -385,7 +385,6 @@ $(document).ready(function () {
       "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.");
-  document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(function(el) 
{ new bootstrap.Tooltip(el); });
   var tasksSummary = $("#parent-container");
   getStandAloneAppId(function (appId) {
     // rendering the UI page
@@ -622,35 +621,30 @@ $(document).ready(function () {
             };
             executorSummaryTableSelector =
               $("#summary-executor-table").DataTable(executorSummaryConf);
-            $('#parent-container [data-bs-toggle="tooltip"]').each(function() 
{ new bootstrap.Tooltip(this); });
   
             
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.");
-              new 
bootstrap.Tooltip(document.getElementById('executor-summary-input'));
             }
             
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.");
-              new 
bootstrap.Tooltip(document.getElementById('executor-summary-output'));
             }
             
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).");
-              new 
bootstrap.Tooltip(document.getElementById('executor-summary-shuffle-read'));
             }
             
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.");
-              new 
bootstrap.Tooltip(document.getElementById('executor-summary-shuffle-write'));
             }
             
executorSummaryTableSelector.column(13).visible(dataToShow.showBytesSpilledData);
             
executorSummaryTableSelector.column(14).visible(dataToShow.showBytesSpilledData);
diff --git 
a/core/src/main/resources/org/apache/spark/ui/static/timeline-view.js 
b/core/src/main/resources/org/apache/spark/ui/static/timeline-view.js
index 2e368fcbd596..5731ab3e931d 100644
--- a/core/src/main/resources/org/apache/spark/ui/static/timeline-view.js
+++ b/core/src/main/resources/org/apache/spark/ui/static/timeline-view.js
@@ -94,7 +94,7 @@ function drawApplicationTimeline(groupArray, eventObjArray, 
startTime, offset) {
         function() {
           
$(getSelectorForJobEntry(getIdForJobEntry(this))).addClass("corresponding-item-hover");
           var el = $($(this).find("div.application-timeline-content")[0])[0];
-          var tt = bootstrap.Tooltip.getInstance(el); if (tt) tt.show();
+          var tt = bootstrap.Tooltip.getOrCreateInstance(el, {container: 
"body"}); tt.show();
         },
         function() {
           
$(getSelectorForJobEntry(getIdForJobEntry(this))).removeClass("corresponding-item-hover");
@@ -192,7 +192,7 @@ function drawJobTimeline(groupArray, eventObjArray, 
startTime, offset) {
           $(getSelectorForStageEntry(getStageIdAndAttemptForStageEntry(this)))
             .addClass("corresponding-item-hover");
           $($(this).find("div.job-timeline-content")[0]).each(function() {
-            var tt = bootstrap.Tooltip.getInstance(this); if (tt) tt.show();
+            var tt = bootstrap.Tooltip.getOrCreateInstance(this, {container: 
"body"}); tt.show();
           });
         },
         function() {
@@ -321,7 +321,7 @@ function setupExecutorEventAction() {
     $(this).hover(
       function() {
         var el = $($(this).find(".executor-event-content")[0])[0];
-        var tt = bootstrap.Tooltip.getInstance(el); if (tt) tt.show();
+        var tt = bootstrap.Tooltip.getOrCreateInstance(el, {container: 
"body"}); tt.show();
       },
       function() {
         var el = $($(this).find(".executor-event-content")[0])[0];


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

Reply via email to