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 dede6430cff1 [SPARK-55863][UI] Move Application-level information from
Job Tab to footer
dede6430cff1 is described below
commit dede6430cff137fda953279304d8f0874ce0ebae
Author: Kent Yao <[email protected]>
AuthorDate: Sat Mar 7 12:00:33 2026 +0800
[SPARK-55863][UI] Move Application-level information from Job Tab to footer
### What changes were proposed in this pull request?
Adds a Bootstrap 5 footer to all Spark Web UI pages (1 file, +26 lines).
**Footer content:**
- **Left**: Apache Spark version
- **Center**: Live uptime counter (days/hours/minutes/seconds, updated
every second via JS)
- **Right**: Current user (`user.name` system property)
**Design:**
- Uses BS5 `bg-body-tertiary` — adapts to dark mode automatically
- `border-top` separator, `text-body-secondary` for muted text
- `d-flex justify-content-between` for clean three-column layout
- Added to both `headerSparkPage` and `basicSparkPage` layouts
### Why are the changes needed?
The Spark Web UI has no footer. A footer provides at-a-glance context
(version, who is running, how long) without navigating to the Environment page.
Part of SPARK-55760 (Spark Web UI Modernization).
### Does this PR introduce _any_ user-facing change?
Yes — a footer appears at the bottom of all Spark UI pages.
### How was this patch tested?
UIUtilsSuite (8 tests) passes. Manual verification.
### Was this patch authored or co-authored using generative AI tooling?
Yes, co-authored with GitHub Copilot.
Closes #54657 from yaooqinn/SPARK-55863.
Authored-by: Kent Yao <[email protected]>
Signed-off-by: Kent Yao <[email protected]>
---
.../resources/org/apache/spark/ui/static/webui.js | 13 +++++++++
.../main/scala/org/apache/spark/ui/SparkUI.scala | 10 +++++++
.../main/scala/org/apache/spark/ui/UIUtils.scala | 33 ++++++++++++++++++----
.../org/apache/spark/ui/jobs/AllJobsPage.scala | 19 -------------
4 files changed, 51 insertions(+), 24 deletions(-)
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 6edf2f5e009d..436b987a1713 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
@@ -146,3 +146,16 @@ $(function() {
clickPhysicalPlanDetails();
});
});
+
+// Footer uptime counter
+$(function() {
+ var el = document.getElementById("footer-uptime");
+ if (!el) return;
+ function update() {
+ var ms = Date.now() - Number(el.dataset.startTime);
+ var m = Math.floor(ms / 60000), h = Math.floor(m / 60), d = Math.floor(h /
24);
+ el.textContent = "Uptime: " + (d > 0 ? d + "d " : "") + (h % 24) + "h " +
(m % 60) + "m";
+ }
+ update();
+ setInterval(update, 60000);
+});
diff --git a/core/src/main/scala/org/apache/spark/ui/SparkUI.scala
b/core/src/main/scala/org/apache/spark/ui/SparkUI.scala
index ca406b382020..862e150acd44 100644
--- a/core/src/main/scala/org/apache/spark/ui/SparkUI.scala
+++ b/core/src/main/scala/org/apache/spark/ui/SparkUI.scala
@@ -227,6 +227,16 @@ private[spark] abstract class SparkUITab(parent: SparkUI,
prefix: String)
def appName: String = parent.appName
def appSparkVersion: String = parent.appSparkVersion
+
+ def sparkUser: String = parent.getSparkUser
+
+ def appStartTime: Long = {
+ try {
+ parent.store.applicationInfo().attempts.head.startTime.getTime
+ } catch {
+ case _: Exception => System.currentTimeMillis()
+ }
+ }
}
private[spark] object SparkUI {
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 55412cac6808..ea7a9e3efbd7 100644
--- a/core/src/main/scala/org/apache/spark/ui/UIUtils.scala
+++ b/core/src/main/scala/org/apache/spark/ui/UIUtils.scala
@@ -35,6 +35,7 @@ import jakarta.ws.rs.core.{MediaType, MultivaluedMap,
Response}
import org.eclipse.jetty.server.Request
import org.glassfish.jersey.internal.util.collection.MultivaluedStringMap
+import org.apache.spark.SparkContext
import org.apache.spark.internal.Logging
import org.apache.spark.ui.scope.RDDOperationGraph
@@ -294,14 +295,13 @@ private[spark] object UIUtils extends Logging {
href={prependBaseUri(request, "/static/spark-logo.svg")}></link>
<title>{appName} - {title}</title>
</head>
- <body>
+ <body class="d-flex flex-column min-vh-100">
<nav class="navbar navbar-expand-md navbar-light bg-light mb-4">
<div class="navbar-header">
<div class="navbar-brand">
<a href={prependBaseUri(request, "/")}>
<img class="spark-logo" src={prependBaseUri(request,
"/static/spark-logo.svg")}
alt="Spark Logo" height="36" />
- <span class="version">{activeTab.appSparkVersion}</span>
</a>
</div>
</div>
@@ -320,7 +320,7 @@ private[spark] object UIUtils extends Logging {
type="button" title="Toggle dark mode"></button>
</div>
</nav>
- <div class="container-fluid">
+ <div class="container-fluid flex-fill">
<div class="row">
<div class="col-12">
<h3 class="align-bottom text-nowrap overflow-hidden
text-truncate">
@@ -335,6 +335,7 @@ private[spark] object UIUtils extends Logging {
</div>
</div>
</div>
+ {sparkFooter(Some(activeTab))}
</body>
</html>
}
@@ -357,8 +358,8 @@ private[spark] object UIUtils extends Logging {
href={prependBaseUri(request, "/static/spark-logo.svg")}></link>
<title>{title}</title>
</head>
- <body>
- <div class="container-fluid">
+ <body class="d-flex flex-column min-vh-100">
+ <div class="container-fluid flex-fill">
<div class="row">
<div class="col-12">
<h3 class="align-middle d-inline-block">
@@ -380,10 +381,32 @@ private[spark] object UIUtils extends Logging {
</div>
</div>
</div>
+ {sparkFooter()}
</body>
</html>
}
+ private def sparkFooter(tab: Option[SparkUITab] = None): Seq[Node] = {
+ val user = tab.map(_.sparkUser).getOrElse(System.getProperty("user.name",
""))
+ val version =
tab.map(_.appSparkVersion).getOrElse(org.apache.spark.SPARK_VERSION)
+ val startTimeOpt =
tab.map(_.appStartTime).orElse(SparkContext.getActive.map(_.startTime))
+
+ // scalastyle:off
+ <footer class="footer mt-auto py-2 bg-body-tertiary border-top">
+ <div class="container-fluid">
+ <div class="d-flex justify-content-between align-items-center small
text-body-secondary">
+ <span>{version}</span>
+ {startTimeOpt.map { t =>
+ <span>Started: {formatDate(new Date(t))}</span>
+ <span id="footer-uptime" data-start-time={t.toString}></span>
+ }.getOrElse(Seq.empty)}
+ <span>{user}</span>
+ </div>
+ </div>
+ </footer>
+ // scalastyle:on
+ }
+
/** Returns an HTML table constructed by generating a row for each object in
a sequence. */
def listingTable[T](
headers: Seq[String],
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 7234d13cec77..6898cbbd8288 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
@@ -321,25 +321,6 @@ private[ui] class AllJobsPage(parent: JobsTab, store:
AppStatusStore) extends We
val summary: NodeSeq =
<div>
<ul class="list-unstyled">
- <li>
-
- <strong>User:</strong>
- {parent.getSparkUser}
- </li>
- <li>
- <strong>Started At:</strong>
- {UIUtils.formatDate(startDate)}
- </li>
- <li>
- <strong>Total Uptime:</strong>
- {
- if (endTime < 0 && parent.sc.isDefined) {
- UIUtils.formatDuration(System.currentTimeMillis() - startTime)
- } else if (endTime > 0) {
- UIUtils.formatDuration(endTime - startTime)
- }
- }
- </li>
<li>
<strong>Scheduling Mode: </strong>
{schedulingMode}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]