[SPARK-31161][WEBUI] Refactor the on-click timeline action in streagming-page.js

### What changes were proposed in this pull request?

Refactor `streaming-page.js` by making on-click timeline action customizable.

### Why are the changes needed?

In the current implementation, `streaming-page.js` is used from Streaming page and Structured Streaming page but the implementation of the on-click timeline action is strongly dependent on Streamng page.
Structured Streaming page doesn't define the on-click action for now but it's better to remove the dependncy for the future.

Originally, I make this change to fix `SPARK-31128` but #27883 resolved it.
So, now this is just for refactoring.

### Does this PR introduce any user-facing change?

No.

### How was this patch tested?

Manual tests with following code and confirmed there are no regression and no error in the debug console in Firefox.

For Structured Streaming:
```
spark.readStream.format("socket").options(Map("host"->"localhost", "port"->"8765")).load.writeStream.format("console").start
```
And then, visited Structured Streaming page and there were no error in the debug console when I clicked a point in the timeline.

For Spark Streaming:
```
import org.apache.spark.streaming._
val ssc = new StreamingContext(sc, Seconds(1))
ssc.socketTextStream("localhost", 8765)
dstream.foreachRDD(rdd => rdd.foreach(println))
ssc.start
```
And then, visited Streaming page and confirmed scrolling down and hilighting work well and there were no error in the debug console when I clicked a point in the timeline.

Closes #27921 from sarutak/followup-SPARK-29543-fix-oncick.

Authored-by: Kousuke Saruta <sarutak@oss.nttdata.com>
Signed-off-by: Sean Owen <srowen@gmail.com>
This commit is contained in:
Kousuke Saruta 2020-03-24 13:00:46 -05:00 committed by Sean Owen
parent f6ff7d0cf8
commit 88864c0615
2 changed files with 50 additions and 29 deletions

View file

@ -33,6 +33,8 @@ var yValueFormat = d3.format(",.2f");
var unitLabelYOffset = -10; var unitLabelYOffset = -10;
var onClickTimeline = function() {};
// Show a tooltip "text" for "node" // Show a tooltip "text" for "node"
function showBootstrapTooltip(node, text) { function showBootstrapTooltip(node, text) {
$(node).tooltip({title: text, trigger: "manual", container: "body"}); $(node).tooltip({title: text, trigger: "manual", container: "body"});
@ -44,6 +46,45 @@ function hideBootstrapTooltip(node) {
$(node).tooltip("dispose"); $(node).tooltip("dispose");
} }
// Return the function to scroll to the corresponding
// row on clicking a point of batch in the timeline.
function getOnClickTimelineFunction() {
// If the user click one point in the graphs, jump to the batch row and highlight it. And
// recovery the batch row after 3 seconds if necessary.
// We need to remember the last clicked batch so that we can recovery it.
var lastClickedBatch = null;
var lastTimeout = null;
return function(d) {
var batchSelector = $("#batch-" + d.x);
// If there is a corresponding batch row, scroll down to it and highlight it.
if (batchSelector.length > 0) {
if (lastTimeout != null) {
window.clearTimeout(lastTimeout);
}
if (lastClickedBatch != null) {
clearBatchRow(lastClickedBatch);
lastClickedBatch = null;
}
lastClickedBatch = d.x;
highlightBatchRow(lastClickedBatch);
lastTimeout = window.setTimeout(function () {
lastTimeout = null;
if (lastClickedBatch != null) {
clearBatchRow(lastClickedBatch);
lastClickedBatch = null;
}
}, 3000); // Clean up after 3 seconds
var topOffset = batchSelector.offset().top - 15;
if (topOffset < 0) {
topOffset = 0;
}
$('html,body').animate({scrollTop: topOffset}, 200);
}
}
}
// Register a timeline graph. All timeline graphs should be register before calling any // Register a timeline graph. All timeline graphs should be register before calling any
// "drawTimeline" so that we can determine the max margin left for all timeline graphs. // "drawTimeline" so that we can determine the max margin left for all timeline graphs.
function registerTimeline(minY, maxY) { function registerTimeline(minY, maxY) {
@ -189,34 +230,7 @@ function drawTimeline(id, data, minX, maxX, minY, maxY, unitY, batchInterval) {
.attr("opacity", function(d) { return isFailedBatch(d.x) ? "1" : "0";}) .attr("opacity", function(d) { return isFailedBatch(d.x) ? "1" : "0";})
.attr("r", function(d) { return isFailedBatch(d.x) ? "2" : "3";}); .attr("r", function(d) { return isFailedBatch(d.x) ? "2" : "3";});
}) })
.on("click", function(d) { .on("click", onClickTimeline);
var batchSelector = $("#batch-" + d.x);
// If there is a corresponding batch row, scroll down to it and highlight it.
if (batchSelector.length > 0) {
if (lastTimeout != null) {
window.clearTimeout(lastTimeout);
}
if (lastClickedBatch != null) {
clearBatchRow(lastClickedBatch);
lastClickedBatch = null;
}
lastClickedBatch = d.x;
highlightBatchRow(lastClickedBatch);
lastTimeout = window.setTimeout(function () {
lastTimeout = null;
if (lastClickedBatch != null) {
clearBatchRow(lastClickedBatch);
lastClickedBatch = null;
}
}, 3000); // Clean up after 3 seconds
var topOffset = batchSelector.offset().top - 15;
if (topOffset < 0) {
topOffset = 0;
}
$('html,body').animate({scrollTop: topOffset}, 200);
}
});
} }
/** /**

View file

@ -80,9 +80,10 @@ private[ui] class StreamingPage(parent: StreamingTab)
/** Render the page */ /** Render the page */
def render(request: HttpServletRequest): Seq[Node] = { def render(request: HttpServletRequest): Seq[Node] = {
val resources = generateLoadResources(request) val resources = generateLoadResources(request)
val onClickTimelineFunc = generateOnClickTimelineFunction()
val basicInfo = generateBasicInfo() val basicInfo = generateBasicInfo()
val content = resources ++ val content = resources ++
basicInfo ++ onClickTimelineFunc ++ basicInfo ++
listener.synchronized { listener.synchronized {
generateStatTable() ++ generateStatTable() ++
generateBatchListTables() generateBatchListTables()
@ -101,6 +102,12 @@ private[ui] class StreamingPage(parent: StreamingTab)
// scalastyle:on // scalastyle:on
} }
/** Generate html that will set onClickTimeline declared in streaming-page.js */
private def generateOnClickTimelineFunction(): Seq[Node] = {
val js = "onClickTimeline = getOnClickTimelineFunction();"
<script>{Unparsed(js)}</script>
}
/** Generate basic information of the streaming program */ /** Generate basic information of the streaming program */
private def generateBasicInfo(): Seq[Node] = { private def generateBasicInfo(): Seq[Node] = {
val timeSinceStart = System.currentTimeMillis() - startTime val timeSinceStart = System.currentTimeMillis() - startTime