From a1ec08f7edc8d956afcfbb92d10b26b7619486e8 Mon Sep 17 00:00:00 2001
From: Matei Zaharia <matei@databricks.com>
Date: Fri, 8 May 2015 14:41:42 -0400
Subject: [PATCH] [SPARK-7298] Harmonize style of new visualizations

- Colors on the timeline now match the rest of the UI
- The expandable buttons to show timeline view, DAG, etc are now more visible
- Timeline text is smaller
- DAG visualization text and colors are more consistent throughout
- Fix some JavaScript style issues
- Various small fixes throughout (e.g. inconsistent capitalization, some confusing names, HTML escaping, etc)

Author: Matei Zaharia <matei@databricks.com>

Closes #5942 from mateiz/ui and squashes the following commits:

def38d0 [Matei Zaharia] Add some tooltips
4c5a364 [Matei Zaharia] Reduce stage and rank separation slightly
43dcbe3 [Matei Zaharia] Some updates to DAG
fac734a [Matei Zaharia] tweaks
6a6705d [Matei Zaharia] More fixes
67629f5 [Matei Zaharia] Various small tweaks
---
 .../apache/spark/ui/static/spark-dag-viz.css  |  60 +++++---
 .../apache/spark/ui/static/spark-dag-viz.js   |  57 ++++----
 .../apache/spark/ui/static/timeline-view.css  | 128 +++++++++++-------
 .../apache/spark/ui/static/timeline-view.js   |   4 +-
 .../org/apache/spark/ui/static/webui.css      |  36 +++--
 .../scala/org/apache/spark/ui/ToolTips.scala  |  19 +++
 .../scala/org/apache/spark/ui/UIUtils.scala   |  15 +-
 .../apache/spark/ui/jobs/AllJobsPage.scala    |  31 +++--
 .../apache/spark/ui/jobs/AllStagesPage.scala  |  15 +-
 .../org/apache/spark/ui/jobs/JobPage.scala    |  23 ++--
 .../org/apache/spark/ui/jobs/StagePage.scala  |  16 +--
 .../spark/ui/scope/RDDOperationGraph.scala    |   2 +-
 12 files changed, 255 insertions(+), 151 deletions(-)

diff --git a/core/src/main/resources/org/apache/spark/ui/static/spark-dag-viz.css b/core/src/main/resources/org/apache/spark/ui/static/spark-dag-viz.css
index 8481710828..18c72694f3 100644
--- a/core/src/main/resources/org/apache/spark/ui/static/spark-dag-viz.css
+++ b/core/src/main/resources/org/apache/spark/ui/static/spark-dag-viz.css
@@ -16,40 +16,51 @@
  */
 
 #dag-viz-graph svg path {
-  stroke: #444444;
+  stroke: #444;
   stroke-width: 1.5px;
 }
 
 #dag-viz-graph svg g.cluster rect {
-  stroke-width: 4px;
-  stroke-opacity: 0.5;
+  stroke-width: 1px;
+}
+
+#dag-viz-graph svg g.node circle {
+  fill: #444;
 }
 
-#dag-viz-graph svg g.node circle,
 #dag-viz-graph svg g.node rect {
-  fill: #444444;
+  fill: #C3EBFF;
+  stroke: #3EC0FF;
+  stroke-width: 1px;
+}
+
+#dag-viz-graph svg g.node.cached circle {
+  fill: #444;
 }
 
-#dag-viz-graph svg g.node.cached circle,
 #dag-viz-graph svg g.node.cached rect {
-  fill: #FF0000;
+  fill: #B3F5C5;
+  stroke: #56F578;
+  stroke-width: 1px;
 }
 
 /* Job page specific styles */
 
 #dag-viz-graph svg.job marker#marker-arrow path {
-  fill: #444444;
+  fill: #333;
   stroke-width: 0px;
 }
 
 #dag-viz-graph svg.job g.cluster rect {
-  fill: #FFFFFF;
-  stroke: #AADFFF;
+  fill: #A0DFFF;
+  stroke: #3EC0FF;
+  stroke-width: 1px;
 }
 
 #dag-viz-graph svg.job g.cluster[id*="stage"] rect {
-  stroke: #FFDDEE;
-  stroke-width: 6px;
+  fill: #FFFFFF;
+  stroke: #FF99AC;
+  stroke-width: 1px;
 }
 
 #dag-viz-graph svg.job g#cross-stage-edges path {
@@ -57,27 +68,36 @@
 }
 
 #dag-viz-graph svg.job g.cluster text {
-  fill: #AAAAAA;
+  fill: #333;
 }
 
 /* Stage page specific styles */
 
 #dag-viz-graph svg.stage g.cluster rect {
-  fill: #F0F8FF;
-  stroke: #AADFFF;
+  fill: #A0DFFF;
+  stroke: #3EC0FF;
+  stroke-width: 1px;
 }
 
 #dag-viz-graph svg.stage g.cluster[id*="stage"] rect {
   fill: #FFFFFF;
-  stroke: #FFDDEE;
-  stroke-width: 6px;
+  stroke: #FFA6B6;
+  stroke-width: 1px;
 }
 
 #dag-viz-graph svg.stage g.node g.label text tspan {
-  fill: #FFFFFF;
+  fill: #333;
 }
 
 #dag-viz-graph svg.stage g.cluster text {
-  fill: #444444;
-  font-weight: bold;
+  fill: #333;
+}
+
+#dag-viz-graph a, #dag-viz-graph a:hover {
+  text-decoration: none;
+}
+
+#dag-viz-graph .label {
+  font-weight: normal;
+  text-shadow: none;
 }
diff --git a/core/src/main/resources/org/apache/spark/ui/static/spark-dag-viz.js b/core/src/main/resources/org/apache/spark/ui/static/spark-dag-viz.js
index a0e3e914c2..764dd2cfcd 100644
--- a/core/src/main/resources/org/apache/spark/ui/static/spark-dag-viz.js
+++ b/core/src/main/resources/org/apache/spark/ui/static/spark-dag-viz.js
@@ -52,9 +52,9 @@
  */
 
 var VizConstants = {
-  svgMarginX: 20,
-  svgMarginY: 20,
-  stageSep: 50,
+  svgMarginX: 16,
+  svgMarginY: 16,
+  stageSep: 40,
   graphPrefix: "graph_",
   nodePrefix: "node_",
   stagePrefix: "stage_",
@@ -63,14 +63,16 @@ var VizConstants = {
 };
 
 var JobPageVizConstants = {
-  clusterLabelSize: 11,
-  stageClusterLabelSize: 14
-}
+  clusterLabelSize: 12,
+  stageClusterLabelSize: 14,
+  rankSep: 40
+};
 
 var StagePageVizConstants = {
   clusterLabelSize: 14,
-  stageClusterLabelSize: 18
-}
+  stageClusterLabelSize: 14,
+  rankSep: 40
+};
 
 /*
  * Show or hide the RDD DAG visualization.
@@ -149,11 +151,11 @@ function renderDagVizForStage(svgContainer) {
   var dot = metadata.select(".dot-file").text();
   var containerId = VizConstants.graphPrefix + metadata.attr("stage-id");
   var container = svgContainer.append("g").attr("id", containerId);
-  renderDot(dot, container);
+  renderDot(dot, container, StagePageVizConstants.rankSep);
 
-  // Round corners on RDDs
+  // Round corners on rectangles
   svgContainer
-    .selectAll("g.node rect")
+    .selectAll("rect")
     .attr("rx", "5")
     .attr("ry", "5");
 }
@@ -207,7 +209,13 @@ function renderDagVizForJob(svgContainer) {
     }
 
     // Actually render the stage
-    renderDot(dot, container);
+    renderDot(dot, container, JobPageVizConstants.rankSep);
+
+    // Round corners on rectangles
+    container
+      .selectAll("rect")
+      .attr("rx", "4")
+      .attr("ry", "4");
 
     // If there are any incoming edges into this graph, keep track of them to render
     // them separately later. Note that we cannot draw them now because we need to
@@ -223,12 +231,13 @@ function renderDagVizForJob(svgContainer) {
 }
 
 /* Render the dot file as an SVG in the given container. */
-function renderDot(dot, container) {
+function renderDot(dot, container, rankSep) {
   var escaped_dot = dot
     .replace(/&lt;/g, "<")
     .replace(/&gt;/g, ">")
     .replace(/&quot;/g, "\"");
   var g = graphlibDot.read(escaped_dot);
+  g.graph().rankSep = rankSep;
   var renderer = new dagreD3.render();
   renderer(container, g);
 }
@@ -248,12 +257,13 @@ function metadataContainer() { return d3.select("#dag-viz-metadata"); }
  * In general, the clustering support for dagre-d3 is quite limited at this point.
  */
 function drawClusterLabels(svgContainer, forJob) {
+  var clusterLabelSize, stageClusterLabelSize;
   if (forJob) {
-    var clusterLabelSize = JobPageVizConstants.clusterLabelSize;
-    var stageClusterLabelSize = JobPageVizConstants.stageClusterLabelSize;
+    clusterLabelSize = JobPageVizConstants.clusterLabelSize;
+    stageClusterLabelSize = JobPageVizConstants.stageClusterLabelSize;
   } else {
-    var clusterLabelSize = StagePageVizConstants.clusterLabelSize;
-    var stageClusterLabelSize = StagePageVizConstants.stageClusterLabelSize;
+    clusterLabelSize = StagePageVizConstants.clusterLabelSize;
+    stageClusterLabelSize = StagePageVizConstants.stageClusterLabelSize;
   }
   svgContainer.selectAll("g.cluster").each(function() {
     var cluster = d3.select(this);
@@ -283,7 +293,7 @@ function drawClusterLabel(d3cluster, fontSize) {
     .attr("x", labelX)
     .attr("y", labelY)
     .attr("text-anchor", "end")
-    .style("font-size", fontSize)
+    .style("font-size", fontSize + "px")
     .text(labelText);
 }
 
@@ -303,12 +313,12 @@ function resizeSvg(svg) {
     }));
   var endX = VizConstants.svgMarginX +
     toFloat(d3.max(allClusters, function(e) {
-      var t = d3.select(e)
+      var t = d3.select(e);
       return getAbsolutePosition(t).x + toFloat(t.attr("width"));
     }));
   var endY = VizConstants.svgMarginY +
     toFloat(d3.max(allClusters, function(e) {
-      var t = d3.select(e)
+      var t = d3.select(e);
       return getAbsolutePosition(t).y + toFloat(t.attr("height"));
     }));
   var width = endX - startX;
@@ -338,7 +348,7 @@ function drawCrossStageEdges(edges, svgContainer) {
   if (!dagreD3Marker.empty()) {
     svgContainer
       .append(function() { return dagreD3Marker.node().cloneNode(true); })
-      .attr("id", "marker-arrow")
+      .attr("id", "marker-arrow");
     svgContainer.selectAll("g > path").attr("marker-end", "url(#marker-arrow)");
     svgContainer.selectAll("g.edgePaths def").remove(); // We no longer need these
   }
@@ -394,12 +404,13 @@ function connectRDDs(fromRDDId, toRDDId, edgesContainer, svgContainer) {
     toPos.x += delta;
   }
 
+  var points;
   if (fromPos.y == toPos.y) {
     // If they are on the same rank, curve the middle part of the edge
     // upward a little to avoid interference with things in between
     // e.g.       _______
     //      _____/       \_____
-    var points = [
+    points = [
       [fromPos.x, fromPos.y],
       [fromPos.x + (toPos.x - fromPos.x) * 0.2, fromPos.y],
       [fromPos.x + (toPos.x - fromPos.x) * 0.3, fromPos.y - 20],
@@ -413,7 +424,7 @@ function connectRDDs(fromRDDId, toRDDId, edgesContainer, svgContainer) {
     //           /
     //          |
     //    _____/
-    var points = [
+    points = [
       [fromPos.x, fromPos.y],
       [fromPos.x + (toPos.x - fromPos.x) * 0.4, fromPos.y],
       [fromPos.x + (toPos.x - fromPos.x) * 0.6, toPos.y],
diff --git a/core/src/main/resources/org/apache/spark/ui/static/timeline-view.css b/core/src/main/resources/org/apache/spark/ui/static/timeline-view.css
index 35ef14e5aa..d40de70422 100644
--- a/core/src/main/resources/org/apache/spark/ui/static/timeline-view.css
+++ b/core/src/main/resources/org/apache/spark/ui/static/timeline-view.css
@@ -23,6 +23,10 @@ div#application-timeline, div#job-timeline {
   margin-top: 5px;
 }
 
+.vis.timeline {
+  line-height: 14px;
+}
+
 .vis.timeline div.content {
   width: 100%;
 }
@@ -32,48 +36,55 @@ div#application-timeline, div#job-timeline {
 }
 
 .vis.timeline .item.stage.succeeded {
-  background-color: #D5DDF6;
+  background-color: #A0DFFF;
+  border-color: #3EC0FF;
 }
 
 .vis.timeline .item.stage.succeeded.selected {
-   background-color: #D5DDF6;
-   border-color: #97B0F8;
-   z-index: auto;
+  background-color: #A0DFFF;
+  border-color: #3EC0FF;
+  z-index: auto;
 }
 
 .legend-area rect.completed-stage-legend {
-  fill: #D5DDF6;
-  stroke: #97B0F8;
+  fill: #A0DFFF;
+  stroke: #3EC0FF;
 }
 
 .vis.timeline .item.stage.failed {
-   background-color: #FF5475;
+  background-color: #FFA1B0;
+  border-color: #FF4D6D;
 }
 
 .vis.timeline .item.stage.failed.selected {
-   background-color: #FF5475;
-   border-color: #97B0F8;
-   z-index: auto;
+  background-color: #FFA1B0;
+  border-color: #FF4D6D;
+  z-index: auto;
 }
 
 .legend-area rect.failed-stage-legend {
-  fill: #FF5475;
-  stroke: #97B0F8;
+  fill: #FFA1B0;
+  stroke: #FF4D6D;
 }
 
 .vis.timeline .item.stage.running {
-   background-color: #FDFFCA;
+  background-color: #A2FCC0;
+  border-color: #36F572;
 }
 
 .vis.timeline .item.stage.running.selected {
-   background-color: #FDFFCA;
-   border-color: #97B0F8;
-   z-index: auto;
+  background-color: #A2FCC0;
+  border-color: #36F572;
+  z-index: auto;
 }
 
 .legend-area rect.active-stage-legend {
-  fill: #FDFFCA;
-  stroke: #97B0F8;
+  fill: #A2FCC0;
+  stroke: #36F572;
+}
+
+.vis.timeline .foreground {
+  cursor: move;
 }
 
 .vis.timeline .item.job {
@@ -81,76 +92,81 @@ div#application-timeline, div#job-timeline {
 }
 
 .vis.timeline .item.job.succeeded {
-  background-color: #D5DDF6;
+  background-color: #A0DFFF;
+  border-color: #3EC0FF;
 }
 
 .vis.timeline .item.job.succeeded.selected {
-   background-color: #D5DDF6;
-   border-color: #97B0F8;
-   z-index: auto;
+  background-color: #A0DFFF;
+  border-color: #3EC0FF;
+  z-index: auto;
 }
 
 .legend-area rect.succeeded-job-legend {
-  fill: #D5DDF6;
-  stroke: #97B0F8;
+  fill: #A0DFFF;
+  stroke: #3EC0FF;
 }
 
 .vis.timeline .item.job.failed {
-   background-color: #FF5475;
+  background-color: #FFA1B0;
+  border-color: #FF4D6D;
 }
 
 .vis.timeline .item.job.failed.selected {
-   background-color: #FF5475;
-   border-color: #97B0F8;
-   z-index: auto;
+  background-color: #FFA1B0;
+  border-color: #FF4D6D;
+  z-index: auto;
 }
 
 .legend-area rect.failed-job-legend {
-  fill: #FF5475;
-  stroke: #97B0F8;
+  fill: #FFA1B0;
+  stroke: #FF4D6D;
 }
 
 .vis.timeline .item.job.running {
-   background-color: #FDFFCA;
+  background-color: #A2FCC0;
+  border-color: #36F572;
 }
 
 .vis.timeline .item.job.running.selected {
-   background-color: #FDFFCA;
-   border-color: #97B0F8;
-   z-index: auto;
+  background-color: #A2FCC0;
+  border-color: #36F572;
+  z-index: auto;
 }
 
 .legend-area rect.running-job-legend {
-  fill: #FDFFCA;
-  stroke: #97B0F8;
+  fill: #A2FCC0;
+  stroke: #36F572;
 }
 
 .vis.timeline .item.executor.added {
-  background-color: #D5DDF6;
+  background-color: #A0DFFF;
+  border-color: #3EC0FF;
 }
 
 .legend-area rect.executor-added-legend {
-  fill: #D5DDF6;
-  stroke: #97B0F8;
+  fill: #A0DFFF;
+  stroke: #3EC0FF;
 }
 
 .vis.timeline .item.executor.removed {
-  background-color: #EBCA59;
+  background-color: #FFA1B0;
+  border-color: #FF4D6D;
 }
 
 .legend-area rect.executor-removed-legend {
-  fill: #EBCA59;
-  stroke: #97B0F8;
+  fill: #FFA1B0;
+  stroke: #FF4D6D;
 }
 
 .vis.timeline .item.executor.selected {
-  border-color: #FFC200;
-  background-color: #FFF785;
+  background-color: #A2FCC0;
+  border-color: #36F572;
   z-index: 2;
 }
 
-tr.corresponding-item-hover>td, tr.corresponding-item-hover>th {
-  background-color: #FFE1FA !important;
+tr.corresponding-item-hover > td, tr.corresponding-item-hover > th {
+  background-color: #D6FFE4 !important;
 }
 
 #application-timeline.collapsed {
@@ -165,11 +181,15 @@ tr.corresponding-item-hover>td, tr.corresponding-item-hover>th {
   margin-bottom: 5px;
 }
 
+.control-panel input[type="checkbox"] {
+  margin: 0;
+}
+
 span.expand-application-timeline, span.expand-job-timeline {
   cursor: pointer;
 }
 
-.control-panel input+span {
+.control-panel input + span {
   cursor: pointer;
 }
 
@@ -180,3 +200,17 @@ span.expand-application-timeline, span.expand-job-timeline {
 .vis.timeline .item .tooltip-inner {
   max-width: unset !important;
 }
+
+.vispanel.center {
+  font-size: 12px;
+  line-height: 12px;
+}
+
+.legend-area text {
+  fill: #4D4D4D;
+}
+
+.additional-metrics ul {
+  list-style: none;
+  margin-left: 15px;
+}
diff --git a/core/src/main/resources/org/apache/spark/ui/static/timeline-view.js b/core/src/main/resources/org/apache/spark/ui/static/timeline-view.js
index e4a891d47f..48fbb33b11 100644
--- a/core/src/main/resources/org/apache/spark/ui/static/timeline-view.js
+++ b/core/src/main/resources/org/apache/spark/ui/static/timeline-view.js
@@ -156,9 +156,9 @@ function setupExecutorEventAction() {
 function setupZoomable(id, timeline) {
   $(id + '>input[type="checkbox"]').click(function() {
     if (this.checked) {
-      timeline.setOptions({zoomable: false});
-    } else {
       timeline.setOptions({zoomable: true});
+    } else {
+      timeline.setOptions({zoomable: false});
     }
   });
 
diff --git a/core/src/main/resources/org/apache/spark/ui/static/webui.css b/core/src/main/resources/org/apache/spark/ui/static/webui.css
index 669ad48937..e7c1d475d4 100644
--- a/core/src/main/resources/org/apache/spark/ui/static/webui.css
+++ b/core/src/main/resources/org/apache/spark/ui/static/webui.css
@@ -106,14 +106,18 @@ span.rest-uri {
 }
 
 pre {
-  font-size: 0.8em;
+  font-size: 12px;
+  line-height: 18px;
+  padding: 6px;
+  margin: 0;
+  border-radius: 3px;
 }
 
 .stage-details {
   max-height: 100px;
   overflow-y: auto;
   margin: 0;
-  transition: max-height 0.5s ease-out, padding 0.5s ease-out;
+  transition: max-height 0.25s ease-out, padding 0.25s ease-out;
 }
 
 .stage-details.collapsed {
@@ -135,7 +139,7 @@ pre {
   max-height: 300px;
   overflow-y: auto;
   margin: 0;
-  transition: max-height 0.5s ease-out, padding 0.5s ease-out;
+  transition: max-height 0.25s ease-out, padding 0.25s ease-out;
 }
 
 .stacktrace-details.collapsed {
@@ -158,7 +162,7 @@ span.additional-metric-title {
 }
 
 .tooltip {
- font-weight: normal;
+  font-weight: normal;
 }
 
 .arrow-open {
@@ -166,9 +170,9 @@ span.additional-metric-title {
   height: 0;
   border-left: 5px solid transparent;
   border-right: 5px solid transparent;
-  border-top: 5px solid black;
-  float: left;
-  margin-top: 6px;
+  border-top: 5px solid #08c;
+  display: inline-block;
+  margin-bottom: 2px;
 }
 
 .arrow-closed {
@@ -176,8 +180,10 @@ span.additional-metric-title {
   height: 0;
   border-top: 5px solid transparent;
   border-bottom: 5px solid transparent;
-  border-left: 5px solid black;
+  border-left: 5px solid #08c;
   display: inline-block;
+  margin-left: 2px;
+  margin-right: 3px;
 }
 
 .version {
@@ -196,3 +202,17 @@ span.additional-metric-title {
 .serialization_time, .getting_result_time {
   display: none;
 }
+
+.accordion-inner {
+  background: #f5f5f5;
+}
+
+.accordion-inner pre {
+  border: 0;
+  padding: 0;
+  background: none;
+}
+
+a.expandbutton {
+  cursor: pointer;
+}
diff --git a/core/src/main/scala/org/apache/spark/ui/ToolTips.scala b/core/src/main/scala/org/apache/spark/ui/ToolTips.scala
index 24f3236456..063e2a1f8b 100644
--- a/core/src/main/scala/org/apache/spark/ui/ToolTips.scala
+++ b/core/src/main/scala/org/apache/spark/ui/ToolTips.scala
@@ -57,4 +57,23 @@ private[spark] object ToolTips {
   val GC_TIME =
     """Time that the executor spent paused for Java garbage collection while the task was
        running."""
+
+  val JOB_TIMELINE =
+    """Shows when jobs started and ended and when executors joined or left. Drag to scroll.
+       Click Enable Zooming and use mouse wheel to zoom in/out."""
+
+  val STAGE_TIMELINE =
+    """Shows when stages started and ended and when executors joined or left. Drag to scroll.
+       Click Enable Zooming and use mouse wheel to zoom in/out."""
+
+  val JOB_DAG =
+    """Shows a graph of stages executed for this job, each of which can contain
+       multiple RDD operations (e.g. map() and filter()), and of RDDs inside each operation
+       (shown as dots)."""
+
+  val STAGE_DAG =
+    """Shows a graph of RDD operations in this stage, and RDDs inside each one. A stage can run
+       multiple operations (e.g. two map() functions) if they can be pipelined. Some operations
+       also create multiple RDDs internally. Cached RDDs are shown in green.
+    """
 }
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 97eed13c2d..6a0f5c5d16 100644
--- a/core/src/main/scala/org/apache/spark/ui/UIUtils.scala
+++ b/core/src/main/scala/org/apache/spark/ui/UIUtils.scala
@@ -156,10 +156,10 @@ private[spark] object UIUtils extends Logging {
 
   def commonHeaderNodes: Seq[Node] = {
     <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
-    <link rel="stylesheet" href={prependBaseUri("/static/bootstrap.min.css")} type="text/css" />
-    <link rel="stylesheet" href={prependBaseUri("/static/webui.css")} type="text/css" />
-    <link rel="stylesheet" href={prependBaseUri("/static/vis.min.css")} type="text/css" />
-    <link rel="stylesheet" href={prependBaseUri("/static/timeline-view.css")} type="text/css" />
+    <link rel="stylesheet" href={prependBaseUri("/static/bootstrap.min.css")} type="text/css"/>
+    <link rel="stylesheet" href={prependBaseUri("/static/vis.min.css")} type="text/css"/>
+    <link rel="stylesheet" href={prependBaseUri("/static/webui.css")} type="text/css"/>
+    <link rel="stylesheet" href={prependBaseUri("/static/timeline-view.css")} type="text/css"/>
     <script src={prependBaseUri("/static/sorttable.js")} ></script>
     <script src={prependBaseUri("/static/jquery-1.11.1.min.js")}></script>
     <script src={prependBaseUri("/static/vis.min.js")}></script>
@@ -250,7 +250,7 @@ private[spark] object UIUtils extends Logging {
               <h3 style="vertical-align: middle; display: inline-block;">
                 <a style="text-decoration: none" href={prependBaseUri("/")}>
                   <img src={prependBaseUri("/static/spark-logo-77x50px-hd.png")} />
-                  <span class="version" 
+                  <span class="version"
                         style="margin-right: 15px;">{org.apache.spark.SPARK_VERSION}</span>
                 </a>
                 {title}
@@ -350,7 +350,10 @@ private[spark] object UIUtils extends Logging {
     <div>
       <span class="expand-dag-viz" onclick={s"toggleDagViz($forJob);"}>
         <span class="expand-dag-viz-arrow arrow-closed"></span>
-        <strong>DAG visualization</strong>
+        <a data-toggle="tooltip" title={if (forJob) ToolTips.JOB_DAG else ToolTips.STAGE_DAG}
+           data-placement="right">
+          DAG Visualization
+        </a>
       </span>
       <div id="dag-viz-graph"></div>
       <div id="dag-viz-metadata">
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 09323d1d80..e010ebef3b 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
@@ -18,12 +18,12 @@
 package org.apache.spark.ui.jobs
 
 import scala.collection.mutable.{HashMap, ListBuffer}
-import scala.xml.{Node, NodeSeq, Unparsed}
+import scala.xml.{Node, NodeSeq, Unparsed, Utility}
 
 import java.util.Date
 import javax.servlet.http.HttpServletRequest
 
-import org.apache.spark.ui.{UIUtils, WebUIPage}
+import org.apache.spark.ui.{ToolTips, UIUtils, WebUIPage}
 import org.apache.spark.ui.jobs.UIData.{ExecutorUIData, JobUIData}
 import org.apache.spark.JobExecutionStatus
 
@@ -81,6 +81,9 @@ private[ui] class AllJobsPage(parent: JobsTab) extends WebUIPage("") {
         case JobExecutionStatus.RUNNING => "running"
       }
 
+      // The timeline library treats contents as HTML, so we have to escape them; for the
+      // data-title attribute string we have to escape them twice since that's in a string.
+      val escapedDesc = Utility.escape(displayJobDescription)
       val jobEventJsonAsStr =
         s"""
            |{
@@ -90,16 +93,17 @@ private[ui] class AllJobsPage(parent: JobsTab) extends WebUIPage("") {
            |  'end': new Date(${completionTime}),
            |  'content': '<div class="application-timeline-content"' +
            |     'data-html="true" data-placement="top" data-toggle="tooltip"' +
-           |     'data-title="${displayJobDescription} (Job ${jobId})<br>Status: ${status}<br>' +
-           |     'Submission Time: ${UIUtils.formatDate(new Date(submissionTime))}' +
+           |     'data-title="${Utility.escape(escapedDesc)} (Job ${jobId})<br>' +
+           |     'Status: ${status}<br>' +
+           |     'Submitted: ${UIUtils.formatDate(new Date(submissionTime))}' +
            |     '${
                      if (status != JobExecutionStatus.RUNNING) {
-                       s"""<br>Completion Time: ${UIUtils.formatDate(new Date(completionTime))}"""
+                       s"""<br>Completed: ${UIUtils.formatDate(new Date(completionTime))}"""
                      } else {
                        ""
                      }
                   }">' +
-           |    '${displayJobDescription} (Job ${jobId})</div>'
+           |    '${escapedDesc} (Job ${jobId})</div>'
            |}
          """.stripMargin
       jobEventJsonAsStr
@@ -179,13 +183,15 @@ private[ui] class AllJobsPage(parent: JobsTab) extends WebUIPage("") {
 
     <span class="expand-application-timeline">
       <span class="expand-application-timeline-arrow arrow-closed"></span>
-      <strong>Event timeline</strong>
+      <a data-toggle="tooltip" title={ToolTips.JOB_TIMELINE} data-placement="right">
+        Event Timeline
+      </a>
     </span> ++
     <div id="application-timeline" class="collapsed">
       <div class="control-panel">
         <div id="application-timeline-zoom-lock">
-          <input type="checkbox" checked="checked"></input>
-          <span>Zoom Lock</span>
+          <input type="checkbox"></input>
+          <span>Enable zooming</span>
         </div>
       </div>
     </div> ++
@@ -283,7 +289,7 @@ private[ui] class AllJobsPage(parent: JobsTab) extends WebUIPage("") {
             {if (parent.sc.isDefined) {
               // Total duration is not meaningful unless the UI is live
               <li>
-                <strong>Total Duration: </strong>
+                <strong>Total Uptime: </strong>
                 {UIUtils.formatDuration(System.currentTimeMillis() - startTime)}
               </li>
             }}
@@ -336,9 +342,8 @@ private[ui] class AllJobsPage(parent: JobsTab) extends WebUIPage("") {
           failedJobsTable
       }
 
-      val helpText = """A job is triggered by an action, like "count()" or "saveAsTextFile()".""" +
-        " Click on a job's title to see information about the stages of tasks associated with" +
-        " the job."
+      val helpText = """A job is triggered by an action, like count() or saveAsTextFile().""" +
+        " Click on a job to see information about the stages of tasks inside it."
 
       UIUtils.headerSparkPage("Spark Jobs", content, parent, helpText = Some(helpText))
     }
diff --git a/core/src/main/scala/org/apache/spark/ui/jobs/AllStagesPage.scala b/core/src/main/scala/org/apache/spark/ui/jobs/AllStagesPage.scala
index a37f739ab9..5e52942b64 100644
--- a/core/src/main/scala/org/apache/spark/ui/jobs/AllStagesPage.scala
+++ b/core/src/main/scala/org/apache/spark/ui/jobs/AllStagesPage.scala
@@ -73,19 +73,6 @@ private[ui] class AllStagesPage(parent: StagesTab) extends WebUIPage("") {
       val summary: NodeSeq =
         <div>
           <ul class="unstyled">
-            {
-              if (sc.isDefined) {
-                // Total duration is not meaningful unless the UI is live
-                <li>
-                  <strong>Total Duration: </strong>
-                  {UIUtils.formatDuration(now - sc.get.startTime)}
-                </li>
-              }
-            }
-            <li>
-              <strong>Scheduling Mode: </strong>
-              {listener.schedulingMode.map(_.toString).getOrElse("Unknown")}
-            </li>
             {
               if (shouldShowActiveStages) {
                 <li>
@@ -145,7 +132,7 @@ private[ui] class AllStagesPage(parent: StagesTab) extends WebUIPage("") {
         content ++= <h4 id ="failed">Failed Stages ({numFailedStages})</h4> ++
         failedStagesTable.toNodeSeq
       }
-      UIUtils.headerSparkPage("Spark Stages (for all jobs)", content, parent)
+      UIUtils.headerSparkPage("Stages for All Jobs", content, parent)
     }
   }
 }
diff --git a/core/src/main/scala/org/apache/spark/ui/jobs/JobPage.scala b/core/src/main/scala/org/apache/spark/ui/jobs/JobPage.scala
index 7163217e1f..2cad0a7969 100644
--- a/core/src/main/scala/org/apache/spark/ui/jobs/JobPage.scala
+++ b/core/src/main/scala/org/apache/spark/ui/jobs/JobPage.scala
@@ -20,13 +20,13 @@ package org.apache.spark.ui.jobs
 import java.util.Date
 
 import scala.collection.mutable.{Buffer, HashMap, ListBuffer}
-import scala.xml.{NodeSeq, Node, Unparsed}
+import scala.xml.{NodeSeq, Node, Unparsed, Utility}
 
 import javax.servlet.http.HttpServletRequest
 
 import org.apache.spark.JobExecutionStatus
 import org.apache.spark.scheduler.StageInfo
-import org.apache.spark.ui.{UIUtils, WebUIPage}
+import org.apache.spark.ui.{ToolTips, UIUtils, WebUIPage}
 import org.apache.spark.ui.jobs.UIData.ExecutorUIData
 
 /** Page showing statistics and stage list for a given job */
@@ -64,6 +64,9 @@ private[ui] class JobPage(parent: JobsTab) extends WebUIPage("job") {
       val submissionTime = stage.submissionTime.get
       val completionTime = stage.completionTime.getOrElse(System.currentTimeMillis())
 
+      // The timeline library treats contents as HTML, so we have to escape them; for the
+      // data-title attribute string we have to escape them twice since that's in a string.
+      val escapedName = Utility.escape(name)
       s"""
          |{
          |  'className': 'stage job-timeline-object ${status}',
@@ -72,17 +75,17 @@ private[ui] class JobPage(parent: JobsTab) extends WebUIPage("job") {
          |  'end': new Date(${completionTime}),
          |  'content': '<div class="job-timeline-content" data-toggle="tooltip"' +
          |   'data-placement="top" data-html="true"' +
-         |   'data-title="${name} (Stage ${stageId}.${attemptId})<br>' +
+         |   'data-title="${Utility.escape(escapedName)} (Stage ${stageId}.${attemptId})<br>' +
          |   'Status: ${status.toUpperCase}<br>' +
-         |   'Submission Time: ${UIUtils.formatDate(new Date(submissionTime))}' +
+         |   'Submitted: ${UIUtils.formatDate(new Date(submissionTime))}' +
          |   '${
                  if (status != "running") {
-                   s"""<br>Completion Time: ${UIUtils.formatDate(new Date(completionTime))}"""
+                   s"""<br>Completed: ${UIUtils.formatDate(new Date(completionTime))}"""
                  } else {
                    ""
                  }
               }">' +
-         |    '${name} (Stage ${stageId}.${attemptId})</div>',
+         |    '${escapedName} (Stage ${stageId}.${attemptId})</div>',
          |}
        """.stripMargin
     }
@@ -161,13 +164,15 @@ private[ui] class JobPage(parent: JobsTab) extends WebUIPage("job") {
 
     <span class="expand-job-timeline">
       <span class="expand-job-timeline-arrow arrow-closed"></span>
-      <strong>Event timeline</strong>
+      <a data-toggle="tooltip" title={ToolTips.STAGE_TIMELINE} data-placement="right">
+        Event Timeline
+      </a>
     </span> ++
     <div id="job-timeline" class="collapsed">
       <div class="control-panel">
         <div id="job-timeline-zoom-lock">
-          <input type="checkbox" checked="checked"></input>
-          <span>Zoom Lock</span>
+          <input type="checkbox"></input>
+          <span>Enable zooming</span>
         </div>
       </div>
     </div> ++
diff --git a/core/src/main/scala/org/apache/spark/ui/jobs/StagePage.scala b/core/src/main/scala/org/apache/spark/ui/jobs/StagePage.scala
index b01fad8e45..8f7b1c2f09 100644
--- a/core/src/main/scala/org/apache/spark/ui/jobs/StagePage.scala
+++ b/core/src/main/scala/org/apache/spark/ui/jobs/StagePage.scala
@@ -81,7 +81,7 @@ private[ui] class StagePage(parent: StagesTab) extends WebUIPage("stage") {
         <div>
           <ul class="unstyled">
             <li>
-              <strong>Total task time across all tasks: </strong>
+              <strong>Total Time Across All Tasks: </strong>
               {UIUtils.formatDuration(stageData.executorRunTime)}
             </li>
             {if (stageData.hasInput) {
@@ -98,25 +98,25 @@ private[ui] class StagePage(parent: StagesTab) extends WebUIPage("stage") {
             }}
             {if (stageData.hasShuffleRead) {
               <li>
-                <strong>Shuffle read: </strong>
+                <strong>Shuffle Read: </strong>
                 {s"${Utils.bytesToString(stageData.shuffleReadTotalBytes)} / " +
                  s"${stageData.shuffleReadRecords}"}
               </li>
             }}
             {if (stageData.hasShuffleWrite) {
               <li>
-                <strong>Shuffle write: </strong>
+                <strong>Shuffle Write: </strong>
                  {s"${Utils.bytesToString(stageData.shuffleWriteBytes)} / " +
                  s"${stageData.shuffleWriteRecords}"}
               </li>
             }}
             {if (stageData.hasBytesSpilled) {
               <li>
-                <strong>Shuffle spill (memory): </strong>
+                <strong>Shuffle Spill (Memory): </strong>
                 {Utils.bytesToString(stageData.memoryBytesSpilled)}
               </li>
               <li>
-                <strong>Shuffle spill (disk): </strong>
+                <strong>Shuffle Spill (Disk): </strong>
                 {Utils.bytesToString(stageData.diskBytesSpilled)}
               </li>
             }}
@@ -127,10 +127,10 @@ private[ui] class StagePage(parent: StagesTab) extends WebUIPage("stage") {
         <div>
           <span class="expand-additional-metrics">
             <span class="expand-additional-metrics-arrow arrow-closed"></span>
-            <strong>Show additional metrics</strong>
+            <a>Show Additional Metrics</a>
           </span>
           <div class="additional-metrics collapsed">
-            <ul style="list-style-type:none">
+            <ul>
               <li>
                   <input type="checkbox" id="select-all-metrics"/>
                   <span class="additional-metric-title"><em>(De)select All</em></span>
@@ -457,9 +457,9 @@ private[ui] class StagePage(parent: StagesTab) extends WebUIPage("stage") {
 
       val content =
         summary ++
-        showAdditionalMetrics ++
         dagViz ++
         maybeExpandDagViz ++
+        showAdditionalMetrics ++
         <h4>Summary Metrics for {numCompleted} Completed Tasks</h4> ++
         <div>{summaryTable.getOrElse("No tasks have reported metrics yet.")}</div> ++
         <h4>Aggregated Metrics by Executor</h4> ++ executorTable.toNodeSeq ++
diff --git a/core/src/main/scala/org/apache/spark/ui/scope/RDDOperationGraph.scala b/core/src/main/scala/org/apache/spark/ui/scope/RDDOperationGraph.scala
index 2b2db9e62b..c7045c98c8 100644
--- a/core/src/main/scala/org/apache/spark/ui/scope/RDDOperationGraph.scala
+++ b/core/src/main/scala/org/apache/spark/ui/scope/RDDOperationGraph.scala
@@ -182,7 +182,7 @@ private[ui] object RDDOperationGraph extends Logging {
     if (forJob) {
       s"""${node.id} [label="$label" shape="circle" padding="5" labelStyle="font-size: 0"]"""
     } else {
-      s"""${node.id} [label="$label" padding="5" labelStyle="font-size: 10"]"""
+      s"""${node.id} [label="$label" padding="5" labelStyle="font-size: 12px"]"""
     }
   }
 
-- 
GitLab