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 = +
+ + + + + + {data.map(generateDataRowValue)} + +
{header(0)} + + {header(1)} + +
+ + 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 = + + + + + + + {data.map(generateDataRowValue)} + +
{header(0)}{header(1)}
+ + 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" +}