org.apache.spark.ui.PagedTable.scala Maven / Gradle / Ivy
/*
* 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.ui
import java.net.{URLDecoder, URLEncoder}
import java.nio.charset.StandardCharsets.UTF_8
import javax.servlet.http.HttpServletRequest
import scala.collection.JavaConverters._
import scala.xml.{Node, Unparsed}
import com.google.common.base.Splitter
import org.apache.spark.util.Utils
/**
* A data source that provides data for a page.
*
* @param pageSize the number of rows in a page
*/
private[spark] abstract class PagedDataSource[T](val pageSize: Int) {
/**
* Return the size of all data.
*/
protected def dataSize: Int
/**
* Slice a range of data.
*/
protected def sliceData(from: Int, to: Int): Seq[T]
/**
* Slice the data for this page
*/
def pageData(page: Int): PageData[T] = {
// Display all the data in one page, if the pageSize is less than or equal to zero.
val pageTableSize = if (pageSize <= 0) {
dataSize
} else {
pageSize
}
val totalPages = (dataSize + pageTableSize - 1) / pageTableSize
val pageToShow = if (page <= 0) {
1
} else if (page > totalPages) {
totalPages
} else {
page
}
val (from, to) = ((pageToShow - 1) * pageSize, dataSize.min(pageToShow * pageTableSize))
PageData(totalPages, sliceData(from, to))
}
}
/**
* The data returned by `PagedDataSource.pageData`, including the page number, the number of total
* pages and the data in this page.
*/
private[ui] case class PageData[T](totalPage: Int, data: Seq[T])
/**
* A paged table that will generate a HTML table for a specified page and also the page navigation.
*/
private[spark] trait PagedTable[T] {
def tableId: String
def tableCssClass: String
def pageSizeFormField: String
def pageNumberFormField: String
def dataSource: PagedDataSource[T]
def headers: Seq[Node]
def row(t: T): Seq[Node]
def table(page: Int): Seq[Node] = {
val _dataSource = dataSource
try {
val PageData(totalPages, data) = _dataSource.pageData(page)
val pageToShow = if (page <= 0) {
1
} else if (page > totalPages) {
totalPages
} else {
page
}
// Display all the data in one page, if the pageSize is less than or equal to zero.
val pageSize = if (_dataSource.pageSize <= 0) {
data.size
} else {
_dataSource.pageSize
}
val pageNaviTop = pageNavigation(pageToShow, pageSize, totalPages, tableId + "-top")
val pageNaviBottom = pageNavigation(pageToShow, pageSize, totalPages, tableId + "-bottom")
{pageNaviTop}
{headers}
{data.map(row)}
{pageNaviBottom}
} catch {
case e: IndexOutOfBoundsException =>
val PageData(totalPages, _) = _dataSource.pageData(1)
{pageNavigation(1, _dataSource.pageSize, totalPages)}
Error while rendering table:
{Utils.exceptionString(e)}
}
}
/**
* Return a page navigation.
*
* It will create a page navigation including a group of page numbers and a form
* to submit the page number.
*
* Here are some examples of the page navigation:
* {{{
* << < 11 12 13* 14 15 16 17 18 19 20 > >>
*
* This is the first group, so "<<" is hidden.
* < 1 2* 3 4 5 6 7 8 9 10 > >>
*
* This is the first group and the first page, so "<<" and "<" are hidden.
* 1* 2 3 4 5 6 7 8 9 10 > >>
*
* Assume totalPages is 19. This is the last group, so ">>" is hidden.
* << < 11 12 13* 14 15 16 17 18 19 >
*
* Assume totalPages is 19. This is the last group and the last page, so ">>" and ">" are hidden.
* << < 11 12 13 14 15 16 17 18 19*
*
* * means the current page number
* << means jumping to the first page of the previous group.
* < means jumping to the previous page.
* >> means jumping to the first page of the next group.
* > means jumping to the next page.
* }}}
*/
private[ui] def pageNavigation(
page: Int,
pageSize: Int,
totalPages: Int,
navigationId: String = tableId): Seq[Node] = {
// A group includes all page numbers will be shown in the page navigation.
// The size of group is 10 means there are 10 page numbers will be shown.
// The first group is 1 to 10, the second is 2 to 20, and so on
val groupSize = 10
val firstGroup = 0
val lastGroup = (totalPages - 1) / groupSize
val currentGroup = (page - 1) / groupSize
val startPage = currentGroup * groupSize + 1
val endPage = totalPages.min(startPage + groupSize - 1)
val pageTags = (startPage to endPage).map { p =>
if (p == page) {
// The current page should be disabled so that it cannot be clicked.
{p}
} else {
{p}
}
}
val hiddenFormFields = {
if (goButtonFormPath.contains('?')) {
val queryString = goButtonFormPath.split("\\?", 2)(1)
val search = queryString.split("#")(0)
Splitter
.on('&')
.trimResults()
.omitEmptyStrings()
.withKeyValueSeparator("=")
.split(search)
.asScala
.filterKeys(_ != pageSizeFormField)
.filterKeys(_ != pageNumberFormField)
.mapValues(URLDecoder.decode(_, UTF_8.name()))
.map { case (k, v) =>
}
} else {
Seq.empty
}
}
}
/**
* Return a link to jump to a page.
*/
def pageLink(page: Int): String
/**
* Returns the submission path for the "go to page #" form.
*/
def goButtonFormPath: String
/**
* Returns parameters of other tables in the page.
*/
def getParameterOtherTable(request: HttpServletRequest, tableTag: String): String = {
request.getParameterMap.asScala
.filterNot(_._1.startsWith(tableTag))
.map(parameter => parameter._1 + "=" + parameter._2(0))
.mkString("&")
}
/**
* Returns parameter of this table.
*/
def getTableParameters(
request: HttpServletRequest,
tableTag: String,
defaultSortColumn: String): (String, Boolean, Int) = {
val parameterSortColumn = request.getParameter(s"$tableTag.sort")
val parameterSortDesc = request.getParameter(s"$tableTag.desc")
val parameterPageSize = request.getParameter(s"$tableTag.pageSize")
val sortColumn = Option(parameterSortColumn).map { sortColumn =>
UIUtils.decodeURLParameter(sortColumn)
}.getOrElse(defaultSortColumn)
val desc = Option(parameterSortDesc).map(_.toBoolean).getOrElse(
sortColumn == defaultSortColumn
)
val pageSize = Option(parameterPageSize).map(_.toInt).getOrElse(100)
(sortColumn, desc, pageSize)
}
/**
* Check if given sort column is valid or not. If invalid then an exception is thrown.
*/
def isSortColumnValid(
headerInfo: Seq[(String, Boolean, Option[String])],
sortColumn: String): Unit = {
if (!headerInfo.filter(_._2).map(_._1).contains(sortColumn)) {
throw new IllegalArgumentException(s"Unknown column: $sortColumn")
}
}
def headerRow(
headerInfo: Seq[(String, Boolean, Option[String])],
desc: Boolean,
pageSize: Int,
sortColumn: String,
parameterPath: String,
tableTag: String,
headerId: String): Seq[Node] = {
val row: Seq[Node] = {
headerInfo.map { case (header, sortable, tooltip) =>
if (header == sortColumn) {
val headerLink = Unparsed(
parameterPath +
s"&$tableTag.sort=${URLEncoder.encode(header, UTF_8.name())}" +
s"&$tableTag.desc=${!desc}" +
s"&$tableTag.pageSize=$pageSize" +
s"#$headerId")
val arrow = if (desc) "▾" else "▴" // UP or DOWN
{header} {Unparsed(arrow)}
} else {
if (sortable) {
val headerLink = Unparsed(
parameterPath +
s"&$tableTag.sort=${URLEncoder.encode(header, UTF_8.name())}" +
s"&$tableTag.pageSize=$pageSize" +
s"#$headerId")
{header}
} else {
{header}
}
}
}
}
{row}
}
}