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]

Reply via email to