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 70e24bd0e7ecd..6dbe63b564e69 100644
--- a/core/src/main/scala/org/apache/spark/ui/UIUtils.scala
+++ b/core/src/main/scala/org/apache/spark/ui/UIUtils.scala
@@ -309,9 +309,13 @@ private[spark] object UIUtils extends Logging {
data: Iterable[T],
fixedWidth: Boolean = false,
id: Option[String] = None,
+ // When headerClasses is not empty, it should have the same length as headers parameter
headerClasses: Seq[String] = Seq.empty,
stripeRowsWithCss: Boolean = true,
- sortable: Boolean = true): Seq[Node] = {
+ sortable: Boolean = true,
+ // The tooltip information could be None, which indicates header does not have a tooltip.
+ // When tooltipHeaders is not empty, it should have the same length as headers parameter
+ tooltipHeaders: Seq[Option[String]] = Seq.empty): Seq[Node] = {
val listingTableClass = {
val _tableClass = if (stripeRowsWithCss) TABLE_CLASS_STRIPED else TABLE_CLASS_NOT_STRIPED
@@ -332,6 +336,14 @@ private[spark] object UIUtils extends Logging {
}
}
+ def getTooltip(index: Int): Option[String] = {
+ if (index < tooltipHeaders.size) {
+ tooltipHeaders(index)
+ } else {
+ None
+ }
+ }
+
val newlinesInHeader = headers.exists(_.contains("\n"))
def getHeaderContent(header: String): Seq[Node] = {
if (newlinesInHeader) {
@@ -345,7 +357,15 @@ private[spark] object UIUtils extends Logging {
val headerRow: Seq[Node] = {
headers.view.zipWithIndex.map { x =>
-
{getHeaderContent(x._1)} |
+ getTooltip(x._2) match {
+ case Some(tooltip) =>
+
+
+ {getHeaderContent(x._1)}
+
+ |
+ case None => {getHeaderContent(x._1)} |
+ }
}
}
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 de105b6f188f5..82773e3cc6860 100644
--- a/core/src/test/scala/org/apache/spark/ui/UIUtilsSuite.scala
+++ b/core/src/test/scala/org/apache/spark/ui/UIUtilsSuite.scala
@@ -18,6 +18,7 @@
package org.apache.spark.ui
import scala.xml.{Node, Text}
+import scala.xml.Utility.trim
import org.apache.spark.SparkFunSuite
@@ -129,6 +130,55 @@ class UIUtilsSuite extends SparkFunSuite {
assert(decoded1 === decodeURLParameter(decoded1))
}
+ test("listingTable with tooltips") {
+
+ def generateDataRowValue: String => Seq[Node] = row => {row}
+ val header = Seq("Header1", "Header2")
+ val data = Seq("Data1", "Data2")
+ val tooltip = Seq(None, Some("tooltip"))
+
+ val generated = listingTable(header, generateDataRowValue, data, tooltipHeaders = tooltip)
+
+ val expected: Node =
+
+
+ | {header(0)} |
+
+
+ {header(1)}
+
+ |
+
+
+ {data.map(generateDataRowValue)}
+
+
+
+ assert(trim(generated(0)) == trim(expected))
+ }
+
+ test("listingTable without tooltips") {
+
+ def generateDataRowValue: String => Seq[Node] = row => {row}
+ val header = Seq("Header1", "Header2")
+ val data = Seq("Data1", "Data2")
+
+ val generated = listingTable(header, generateDataRowValue, data)
+
+ val expected =
+
+
+ | {header(0)} |
+ {header(1)} |
+
+
+ {data.map(generateDataRowValue)}
+
+
+
+ assert(trim(generated(0)) == trim(expected))
+ }
+
private def verify(
desc: String,
expected: Node,
diff --git a/sql/hive-thriftserver/src/main/scala/org/apache/spark/sql/hive/thriftserver/ui/ThriftServerPage.scala b/sql/hive-thriftserver/src/main/scala/org/apache/spark/sql/hive/thriftserver/ui/ThriftServerPage.scala
index 261e8fc912eb9..0232fdba97b74 100644
--- a/sql/hive-thriftserver/src/main/scala/org/apache/spark/sql/hive/thriftserver/ui/ThriftServerPage.scala
+++ b/sql/hive-thriftserver/src/main/scala/org/apache/spark/sql/hive/thriftserver/ui/ThriftServerPage.scala
@@ -26,6 +26,7 @@ import org.apache.commons.text.StringEscapeUtils
import org.apache.spark.internal.Logging
import org.apache.spark.sql.hive.thriftserver.HiveThriftServer2.{ExecutionInfo, ExecutionState, SessionInfo}
+import org.apache.spark.sql.hive.thriftserver.ui.ToolTips._
import org.apache.spark.ui._
import org.apache.spark.ui.UIUtils._
@@ -72,6 +73,10 @@ private[ui] class ThriftServerPage(parent: ThriftServerTab) extends WebUIPage(""
val table = if (numStatement > 0) {
val headerRow = Seq("User", "JobID", "GroupID", "Start Time", "Finish Time", "Close Time",
"Execution Time", "Duration", "Statement", "State", "Detail")
+ val tooltips = Seq(None, None, None, None, Some(THRIFT_SERVER_FINISH_TIME),
+ Some(THRIFT_SERVER_CLOSE_TIME), Some(THRIFT_SERVER_EXECUTION),
+ Some(THRIFT_SERVER_DURATION), None, None, None)
+ assert(headerRow.length == tooltips.length)
val dataRows = listener.getExecutionList.sortBy(_.startTimestamp).reverse
def generateDataRow(info: ExecutionInfo): Seq[Node] = {
@@ -100,7 +105,7 @@ private[ui] class ThriftServerPage(parent: ThriftServerTab) extends WebUIPage(""
}
Some(UIUtils.listingTable(headerRow, generateDataRow,
- dataRows, false, None, Seq(null), false))
+ dataRows, false, None, Seq(null), false, tooltipHeaders = tooltips))
} else {
None
}
diff --git a/sql/hive-thriftserver/src/main/scala/org/apache/spark/sql/hive/thriftserver/ui/ThriftServerSessionPage.scala b/sql/hive-thriftserver/src/main/scala/org/apache/spark/sql/hive/thriftserver/ui/ThriftServerSessionPage.scala
index 81df1304085e8..55804721fbb50 100644
--- a/sql/hive-thriftserver/src/main/scala/org/apache/spark/sql/hive/thriftserver/ui/ThriftServerSessionPage.scala
+++ b/sql/hive-thriftserver/src/main/scala/org/apache/spark/sql/hive/thriftserver/ui/ThriftServerSessionPage.scala
@@ -26,6 +26,7 @@ import org.apache.commons.text.StringEscapeUtils
import org.apache.spark.internal.Logging
import org.apache.spark.sql.hive.thriftserver.HiveThriftServer2.{ExecutionInfo, ExecutionState}
+import org.apache.spark.sql.hive.thriftserver.ui.ToolTips._
import org.apache.spark.ui._
import org.apache.spark.ui.UIUtils._
@@ -81,6 +82,10 @@ private[ui] class ThriftServerSessionPage(parent: ThriftServerTab)
val table = if (numStatement > 0) {
val headerRow = Seq("User", "JobID", "GroupID", "Start Time", "Finish Time", "Close Time",
"Execution Time", "Duration", "Statement", "State", "Detail")
+ val tooltips = Seq(None, None, None, None, Some(THRIFT_SERVER_FINISH_TIME),
+ Some(THRIFT_SERVER_CLOSE_TIME), Some(THRIFT_SERVER_EXECUTION),
+ Some(THRIFT_SERVER_DURATION), None, None, None)
+ assert(headerRow.length == tooltips.length)
val dataRows = executionList.sortBy(_.startTimestamp).reverse
def generateDataRow(info: ExecutionInfo): Seq[Node] = {
@@ -109,7 +114,7 @@ private[ui] class ThriftServerSessionPage(parent: ThriftServerTab)
}
Some(UIUtils.listingTable(headerRow, generateDataRow,
- dataRows, false, None, Seq(null), false))
+ dataRows, false, None, Seq(null), false, tooltipHeaders = tooltips))
} else {
None
}
diff --git a/sql/hive-thriftserver/src/main/scala/org/apache/spark/sql/hive/thriftserver/ui/ToolTips.scala b/sql/hive-thriftserver/src/main/scala/org/apache/spark/sql/hive/thriftserver/ui/ToolTips.scala
new file mode 100644
index 0000000000000..1990b8f2d3285
--- /dev/null
+++ b/sql/hive-thriftserver/src/main/scala/org/apache/spark/sql/hive/thriftserver/ui/ToolTips.scala
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.spark.sql.hive.thriftserver.ui
+
+private[ui] object ToolTips {
+ val THRIFT_SERVER_FINISH_TIME =
+ "Execution finish time, before fetching the results"
+
+ val THRIFT_SERVER_CLOSE_TIME =
+ "Operation close time after fetching the results"
+
+ val THRIFT_SERVER_EXECUTION =
+ "Difference between start time and finish time"
+
+ val THRIFT_SERVER_DURATION =
+ "Difference between start time and close time"
+}