Skip to content

Commit ef34a5b

Browse files
committed
Implement tooltip using bootstrap
1 parent b09d0c5 commit ef34a5b

File tree

4 files changed

+165
-111
lines changed

4 files changed

+165
-111
lines changed

core/src/main/resources/org/apache/spark/ui/static/timeline-view.css

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,3 +168,15 @@ tr.corresponding-item-hover>td, tr.corresponding-item-hover>th {
168168
span.expand-application-timeline, span.expand-job-timeline {
169169
cursor: pointer;
170170
}
171+
172+
.control-panel input+span {
173+
cursor: pointer;
174+
}
175+
176+
.vis.timeline .item.range .content {
177+
position: unset;
178+
}
179+
180+
.vis.timeline .item .tooltip-inner {
181+
max-width: unset !important;
182+
}

core/src/main/resources/org/apache/spark/ui/static/timeline-view.js

Lines changed: 71 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -35,27 +35,34 @@ function drawApplicationTimeline(groupArray, eventObjArray, startTime) {
3535
applicationTimeline.setItems(items);
3636

3737
setupZoomable("#application-timeline-zoom-lock", applicationTimeline);
38-
39-
$(".item.range.job.application-timeline-object").each(function() {
40-
var getJobId = function(baseElem) {
41-
var jobIdText = $($(baseElem).find(".application-timeline-content")[0]).text();
42-
var jobId = jobIdText.match("\\(Job (\\d+)\\)")[1];
43-
return jobId;
44-
};
45-
46-
$(this).click(function() {
47-
window.location.href = "job/?id=" + getJobId(this);
38+
setupExecutorEventAction();
39+
40+
function setupJobEventAction() {
41+
$(".item.range.job.application-timeline-object").each(function() {
42+
var getJobId = function(baseElem) {
43+
var jobIdText = $($(baseElem).find(".application-timeline-content")[0]).text();
44+
var jobId = jobIdText.match("\\(Job (\\d+)\\)")[1];
45+
return jobId;
46+
};
47+
48+
$(this).click(function() {
49+
window.location.href = "job/?id=" + getJobId(this);
50+
});
51+
52+
$(this).hover(
53+
function() {
54+
$("#job-" + getJobId(this)).addClass("corresponding-item-hover");
55+
$($(this).find("div.application-timeline-content")[0]).tooltip("show");
56+
},
57+
function() {
58+
$("#job-" + getJobId(this)).removeClass("corresponding-item-hover");
59+
$($(this).find("div.application-timeline-content")[0]).tooltip("hide");
60+
}
61+
);
4862
});
63+
}
4964

50-
$(this).hover(
51-
function() {
52-
$("#job-" + getJobId(this)).addClass("corresponding-item-hover");
53-
},
54-
function() {
55-
$("#job-" + getJobId(this)).removeClass("corresponding-item-hover");
56-
}
57-
);
58-
});
65+
setupJobEventAction();
5966

6067
$("span.expand-application-timeline").click(function() {
6168
$("#application-timeline").toggleClass('collapsed');
@@ -86,36 +93,43 @@ function drawJobTimeline(groupArray, eventObjArray, startTime) {
8693
jobTimeline.setItems(items);
8794

8895
setupZoomable("#job-timeline-zoom-lock", jobTimeline);
96+
setupExecutorEventAction();
8997

90-
$(".item.range.stage.job-timeline-object").each(function() {
91-
var getStageIdAndAttempt = function(baseElem) {
92-
var stageIdText = $($(baseElem).find(".job-timeline-content")[0]).text();
93-
var stageIdAndAttempt = stageIdText.match("\\(Stage (\\d+\\.\\d+)\\)")[1].split(".");
94-
return stageIdAndAttempt;
95-
};
96-
97-
$(this).click(function() {
98-
var idAndAttempt = getStageIdAndAttempt(this);
99-
var id = idAndAttempt[0];
100-
var attempt = idAndAttempt[1];
101-
window.location.href = "../../stages/stage/?id=" + id + "&attempt=" + attempt;
102-
});
98+
function setupStageEventAction() {
99+
$(".item.range.stage.job-timeline-object").each(function() {
100+
var getStageIdAndAttempt = function(baseElem) {
101+
var stageIdText = $($(baseElem).find(".job-timeline-content")[0]).text();
102+
var stageIdAndAttempt = stageIdText.match("\\(Stage (\\d+\\.\\d+)\\)")[1].split(".");
103+
return stageIdAndAttempt;
104+
};
103105

104-
$(this).hover(
105-
function() {
106+
$(this).click(function() {
106107
var idAndAttempt = getStageIdAndAttempt(this);
107108
var id = idAndAttempt[0];
108109
var attempt = idAndAttempt[1];
109-
$("#stage-" + id + "-" + attempt).addClass("corresponding-item-hover");
110-
},
111-
function() {
112-
var idAndAttempt = getStageIdAndAttempt(this);
113-
var id = idAndAttempt[0];
114-
var attempt = idAndAttempt[1];
115-
$("#stage-" + id + "-" + attempt).removeClass("corresponding-item-hover");
116-
}
117-
);
118-
});
110+
window.location.href = "../../stages/stage/?id=" + id + "&attempt=" + attempt;
111+
});
112+
113+
$(this).hover(
114+
function() {
115+
var idAndAttempt = getStageIdAndAttempt(this);
116+
var id = idAndAttempt[0];
117+
var attempt = idAndAttempt[1];
118+
$("#stage-" + id + "-" + attempt).addClass("corresponding-item-hover");
119+
$($(this).find("div.job-timeline-content")[0]).tooltip("show");
120+
},
121+
function() {
122+
var idAndAttempt = getStageIdAndAttempt(this);
123+
var id = idAndAttempt[0];
124+
var attempt = idAndAttempt[1];
125+
$("#stage-" + id + "-" + attempt).removeClass("corresponding-item-hover");
126+
$($(this).find("div.job-timeline-content")[0]).tooltip("hide");
127+
}
128+
);
129+
});
130+
}
131+
132+
setupStageEventAction();
119133

120134
$("span.expand-job-timeline").click(function() {
121135
$("#job-timeline").toggleClass('collapsed');
@@ -126,6 +140,19 @@ function drawJobTimeline(groupArray, eventObjArray, startTime) {
126140
});
127141
}
128142

143+
function setupExecutorEventAction() {
144+
$(".item.box.executor").each(function () {
145+
$(this).hover(
146+
function() {
147+
$($(this).find(".executor-event-content")[0]).tooltip("show");
148+
},
149+
function() {
150+
$($(this).find(".executor-event-content")[0]).tooltip("hide");
151+
}
152+
);
153+
});
154+
}
155+
129156
function setupZoomable(id, timeline) {
130157
$(id + '>input[type="checkbox"]').click(function() {
131158
if (this.checked) {

core/src/main/scala/org/apache/spark/ui/jobs/AllJobsPage.scala

Lines changed: 38 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -88,17 +88,18 @@ private[ui] class AllJobsPage(parent: JobsTab) extends WebUIPage("") {
8888
| 'group': 'jobs',
8989
| 'start': new Date(${submissionTime}),
9090
| 'end': new Date(${completionTime}),
91-
| 'content': '<div class="application-timeline-content">' +
92-
| '${displayJobDescription} (Job ${jobId})</div>',
93-
| 'title': '${displayJobDescription} (Job ${jobId})\\nStatus: ${status}\\n' +
94-
| 'Submission Time: ${UIUtils.formatDate(new Date(submissionTime))}' +
95-
| '${
96-
if (status != JobExecutionStatus.RUNNING) {
97-
s"""\\nCompletion Time: ${UIUtils.formatDate(new Date(completionTime))}"""
98-
} else {
99-
""
100-
}
101-
}'
91+
| 'content': '<div class="application-timeline-content"' +
92+
| 'data-html="true" data-placement="top" data-toggle="tooltip"' +
93+
| 'data-title="${displayJobDescription} (Job ${jobId})<br>Status: ${status}<br>' +
94+
| 'Submission Time: ${UIUtils.formatDate(new Date(submissionTime))}' +
95+
| '${
96+
if (status != JobExecutionStatus.RUNNING) {
97+
s"""<br>Completion Time: ${UIUtils.formatDate(new Date(completionTime))}"""
98+
} else {
99+
""
100+
}
101+
}">' +
102+
| '${displayJobDescription} (Job ${jobId})</div>'
102103
|}
103104
""".stripMargin
104105
jobEventJsonAsStr
@@ -109,38 +110,44 @@ private[ui] class AllJobsPage(parent: JobsTab) extends WebUIPage("") {
109110
val events = ListBuffer[String]()
110111
executorUIDatas.foreach {
111112
case (executorId, event) =>
112-
val addedEvent =
113-
s"""
114-
|{
115-
| 'className': 'executor added',
116-
| 'group': 'executors',
117-
| 'start': new Date(${event.startTime}),
118-
| 'content': '<div>Executor ${executorId} added</div>',
119-
| 'title': 'Added at ${UIUtils.formatDate(new Date(event.startTime))}'
120-
|}
121-
""".stripMargin
122-
events += addedEvent
123-
124-
if (event.finishTime.isDefined) {
125-
val removedEvent =
113+
val addedEvent =
126114
s"""
115+
|{
116+
| 'className': 'executor added',
117+
| 'group': 'executors',
118+
| 'start': new Date(${event.startTime}),
119+
| 'content': '<div class="executor-event-content"' +
120+
| 'data-toggle="tooltip" data-placement="bottom"' +
121+
| 'data-title="Executor ${executorId}<br>' +
122+
| 'Added at ${UIUtils.formatDate(new Date(event.startTime))}"' +
123+
| 'data-html="true">Executor ${executorId} added</div>'
124+
|}
125+
""".stripMargin
126+
events += addedEvent
127+
128+
if (event.finishTime.isDefined) {
129+
val removedEvent =
130+
s"""
127131
|{
128132
| 'className': 'executor removed',
129133
| 'group': 'executors',
130134
| 'start': new Date(${event.finishTime.get}),
131-
| 'content': '<div>Executor ${executorId} removed</div>',
132-
| 'title': 'Removed at ${UIUtils.formatDate(new Date(event.finishTime.get))}' +
135+
| 'content': '<div class="executor-event-content"' +
136+
| 'data-toggle="tooltip" data-placement="bottom"' +
137+
| 'data-title="Executor ${executorId}<br>' +
138+
| 'Removed at ${UIUtils.formatDate(new Date(event.finishTime.get))}' +
133139
| '${
134140
if (event.finishReason.isDefined) {
135-
s"""\\nReason: ${event.finishReason.get}"""
141+
s"""<br>Reason: ${event.finishReason.get}"""
136142
} else {
137143
""
138144
}
139-
}'
145+
}"' +
146+
| 'data-html="true">Executor ${executorId} removed</div>'
140147
|}
141148
""".stripMargin
142-
events += removedEvent
143-
}
149+
events += removedEvent
150+
}
144151
}
145152
events.toSeq
146153
}

core/src/main/scala/org/apache/spark/ui/jobs/JobPage.scala

Lines changed: 44 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,10 @@ private[ui] class JobPage(parent: JobsTab) extends WebUIPage("job") {
4848
<div class="legend-area"><svg width="150px" height="55px">
4949
<rect class="executor-added-legend"
5050
x="5px" y="5px" width="20px" height="15px" rx="2px" ry="2px"></rect>
51-
<text x="35px" y="17px">Executor Added</text>
51+
<text x="35px" y="17px">Added</text>
5252
<rect class="executor-removed-legend"
5353
x="5px" y="30px" width="20px" height="15px" rx="2px" ry="2px"></rect>
54-
<text x="35px" y="42px">Executor Removed</text>
54+
<text x="35px" y="42px">Removed</text>
5555
</svg></div>.toString.filter(_ != '\n')
5656

5757
private def makeStageEvent(stageInfos: Seq[StageInfo]): Seq[String] = {
@@ -69,17 +69,19 @@ private[ui] class JobPage(parent: JobsTab) extends WebUIPage("job") {
6969
| 'group': 'stages',
7070
| 'start': new Date(${submissionTime}),
7171
| 'end': new Date(${completionTime}),
72-
| 'content': '<div class="job-timeline-content">' +
72+
| 'content': '<div class="job-timeline-content" data-toggle="tooltip"' +
73+
| 'data-placement="top" data-html="true"' +
74+
| 'data-title="${name} (Stage ${stageId}.${attemptId})<br>' +
75+
| 'Status: ${status.toUpperCase}<br>' +
76+
| 'Submission Time: ${UIUtils.formatDate(new Date(submissionTime))}' +
77+
| '${
78+
if (status != "running") {
79+
s"""<br>Completion Time: ${UIUtils.formatDate(new Date(completionTime))}"""
80+
} else {
81+
""
82+
}
83+
}">' +
7384
| '${name} (Stage ${stageId}.${attemptId})</div>',
74-
| 'title': '${name} (Stage ${stageId}.${attemptId})\\nStatus: ${status.toUpperCase}\\n' +
75-
| 'Submission Time: ${UIUtils.formatDate(new Date(submissionTime))}' +
76-
| '${
77-
if (status != "running") {
78-
s"""\\nCompletion Time: ${UIUtils.formatDate(new Date(completionTime))}"""
79-
} else {
80-
""
81-
}
82-
}'
8385
|}
8486
""".stripMargin
8587
}
@@ -89,46 +91,52 @@ private[ui] class JobPage(parent: JobsTab) extends WebUIPage("job") {
8991
val events = ListBuffer[String]()
9092
executorUIDatas.foreach {
9193
case (executorId, event) =>
92-
val addedEvent =
93-
s"""
94-
|{
95-
| 'className': 'executor added',
96-
| 'group': 'executors',
97-
| 'start': new Date(${event.startTime}),
98-
| 'content': '<div>Executor ${executorId} added</div>',
99-
| 'title': 'Added at ${UIUtils.formatDate(new Date(event.startTime))}'
100-
|}
101-
""".stripMargin
102-
events += addedEvent
103-
104-
if (event.finishTime.isDefined) {
105-
val removedEvent =
94+
val addedEvent =
10695
s"""
96+
|{
97+
| 'className': 'executor added',
98+
| 'group': 'executors',
99+
| 'start': new Date(${event.startTime}),
100+
| 'content': '<div class="executor-event-content"' +
101+
| 'data-toggle="tooltip" data-placement="bottom"' +
102+
| 'data-title="Executor ${executorId}<br>' +
103+
| 'Added at ${UIUtils.formatDate(new Date(event.startTime))}"' +
104+
| 'data-html="true">Executor ${executorId} added</div>'
105+
|}
106+
""".stripMargin
107+
events += addedEvent
108+
109+
if (event.finishTime.isDefined) {
110+
val removedEvent =
111+
s"""
107112
|{
108113
| 'className': 'executor removed',
109114
| 'group': 'executors',
110115
| 'start': new Date(${event.finishTime.get}),
111-
| 'content': '<div>Executor ${executorId} removed</div>',
112-
| 'title': 'Removed at ${UIUtils.formatDate(new Date(event.finishTime.get))}' +
116+
| 'content': '<div class="executor-event-content"' +
117+
| 'data-toggle="tooltip" data-placement="bottom"' +
118+
| 'data-title="Executor ${executorId}<br>' +
119+
| 'Removed at ${UIUtils.formatDate(new Date(event.finishTime.get))}' +
113120
| '${
114121
if (event.finishReason.isDefined) {
115-
s"""\\nReason: ${event.finishReason.get}"""
122+
s"""<br>Reason: ${event.finishReason.get}"""
116123
} else {
117124
""
118125
}
119-
}'
126+
}"' +
127+
| 'data-html="true">Executor ${executorId} removed</div>'
120128
|}
121129
""".stripMargin
122-
events += removedEvent
123-
}
130+
events += removedEvent
131+
}
124132
}
125133
events.toSeq
126134
}
127135

128136
private def makeTimeline(
129137
stages: Seq[StageInfo],
130138
executors: HashMap[String, ExecutorUIData],
131-
jobStartTime: Long): Seq[Node] = {
139+
appStartTime: Long): Seq[Node] = {
132140

133141
val stageEventJsonAsStrSeq = makeStageEvent(stages)
134142
val executorsJsonAsStrSeq = makeExecutorEvent(executors)
@@ -163,7 +171,7 @@ private[ui] class JobPage(parent: JobsTab) extends WebUIPage("job") {
163171
</div>
164172
</div> ++
165173
<script type="text/javascript">
166-
{Unparsed(s"drawJobTimeline(${groupJsonArrayAsStr}, ${eventArrayAsStr}, ${jobStartTime});")}
174+
{Unparsed(s"drawJobTimeline(${groupJsonArrayAsStr}, ${eventArrayAsStr}, ${appStartTime});")}
167175
</script>
168176
}
169177

@@ -293,10 +301,10 @@ private[ui] class JobPage(parent: JobsTab) extends WebUIPage("job") {
293301
</div>
294302

295303
var content = summary
296-
val jobStartTime = jobData.submissionTime.get
304+
val appStartTime = listener.startTime
297305
val executorListener = parent.executorListener
298306
content ++= makeTimeline(activeStages ++ completedStages ++ failedStages,
299-
executorListener.executorIdToData, jobStartTime)
307+
executorListener.executorIdToData, appStartTime)
300308

301309
if (shouldShowActiveStages) {
302310
content ++= <h4 id="active">Active Stages ({activeStages.size})</h4> ++

0 commit comments

Comments
 (0)