Skip to content

Commit 339441f

Browse files
kayousterhoutrxin
authored andcommitted
[SPARK-2384] Add tooltips to UI.
This patch adds tooltips to clarify some points of confusion in the UI. When users mouse over some of the table headers (shuffle read, write, and input size) as well as over the "scheduler delay" metric shown for each stage, a black tool tip (see image below) pops up describing the metric in more detail. After the tooltip mechanism is added by this commit, I imagine others may want to add more tooltips for other things in the UI, but I think this is a good starting point. ![tooltip](https://cloud.githubusercontent.com/assets/1108612/3491905/994e179e-059f-11e4-92f2-c6c12d248d81.jpg) This looks scary-big but much of it is adding the bootstrap tool tip JavaScript. Also I have no idea what to put for the license in tooltip (I left it the same -- the Twitter apache header) or for JQuery (left it as nothing) -- @mateiz what's the right thing here? cc @pwendell @andrewor14 @rxin Author: Kay Ousterhout <[email protected]> Closes apache#1314 from kayousterhout/tooltips and squashes the following commits: 19981b5 [Kay Ousterhout] Exclude non-licensed javascript files from style check d9ab5a9 [Kay Ousterhout] Response to Andrew's review 7752449 [Kay Ousterhout] [SPARK-2384] Add tooltips to UI.
1 parent 1114207 commit 339441f

File tree

11 files changed

+491
-93
lines changed

11 files changed

+491
-93
lines changed

.rat-excludes

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ slaves
2222
spark-env.sh
2323
spark-env.sh.template
2424
log4j-defaults.properties
25+
bootstrap-tooltip.js
26+
jquery-1.11.1.min.js
2527
sorttable.js
2628
.*txt
2729
.*json
Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
/* ===========================================================
2+
* bootstrap-tooltip.js v2.2.2
3+
* http://twitter.github.com/bootstrap/javascript.html#tooltips
4+
* Inspired by the original jQuery.tipsy by Jason Frame
5+
* ===========================================================
6+
* Copyright 2012 Twitter, Inc.
7+
*
8+
* Licensed under the Apache License, Version 2.0 (the "License");
9+
* you may not use this file except in compliance with the License.
10+
* You may obtain a copy of the License at
11+
*
12+
* http://www.apache.org/licenses/LICENSE-2.0
13+
*
14+
* Unless required by applicable law or agreed to in writing, software
15+
* distributed under the License is distributed on an "AS IS" BASIS,
16+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
* See the License for the specific language governing permissions and
18+
* limitations under the License.
19+
* ========================================================== */
20+
21+
22+
!function ($) {
23+
24+
"use strict"; // jshint ;_;
25+
26+
27+
/* TOOLTIP PUBLIC CLASS DEFINITION
28+
* =============================== */
29+
30+
var Tooltip = function (element, options) {
31+
this.init('tooltip', element, options)
32+
}
33+
34+
Tooltip.prototype = {
35+
36+
constructor: Tooltip
37+
38+
, init: function (type, element, options) {
39+
var eventIn
40+
, eventOut
41+
42+
this.type = type
43+
this.$element = $(element)
44+
this.options = this.getOptions(options)
45+
this.enabled = true
46+
47+
if (this.options.trigger == 'click') {
48+
this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this))
49+
} else if (this.options.trigger != 'manual') {
50+
eventIn = this.options.trigger == 'hover' ? 'mouseenter' : 'focus'
51+
eventOut = this.options.trigger == 'hover' ? 'mouseleave' : 'blur'
52+
this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this))
53+
this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this))
54+
}
55+
56+
this.options.selector ?
57+
(this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) :
58+
this.fixTitle()
59+
}
60+
61+
, getOptions: function (options) {
62+
options = $.extend({}, $.fn[this.type].defaults, options, this.$element.data())
63+
64+
if (options.delay && typeof options.delay == 'number') {
65+
options.delay = {
66+
show: options.delay
67+
, hide: options.delay
68+
}
69+
}
70+
71+
return options
72+
}
73+
74+
, enter: function (e) {
75+
var self = $(e.currentTarget)[this.type](this._options).data(this.type)
76+
77+
if (!self.options.delay || !self.options.delay.show) return self.show()
78+
79+
clearTimeout(this.timeout)
80+
self.hoverState = 'in'
81+
this.timeout = setTimeout(function() {
82+
if (self.hoverState == 'in') self.show()
83+
}, self.options.delay.show)
84+
}
85+
86+
, leave: function (e) {
87+
var self = $(e.currentTarget)[this.type](this._options).data(this.type)
88+
89+
if (this.timeout) clearTimeout(this.timeout)
90+
if (!self.options.delay || !self.options.delay.hide) return self.hide()
91+
92+
self.hoverState = 'out'
93+
this.timeout = setTimeout(function() {
94+
if (self.hoverState == 'out') self.hide()
95+
}, self.options.delay.hide)
96+
}
97+
98+
, show: function () {
99+
var $tip
100+
, inside
101+
, pos
102+
, actualWidth
103+
, actualHeight
104+
, placement
105+
, tp
106+
107+
if (this.hasContent() && this.enabled) {
108+
$tip = this.tip()
109+
this.setContent()
110+
111+
if (this.options.animation) {
112+
$tip.addClass('fade')
113+
}
114+
115+
placement = typeof this.options.placement == 'function' ?
116+
this.options.placement.call(this, $tip[0], this.$element[0]) :
117+
this.options.placement
118+
119+
inside = /in/.test(placement)
120+
121+
$tip
122+
.detach()
123+
.css({ top: 0, left: 0, display: 'block' })
124+
.insertAfter(this.$element)
125+
126+
pos = this.getPosition(inside)
127+
128+
actualWidth = $tip[0].offsetWidth
129+
actualHeight = $tip[0].offsetHeight
130+
131+
switch (inside ? placement.split(' ')[1] : placement) {
132+
case 'bottom':
133+
tp = {top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2}
134+
break
135+
case 'top':
136+
tp = {top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2}
137+
break
138+
case 'left':
139+
tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth}
140+
break
141+
case 'right':
142+
tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width}
143+
break
144+
}
145+
146+
$tip
147+
.offset(tp)
148+
.addClass(placement)
149+
.addClass('in')
150+
}
151+
}
152+
153+
, setContent: function () {
154+
var $tip = this.tip()
155+
, title = this.getTitle()
156+
157+
$tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title)
158+
$tip.removeClass('fade in top bottom left right')
159+
}
160+
161+
, hide: function () {
162+
var that = this
163+
, $tip = this.tip()
164+
165+
$tip.removeClass('in')
166+
167+
function removeWithAnimation() {
168+
var timeout = setTimeout(function () {
169+
$tip.off($.support.transition.end).detach()
170+
}, 500)
171+
172+
$tip.one($.support.transition.end, function () {
173+
clearTimeout(timeout)
174+
$tip.detach()
175+
})
176+
}
177+
178+
$.support.transition && this.$tip.hasClass('fade') ?
179+
removeWithAnimation() :
180+
$tip.detach()
181+
182+
return this
183+
}
184+
185+
, fixTitle: function () {
186+
var $e = this.$element
187+
if ($e.attr('title') || typeof($e.attr('data-original-title')) != 'string') {
188+
$e.attr('data-original-title', $e.attr('title') || '').attr('title', '')
189+
}
190+
}
191+
192+
, hasContent: function () {
193+
return this.getTitle()
194+
}
195+
196+
, getPosition: function (inside) {
197+
return $.extend({}, (inside ? {top: 0, left: 0} : this.$element.offset()), {
198+
width: this.$element[0].offsetWidth
199+
, height: this.$element[0].offsetHeight
200+
})
201+
}
202+
203+
, getTitle: function () {
204+
var title
205+
, $e = this.$element
206+
, o = this.options
207+
208+
title = $e.attr('data-original-title')
209+
|| (typeof o.title == 'function' ? o.title.call($e[0]) : o.title)
210+
211+
return title
212+
}
213+
214+
, tip: function () {
215+
return this.$tip = this.$tip || $(this.options.template)
216+
}
217+
218+
, validate: function () {
219+
if (!this.$element[0].parentNode) {
220+
this.hide()
221+
this.$element = null
222+
this.options = null
223+
}
224+
}
225+
226+
, enable: function () {
227+
this.enabled = true
228+
}
229+
230+
, disable: function () {
231+
this.enabled = false
232+
}
233+
234+
, toggleEnabled: function () {
235+
this.enabled = !this.enabled
236+
}
237+
238+
, toggle: function (e) {
239+
var self = $(e.currentTarget)[this.type](this._options).data(this.type)
240+
self[self.tip().hasClass('in') ? 'hide' : 'show']()
241+
}
242+
243+
, destroy: function () {
244+
this.hide().$element.off('.' + this.type).removeData(this.type)
245+
}
246+
247+
}
248+
249+
250+
/* TOOLTIP PLUGIN DEFINITION
251+
* ========================= */
252+
253+
var old = $.fn.tooltip
254+
255+
$.fn.tooltip = function ( option ) {
256+
return this.each(function () {
257+
var $this = $(this)
258+
, data = $this.data('tooltip')
259+
, options = typeof option == 'object' && option
260+
if (!data) $this.data('tooltip', (data = new Tooltip(this, options)))
261+
if (typeof option == 'string') data[option]()
262+
})
263+
}
264+
265+
$.fn.tooltip.Constructor = Tooltip
266+
267+
$.fn.tooltip.defaults = {
268+
animation: true
269+
, placement: 'top'
270+
, selector: false
271+
, template: '<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
272+
, trigger: 'hover'
273+
, title: ''
274+
, delay: 0
275+
, html: false
276+
}
277+
278+
279+
/* TOOLTIP NO CONFLICT
280+
* =================== */
281+
282+
$.fn.tooltip.noConflict = function () {
283+
$.fn.tooltip = old
284+
return this
285+
}
286+
287+
}(window.jQuery);
288+
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
$(document).ready(function(){
19+
$("[data-toggle=tooltip]").tooltip({container: 'body'});
20+
});
21+

core/src/main/resources/org/apache/spark/ui/static/jquery-1.11.1.min.js

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/src/main/resources/org/apache/spark/ui/static/webui.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,8 @@ pre {
112112
padding-bottom: 0;
113113
border: none;
114114
}
115+
116+
.tooltip {
117+
font-weight: normal;
118+
}
119+
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.spark.ui
19+
20+
private[spark] object ToolTips {
21+
val SCHEDULER_DELAY =
22+
"""Scheduler delay includes time to ship the task from the scheduler to
23+
the executor, and time the time to send a message from the executor to the scheduler stating
24+
that the task has completed. When the scheduler becomes overloaded, task completion messages
25+
become queued up, and scheduler delay increases."""
26+
27+
val INPUT = "Bytes read from Hadoop or from Spark storage."
28+
29+
val SHUFFLE_WRITE = "Bytes written to disk in order to be read by a shuffle in a future stage."
30+
31+
val SHUFFLE_READ =
32+
"""Bytes read from remote executors. Typically less than shuffle write bytes
33+
because this does not include shuffle data read locally."""
34+
}

0 commit comments

Comments
 (0)