Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

This request adds hints to the sensor UI. #2

Merged
merged 3 commits into from

1 participant

@oldpatricka

For now, this uses a static list of sensors, which we can update occasionally.

This is mostly because OpenTSDB only supports pulling 25 sensors at a time from

its API (the /suggest URL). To be able to get all of them will require patching

OpenTSDB.

Also note the patch to Bootstrap's typeahead support. This allows showing the
hints when no characters have been typed yet.

oldpatricka added some commits
@oldpatricka oldpatricka Remove extra bootstrap import 8d07a1b
@oldpatricka oldpatricka Add correct input type to sensor input 9dc0c1d
@oldpatricka oldpatricka Add metric name typeahead hints to the sensor input.
Note that this commit requires patching bootstrap Typeahead to support
showing the typeahead when no text has been entered yet.
a42c12d
@oldpatricka oldpatricka merged commit df849d1 into nimbusproject:master
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Feb 4, 2013
  1. @oldpatricka
  2. @oldpatricka
  3. @oldpatricka

    Add metric name typeahead hints to the sensor input.

    oldpatricka authored
    Note that this commit requires patching bootstrap Typeahead to support
    showing the typeahead when no text has been entered yet.
This page is out of date. Refresh to see the latest.
View
297 phantomweb/static/js/bootstrap-hacks.js
@@ -0,0 +1,297 @@
+
+
+/* Make Typeahead be able to work with minLength == 0
+ Should not be neccessary ud https://github.com/twitter/bootstrap/pull/5063 is
+ ever reopened and fixed
+*/
+
+!function($){
+
+ "use strict"; // jshint ;_;
+
+
+ /* TYPEAHEAD PUBLIC CLASS DEFINITION
+ * ================================= */
+
+ var Typeahead = function (element, options) {
+ this.$element = $(element)
+ this.options = $.extend({}, $.fn.typeahead.defaults, options)
+ this.matcher = this.options.matcher || this.matcher
+ this.sorter = this.options.sorter || this.sorter
+ this.highlighter = this.options.highlighter || this.highlighter
+ this.updater = this.options.updater || this.updater
+ this.$menu = $(this.options.menu).appendTo('body')
+ this.source = this.options.source
+ this.shown = false
+ this.listen()
+ }
+
+ Typeahead.prototype = {
+
+ constructor: Typeahead
+
+ , select: function () {
+ var val = this.$menu.find('.active').attr('data-value')
+ this.$element
+ .val(this.updater(val))
+ .change()
+ return this.hide()
+ }
+
+ , updater: function (item) {
+ return item
+ }
+
+ , show: function () {
+ var pos = $.extend({}, this.$element.offset(), {
+ height: this.$element[0].offsetHeight
+ })
+
+ this.$menu.css({
+ top: pos.top + pos.height
+ , left: pos.left
+ })
+
+ this.$menu.show()
+ this.shown = true
+ return this
+ }
+
+ , hide: function () {
+ this.$menu.hide()
+ this.shown = false
+ return this
+ }
+
+ , lookup: function (event) {
+ var items
+
+ this.query = this.$element.val() || ''
+
+ if (this.query.length < this.options.minLength) {
+ return this.shown ? this.hide() : this
+ }
+
+ items = $.isFunction(this.source) ? this.source(this.query, $.proxy(this.process, this)) : this.source
+
+ return items ? this.process(items) : this
+ }
+
+ , process: function (items) {
+ var that = this
+
+ items = $.grep(items, function (item) {
+ return that.matcher(item)
+ })
+
+ items = this.sorter(items)
+
+ if (!items.length) {
+ return this.shown ? this.hide() : this
+ }
+
+ return this.render(items.slice(0, this.options.items)).show()
+ }
+
+ , matcher: function (item) {
+ return ~item.toLowerCase().indexOf(this.query.toLowerCase())
+ }
+
+ , sorter: function (items) {
+ var beginswith = []
+ , caseSensitive = []
+ , caseInsensitive = []
+ , item
+
+ while (item = items.shift()) {
+ if (!item.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item)
+ else if (~item.indexOf(this.query)) caseSensitive.push(item)
+ else caseInsensitive.push(item)
+ }
+
+ return beginswith.concat(caseSensitive, caseInsensitive)
+ }
+
+ , highlighter: function (item) {
+ var query = this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&')
+ return item.replace(new RegExp('(' + query + ')', 'ig'), function ($1, match) {
+ return '<strong>' + match + '</strong>'
+ })
+ }
+
+ , render: function (items) {
+ var that = this
+
+ items = $(items).map(function (i, item) {
+ i = $(that.options.item).attr('data-value', item)
+ i.find('a').html(that.highlighter(item))
+ return i[0]
+ })
+
+ items.first().addClass('active')
+ this.$menu.html(items)
+ return this
+ }
+
+ , next: function (event) {
+ var active = this.$menu.find('.active').removeClass('active')
+ , next = active.next()
+
+ if (!next.length) {
+ next = $(this.$menu.find('li')[0])
+ }
+
+ next.addClass('active')
+ }
+
+ , prev: function (event) {
+ var active = this.$menu.find('.active').removeClass('active')
+ , prev = active.prev()
+
+ if (!prev.length) {
+ prev = this.$menu.find('li').last()
+ }
+
+ prev.addClass('active')
+ }
+
+ , listen: function () {
+ this.$element
+ .on('blur', $.proxy(this.blur, this))
+ .on('keypress', $.proxy(this.keypress, this))
+ .on('keyup', $.proxy(this.keyup, this))
+
+ if (this.eventSupported('keydown')) {
+ this.$element.on('keydown', $.proxy(this.keydown, this))
+ }
+
+ this.$menu
+ .on('click', $.proxy(this.click, this))
+ .on('mouseenter', 'li', $.proxy(this.mouseenter, this))
+ }
+
+ , eventSupported: function(eventName) {
+ var isSupported = eventName in this.$element
+ if (!isSupported) {
+ this.$element.setAttribute(eventName, 'return;')
+ isSupported = typeof this.$element[eventName] === 'function'
+ }
+ return isSupported
+ }
+
+ , move: function (e) {
+ if (!this.shown) return
+
+ switch(e.keyCode) {
+ case 9: // tab
+ case 13: // enter
+ case 27: // escape
+ e.preventDefault()
+ break
+
+ case 38: // up arrow
+ e.preventDefault()
+ this.prev()
+ break
+
+ case 40: // down arrow
+ e.preventDefault()
+ this.next()
+ break
+ }
+
+ e.stopPropagation()
+ }
+
+ , keydown: function (e) {
+ this.suppressKeyPressRepeat = !~$.inArray(e.keyCode, [40,38,9,13,27])
+ this.move(e)
+ }
+
+ , keypress: function (e) {
+ if (this.suppressKeyPressRepeat) return
+ this.move(e)
+ }
+
+ , keyup: function (e) {
+ switch(e.keyCode) {
+ case 40: // down arrow
+ case 38: // up arrow
+ case 16: // shift
+ case 17: // ctrl
+ case 18: // alt
+ break
+
+ case 9: // tab
+ case 13: // enter
+ if (!this.shown) return
+ this.select()
+ break
+
+ case 27: // escape
+ if (!this.shown) return
+ this.hide()
+ break
+
+ default:
+ this.lookup()
+ }
+
+ e.stopPropagation()
+ e.preventDefault()
+ }
+
+ , blur: function (e) {
+ var that = this
+ setTimeout(function () { that.hide() }, 150)
+ }
+
+ , click: function (e) {
+ e.stopPropagation()
+ e.preventDefault()
+ this.select()
+ }
+
+ , mouseenter: function (e) {
+ this.$menu.find('.active').removeClass('active')
+ $(e.currentTarget).addClass('active')
+ }
+
+ }
+
+
+ /* TYPEAHEAD PLUGIN DEFINITION
+ * =========================== */
+
+ $.fn.typeahead = function (option) {
+ return this.each(function () {
+ var $this = $(this)
+ , data = $this.data('typeahead')
+ , options = typeof option == 'object' && option
+ if (!data) $this.data('typeahead', (data = new Typeahead(this, options)))
+ if (typeof option == 'string') data[option]()
+ })
+ }
+
+ $.fn.typeahead.defaults = {
+ source: []
+ , items: 8
+ , menu: '<ul class="typeahead dropdown-menu"></ul>'
+ , item: '<li><a href="#"></a></li>'
+ , minLength: 1
+ }
+
+ $.fn.typeahead.Constructor = Typeahead
+
+
+ /* TYPEAHEAD DATA-API
+ * ================== */
+
+ $(document).on('focus.typeahead.data-api', '[data-provide="typeahead"]', function (e) {
+ var $this = $(this)
+ if ($this.data('typeahead')) return
+ e.preventDefault()
+ $this.typeahead($this.data())
+ })
+
+}(window.jQuery);
View
36 phantomweb/static/js/phantom_domain.js
@@ -8,8 +8,10 @@ var g_decision_engines_by_type = {'sensor': 'Sensor', 'multicloud': 'Multi Cloud
var g_current_details_request = null;
var g_current_details_timer = null;
var g_selected_instance = null;
+var g_available_sensors = [];
var DEFAULT_DECISION_ENGINE = 'Multi Cloud';
var DETAILS_TIMER_MS = 5000;
+var SENSOR_HINT_ITEMS = 25;
$(document).ready(function() {
@@ -17,7 +19,19 @@ $(document).ready(function() {
$("#phantom_domain_main_combined_pane_inner").hide();
- $("#phantom_domain_sensors_input").tagsManager();
+ var $sensor_input = $("#phantom_domain_sensors_input").tagsManager({
+ typeahead: true,
+ typeaheadDelegate: {
+ source: function() {
+ return g_available_sensors;
+ },
+ minLength: 0,
+ items: SENSOR_HINT_ITEMS
+ }
+ });
+
+ //enable showing hints on click
+ $sensor_input.on('focus', $sensor_input.typeahead.bind($sensor_input, 'lookup'));
$("input[name=hidden-tags]").change(function() {
phantom_update_sensors();
@@ -118,6 +132,7 @@ $(document).ready(function() {
return false;
});
+ get_available_sensors();
});
@@ -137,6 +152,25 @@ function phantom_domain_buttons(enabled) {
}
}
+function get_available_sensors(query, callback) {
+ var success = function(response) {
+ try {
+ g_available_sensors = JSON.parse(response);
+ }
+ catch(e) {
+ console.log("problem parsing: " + e);
+ }
+ }
+
+ var failure = function(response) {
+ console.log("Failure getting sensors");
+ }
+
+ var url = make_url('api/sensors/load')
+ ajaxCallREST(url, success, failure);
+}
+
+
function phantom_domain_details_buttons(enabled) {
if (enabled) {
View
1  phantomweb/templates/base.html
@@ -16,6 +16,7 @@
<script src="/static/js/jquery-1.9.0.js"></script>
<script src="/static/js/jquery-ui-1.10.0.custom.js"></script>
<script src="/static/js/bootstrap.js"></script>
+ <script src="/static/js/bootstrap-hacks.js"></script>
{% block headscripts %}{% endblock %}
View
3  phantomweb/templates/phantom_domain.html
@@ -11,7 +11,6 @@
// -->
</script>
- <script src="/static/js/bootstrap.js"></script>
<script src="/static/js/bootstrap-tagmanager.js"></script>
<script src="/static/js/phantom_common.js"></script>
<script src="/static/js/phantom_domain.js"></script>
@@ -71,7 +70,7 @@ <h5 class="phantom_area_header magic-underline">Configuration for <span id="phan
<div class="control-group" >
<label class="control-label" for="phantom_domain_sensors_input">Sensors to Monitor:</label>
<div class="controls">
- <input name="tags" id="phantom_domain_sensors_input" placeholder="Add Sensor" class="span5"></input>
+ <input name="tags" id="phantom_domain_sensors_input" type="text" placeholder="Add Sensor" class="span5"></input>
</div>
</div>
View
2  phantomweb/urls.py
@@ -34,6 +34,8 @@
url(r'^phantom/api/launchconfig/save$', 'phantomweb.views.django_lc_save'),
url(r'^phantom/api/launchconfig/delete$', 'phantomweb.views.django_lc_delete'),
+ url(r'^phantom/api/sensors/load$', 'phantomweb.views.django_sensors_load'),
+
url(r'^phantom/domain$', 'phantomweb.views.django_domain_html'),
url(r'^phantom/api/domain/load$', 'phantomweb.views.django_domain_load'),
url(r'^phantom/api/domain/start$', 'phantomweb.views.django_domain_start'),
View
12 phantomweb/views.py
@@ -7,7 +7,7 @@
from django.contrib.auth.models import User
from phantomweb.phantom_web_exceptions import PhantomWebException, PhantomRedirectException
from phantomweb.util import PhantomWebDecorator, get_user_object, LogEntryDecorator
-from phantomweb.workload import terminate_iaas_instance, phantom_lc_load, phantom_sites_add, phantom_sites_delete, phantom_sites_load, phantom_lc_delete, phantom_lc_save, phantom_domain_load, phantom_domain_terminate, phantom_domain_resize, phantom_domain_start, phantom_domain_details, phantom_instance_terminate
+from phantomweb.workload import terminate_iaas_instance, phantom_lc_load, phantom_sites_add, phantom_sites_delete, phantom_sites_load, phantom_lc_delete, phantom_lc_save, phantom_domain_load, phantom_domain_terminate, phantom_domain_resize, phantom_domain_start, phantom_domain_details, phantom_instance_terminate, phantom_sensors_load
from django.contrib import admin
@@ -37,6 +37,16 @@ def django_domain_html(request):
return HttpResponseRedirect(ex.redir)
return HttpResponse(t.render(c))
+@LogEntryDecorator
+@login_required
+def django_sensors_load(request):
+ user_obj = get_user_object(request.user.username)
+ try:
+ response_dict = phantom_sensors_load(request.GET, user_obj)
+ h = HttpResponse(simplejson.dumps(response_dict), mimetype='application/javascript')
+ finally:
+ user_obj.close()
+ return h
@LogEntryDecorator
@login_required
View
12 phantomweb/workload.py
@@ -1,8 +1,9 @@
-import hashlib
import boto
from boto.ec2.autoscale import Tag
from boto.exception import EC2ResponseError
from boto.regioninfo import RegionInfo
+
+import json
import logging
import urlparse
import boto.ec2.autoscale
@@ -11,6 +12,7 @@
from phantomweb.util import PhantomWebDecorator, LogEntryDecorator
from phantomsql import phantom_get_default_key_name
+
import logging # import the required logging module
g_general_log = logging.getLogger('phantomweb.general')
@@ -25,6 +27,8 @@
PHANTOM_REGION = 'phantom'
+OPENTSDB_METRICS = ["df.1kblocks.free","df.1kblocks.total","df.1kblocks.used","df.inodes.free","df.inodes.total","df.inodes.used","iostat.part.ios_in_progress","iostat.part.msec_read","iostat.part.msec_total","iostat.part.msec_weighted_total","iostat.part.msec_write","iostat.part.read_merged","iostat.part.read_requests","iostat.part.read_sectors","iostat.part.write_merged","iostat.part.write_requests","iostat.part.write_sectors","net.sockstat.ipfragqueues","net.sockstat.memory","net.sockstat.num_orphans","net.sockstat.num_sockets","net.sockstat.num_timewait","net.sockstat.sockets_inuse","net.stat.tcp.abort","net.stat.tcp.abort.failed","net.stat.tcp.congestion.recovery","net.stat.tcp.delayedack","net.stat.tcp.failed_accept","net.stat.tcp.memory.pressure","net.stat.tcp.memory.prune","net.stat.tcp.packetloss.recovery","net.stat.tcp.reording","net.stat.tcp.syncookies","proc.kernel.entropy_avail","proc.loadavg.15min","proc.loadavg.1min","proc.loadavg.5min","proc.loadavg.runnable","proc.loadavg.total_threads","proc.meminfo.active","proc.meminfo.anonpages","proc.meminfo.bounce","proc.meminfo.buffers","proc.meminfo.cached","proc.meminfo.commitlimit","proc.meminfo.committed_as","proc.meminfo.dirty","proc.meminfo.highfree","proc.meminfo.hightotal","proc.meminfo.inactive","proc.meminfo.lowfree","proc.meminfo.lowtotal","proc.meminfo.mapped","proc.meminfo.memfree","proc.meminfo.memtotal","proc.meminfo.nfs_unstable","proc.meminfo.pagetables","proc.meminfo.slab","tcollector.collector.lines_invalid","tcollector.collector.lines_received","tcollector.collector.lines_sent","tcollector.reader.lines_collected","tcollector.reader.lines_dropped"]
+
#
# we are only dealing with launch configurations that were made with the web app
#
@@ -203,7 +207,6 @@ def sensor_tags_from_de_params(phantom_con, domain_name, de_params):
return tags
-
@LogEntryDecorator
def _start_domain(phantom_con, domain_name, lc_name, de_name, de_params, host_list_str, a_cloudname):
@@ -780,6 +783,11 @@ def phantom_instance_terminate(request_params, userobj):
@PhantomWebDecorator
@LogEntryDecorator
+def phantom_sensors_load(request_params, userobj):
+ return json.dumps(OPENTSDB_METRICS)
+
+@PhantomWebDecorator
+@LogEntryDecorator
def phantom_domain_details(request_params, userobj):
params = ['name',]
for p in params:
Something went wrong with that request. Please try again.