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 a896f7f054e27..35ef14e5aaf1a 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
@@ -168,3 +168,15 @@ tr.corresponding-item-hover>td, tr.corresponding-item-hover>th {
span.expand-application-timeline, span.expand-job-timeline {
cursor: pointer;
}
+
+.control-panel input+span {
+ cursor: pointer;
+}
+
+.vis.timeline .item.range .content {
+ position: unset;
+}
+
+.vis.timeline .item .tooltip-inner {
+ max-width: unset !important;
+}
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 80b008e7c3263..e4a891d47f035 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
@@ -35,27 +35,34 @@ function drawApplicationTimeline(groupArray, eventObjArray, startTime) {
applicationTimeline.setItems(items);
setupZoomable("#application-timeline-zoom-lock", applicationTimeline);
-
- $(".item.range.job.application-timeline-object").each(function() {
- var getJobId = function(baseElem) {
- var jobIdText = $($(baseElem).find(".application-timeline-content")[0]).text();
- var jobId = jobIdText.match("\\(Job (\\d+)\\)")[1];
- return jobId;
- };
-
- $(this).click(function() {
- window.location.href = "job/?id=" + getJobId(this);
+ setupExecutorEventAction();
+
+ function setupJobEventAction() {
+ $(".item.range.job.application-timeline-object").each(function() {
+ var getJobId = function(baseElem) {
+ var jobIdText = $($(baseElem).find(".application-timeline-content")[0]).text();
+ var jobId = jobIdText.match("\\(Job (\\d+)\\)")[1];
+ return jobId;
+ };
+
+ $(this).click(function() {
+ window.location.href = "job/?id=" + getJobId(this);
+ });
+
+ $(this).hover(
+ function() {
+ $("#job-" + getJobId(this)).addClass("corresponding-item-hover");
+ $($(this).find("div.application-timeline-content")[0]).tooltip("show");
+ },
+ function() {
+ $("#job-" + getJobId(this)).removeClass("corresponding-item-hover");
+ $($(this).find("div.application-timeline-content")[0]).tooltip("hide");
+ }
+ );
});
+ }
- $(this).hover(
- function() {
- $("#job-" + getJobId(this)).addClass("corresponding-item-hover");
- },
- function() {
- $("#job-" + getJobId(this)).removeClass("corresponding-item-hover");
- }
- );
- });
+ setupJobEventAction();
$("span.expand-application-timeline").click(function() {
$("#application-timeline").toggleClass('collapsed');
@@ -86,36 +93,43 @@ function drawJobTimeline(groupArray, eventObjArray, startTime) {
jobTimeline.setItems(items);
setupZoomable("#job-timeline-zoom-lock", jobTimeline);
+ setupExecutorEventAction();
- $(".item.range.stage.job-timeline-object").each(function() {
- var getStageIdAndAttempt = function(baseElem) {
- var stageIdText = $($(baseElem).find(".job-timeline-content")[0]).text();
- var stageIdAndAttempt = stageIdText.match("\\(Stage (\\d+\\.\\d+)\\)")[1].split(".");
- return stageIdAndAttempt;
- };
-
- $(this).click(function() {
- var idAndAttempt = getStageIdAndAttempt(this);
- var id = idAndAttempt[0];
- var attempt = idAndAttempt[1];
- window.location.href = "../../stages/stage/?id=" + id + "&attempt=" + attempt;
- });
+ function setupStageEventAction() {
+ $(".item.range.stage.job-timeline-object").each(function() {
+ var getStageIdAndAttempt = function(baseElem) {
+ var stageIdText = $($(baseElem).find(".job-timeline-content")[0]).text();
+ var stageIdAndAttempt = stageIdText.match("\\(Stage (\\d+\\.\\d+)\\)")[1].split(".");
+ return stageIdAndAttempt;
+ };
- $(this).hover(
- function() {
+ $(this).click(function() {
var idAndAttempt = getStageIdAndAttempt(this);
var id = idAndAttempt[0];
var attempt = idAndAttempt[1];
- $("#stage-" + id + "-" + attempt).addClass("corresponding-item-hover");
- },
- function() {
- var idAndAttempt = getStageIdAndAttempt(this);
- var id = idAndAttempt[0];
- var attempt = idAndAttempt[1];
- $("#stage-" + id + "-" + attempt).removeClass("corresponding-item-hover");
- }
- );
- });
+ window.location.href = "../../stages/stage/?id=" + id + "&attempt=" + attempt;
+ });
+
+ $(this).hover(
+ function() {
+ var idAndAttempt = getStageIdAndAttempt(this);
+ var id = idAndAttempt[0];
+ var attempt = idAndAttempt[1];
+ $("#stage-" + id + "-" + attempt).addClass("corresponding-item-hover");
+ $($(this).find("div.job-timeline-content")[0]).tooltip("show");
+ },
+ function() {
+ var idAndAttempt = getStageIdAndAttempt(this);
+ var id = idAndAttempt[0];
+ var attempt = idAndAttempt[1];
+ $("#stage-" + id + "-" + attempt).removeClass("corresponding-item-hover");
+ $($(this).find("div.job-timeline-content")[0]).tooltip("hide");
+ }
+ );
+ });
+ }
+
+ setupStageEventAction();
$("span.expand-job-timeline").click(function() {
$("#job-timeline").toggleClass('collapsed');
@@ -126,6 +140,19 @@ function drawJobTimeline(groupArray, eventObjArray, startTime) {
});
}
+function setupExecutorEventAction() {
+ $(".item.box.executor").each(function () {
+ $(this).hover(
+ function() {
+ $($(this).find(".executor-event-content")[0]).tooltip("show");
+ },
+ function() {
+ $($(this).find(".executor-event-content")[0]).tooltip("hide");
+ }
+ );
+ });
+}
+
function setupZoomable(id, timeline) {
$(id + '>input[type="checkbox"]').click(function() {
if (this.checked) {
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 123e376668a33..a7ea12b1655fe 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
@@ -88,17 +88,18 @@ private[ui] class AllJobsPage(parent: JobsTab) extends WebUIPage("") {
| 'group': 'jobs',
| 'start': new Date(${submissionTime}),
| 'end': new Date(${completionTime}),
- | 'content': '
' +
- | '${displayJobDescription} (Job ${jobId})
',
- | 'title': '${displayJobDescription} (Job ${jobId})\\nStatus: ${status}\\n' +
- | 'Submission Time: ${UIUtils.formatDate(new Date(submissionTime))}' +
- | '${
- if (status != JobExecutionStatus.RUNNING) {
- s"""\\nCompletion Time: ${UIUtils.formatDate(new Date(completionTime))}"""
- } else {
- ""
- }
- }'
+ | 'content': 'Status: ${status}
' +
+ | 'Submission Time: ${UIUtils.formatDate(new Date(submissionTime))}' +
+ | '${
+ if (status != JobExecutionStatus.RUNNING) {
+ s"""
Completion Time: ${UIUtils.formatDate(new Date(completionTime))}"""
+ } else {
+ ""
+ }
+ }">' +
+ | '${displayJobDescription} (Job ${jobId})
'
|}
""".stripMargin
jobEventJsonAsStr
@@ -109,38 +110,44 @@ private[ui] class AllJobsPage(parent: JobsTab) extends WebUIPage("") {
val events = ListBuffer[String]()
executorUIDatas.foreach {
case (executorId, event) =>
- val addedEvent =
- s"""
- |{
- | 'className': 'executor added',
- | 'group': 'executors',
- | 'start': new Date(${event.startTime}),
- | 'content': 'Executor ${executorId} added
',
- | 'title': 'Added at ${UIUtils.formatDate(new Date(event.startTime))}'
- |}
- """.stripMargin
- events += addedEvent
-
- if (event.finishTime.isDefined) {
- val removedEvent =
+ val addedEvent =
s"""
+ |{
+ | 'className': 'executor added',
+ | 'group': 'executors',
+ | 'start': new Date(${event.startTime}),
+ | 'content': '' +
+ | 'Added at ${UIUtils.formatDate(new Date(event.startTime))}"' +
+ | 'data-html="true">Executor ${executorId} added
'
+ |}
+ """.stripMargin
+ events += addedEvent
+
+ if (event.finishTime.isDefined) {
+ val removedEvent =
+ s"""
|{
| 'className': 'executor removed',
| 'group': 'executors',
| 'start': new Date(${event.finishTime.get}),
- | 'content': 'Executor ${executorId} removed
',
- | 'title': 'Removed at ${UIUtils.formatDate(new Date(event.finishTime.get))}' +
+ | 'content': '' +
+ | 'Removed at ${UIUtils.formatDate(new Date(event.finishTime.get))}' +
| '${
if (event.finishReason.isDefined) {
- s"""\\nReason: ${event.finishReason.get}"""
+ s"""
Reason: ${event.finishReason.get}"""
} else {
""
}
- }'
+ }"' +
+ | 'data-html="true">Executor ${executorId} removed
'
|}
""".stripMargin
- events += removedEvent
- }
+ events += removedEvent
+ }
}
events.toSeq
}
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 e06cbfaadbd3a..dd968e124738e 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
@@ -48,10 +48,10 @@ private[ui] class JobPage(parent: JobsTab) extends WebUIPage("job") {
.toString.filter(_ != '\n')
private def makeStageEvent(stageInfos: Seq[StageInfo]): Seq[String] = {
@@ -69,17 +69,19 @@ private[ui] class JobPage(parent: JobsTab) extends WebUIPage("job") {
| 'group': 'stages',
| 'start': new Date(${submissionTime}),
| 'end': new Date(${completionTime}),
- | 'content': '' +
+ | 'content': '
' +
+ | 'Status: ${status.toUpperCase}
' +
+ | 'Submission Time: ${UIUtils.formatDate(new Date(submissionTime))}' +
+ | '${
+ if (status != "running") {
+ s"""
Completion Time: ${UIUtils.formatDate(new Date(completionTime))}"""
+ } else {
+ ""
+ }
+ }">' +
| '${name} (Stage ${stageId}.${attemptId})
',
- | 'title': '${name} (Stage ${stageId}.${attemptId})\\nStatus: ${status.toUpperCase}\\n' +
- | 'Submission Time: ${UIUtils.formatDate(new Date(submissionTime))}' +
- | '${
- if (status != "running") {
- s"""\\nCompletion Time: ${UIUtils.formatDate(new Date(completionTime))}"""
- } else {
- ""
- }
- }'
|}
""".stripMargin
}
@@ -89,38 +91,44 @@ private[ui] class JobPage(parent: JobsTab) extends WebUIPage("job") {
val events = ListBuffer[String]()
executorUIDatas.foreach {
case (executorId, event) =>
- val addedEvent =
- s"""
- |{
- | 'className': 'executor added',
- | 'group': 'executors',
- | 'start': new Date(${event.startTime}),
- | 'content': '
Executor ${executorId} added
',
- | 'title': 'Added at ${UIUtils.formatDate(new Date(event.startTime))}'
- |}
- """.stripMargin
- events += addedEvent
-
- if (event.finishTime.isDefined) {
- val removedEvent =
+ val addedEvent =
s"""
+ |{
+ | 'className': 'executor added',
+ | 'group': 'executors',
+ | 'start': new Date(${event.startTime}),
+ | 'content': '
' +
+ | 'Added at ${UIUtils.formatDate(new Date(event.startTime))}"' +
+ | 'data-html="true">Executor ${executorId} added
'
+ |}
+ """.stripMargin
+ events += addedEvent
+
+ if (event.finishTime.isDefined) {
+ val removedEvent =
+ s"""
|{
| 'className': 'executor removed',
| 'group': 'executors',
| 'start': new Date(${event.finishTime.get}),
- | 'content': '
Executor ${executorId} removed
',
- | 'title': 'Removed at ${UIUtils.formatDate(new Date(event.finishTime.get))}' +
+ | 'content': '
' +
+ | 'Removed at ${UIUtils.formatDate(new Date(event.finishTime.get))}' +
| '${
if (event.finishReason.isDefined) {
- s"""\\nReason: ${event.finishReason.get}"""
+ s"""
Reason: ${event.finishReason.get}"""
} else {
""
}
- }'
+ }"' +
+ | 'data-html="true">Executor ${executorId} removed
'
|}
""".stripMargin
- events += removedEvent
- }
+ events += removedEvent
+ }
}
events.toSeq
}
@@ -128,7 +136,7 @@ private[ui] class JobPage(parent: JobsTab) extends WebUIPage("job") {
private def makeTimeline(
stages: Seq[StageInfo],
executors: HashMap[String, ExecutorUIData],
- jobStartTime: Long): Seq[Node] = {
+ appStartTime: Long): Seq[Node] = {
val stageEventJsonAsStrSeq = makeStageEvent(stages)
val executorsJsonAsStrSeq = makeExecutorEvent(executors)
@@ -163,7 +171,7 @@ private[ui] class JobPage(parent: JobsTab) extends WebUIPage("job") {
++
}
@@ -293,10 +301,10 @@ private[ui] class JobPage(parent: JobsTab) extends WebUIPage("job") {
var content = summary
- val jobStartTime = jobData.submissionTime.get
+ val appStartTime = listener.startTime
val executorListener = parent.executorListener
content ++= makeTimeline(activeStages ++ completedStages ++ failedStages,
- executorListener.executorIdToData, jobStartTime)
+ executorListener.executorIdToData, appStartTime)
if (shouldShowActiveStages) {
content ++= Active Stages ({activeStages.size})
++