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]