Repository: brooklyn-ui Updated Branches: refs/heads/master [created] 18b073a95
http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/18b073a9/src/main/webapp/assets/js/view/entity-sensors.js ---------------------------------------------------------------------- diff --git a/src/main/webapp/assets/js/view/entity-sensors.js b/src/main/webapp/assets/js/view/entity-sensors.js new file mode 100644 index 0000000..282c622 --- /dev/null +++ b/src/main/webapp/assets/js/view/entity-sensors.js @@ -0,0 +1,539 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. +*/ +/** + * Render entity sensors tab. + * + * @type {*} + */ +define([ + "underscore", "jquery", "backbone", "brooklyn-utils", "zeroclipboard", "view/viewutils", + "model/sensor-summary", "text!tpl/apps/sensors.html", "text!tpl/apps/sensor-name.html", + "jquery-datatables", "datatables-extensions", "underscore", "jquery", "backbone", "uri", +], function (_, $, Backbone, Util, ZeroClipboard, ViewUtils, SensorSummary, SensorsHtml, SensorNameHtml) { + + // TODO consider extracting all such usages to a shared ZeroClipboard wrapper? + ZeroClipboard.config({ moviePath: '//cdnjs.cloudflare.com/ajax/libs/zeroclipboard/1.3.1/ZeroClipboard.swf' }); + + var sensorHtml = _.template(SensorsHtml), + sensorNameHtml = _.template(SensorNameHtml); + + var EntitySensorsView = Backbone.View.extend({ + template: sensorHtml, + sensorMetadata:{}, + refreshActive:true, + zeroClipboard: null, + + events:{ + /* mouseup might technically be preferred, as moving out then releasing wouldn't + * normally be expected to trigger the action; however this introduces complexity + * as mouseup seems possibly to fire even if a widget has mouseoutted; + * also i note many other places (including backbone examples) seem to use click + * perhaps for this very reason. worth exploring, but as a low priority. */ + 'click .refresh': 'updateSensorsNow', + 'click .filterEmpty':'toggleFilterEmpty', + 'click .toggleAutoRefresh':'toggleAutoRefresh', + 'click #sensors-table div.secret-info':'toggleSecrecyVisibility', + + 'mouseup .valueOpen':'valueOpen', + 'mouseover #sensors-table tbody tr':'noteFloatMenuActive', + 'mouseout #sensors-table tbody tr':'noteFloatMenuSeemsInactive', + 'mouseover .floatGroup':'noteFloatMenuActive', + 'mouseout .floatGroup':'noteFloatMenuSeemsInactive', + 'mouseover .clipboard-item':'noteFloatMenuActiveCI', + 'mouseout .clipboard-item':'noteFloatMenuSeemsInactiveCI', + 'mouseover .hasFloatLeft':'showFloatLeft', + 'mouseover .hasFloatDown':'enterFloatDown', + 'mouseout .hasFloatDown':'exitFloatDown', + 'mouseup .light-popup-menu-item':'closeFloatMenuNow' + + // these have no effect: you must register on the zeroclipboard object, below + // (presumably the same for the .clipboard-item event listeners above, but unconfirmed) +// 'mouseup .clipboard-item':'closeFloatMenuNow', +// 'mouseup .global-zeroclipboard-container object':'closeFloatMenuNow', + }, + + initialize:function () { + _.bindAll(this); + this.$el.html(this.template()); + + var $table = this.$('#sensors-table'), + that = this; + this.table = ViewUtils.myDataTable($table, { + "fnRowCallback": function( nRow, aData, iDisplayIndex, iDisplayIndexFull ) { + $(nRow).attr('id', aData[0]); + $('td',nRow).each(function(i,v){ + if (i==1) $(v).attr('class','sensor-value'); + }); + return nRow; + }, + "aoColumnDefs": [ + { // name (with tooltip) + "mRender": function ( data, type, row ) { + // name (column 1) should have tooltip title + var actions = that.getSensorActions(data.name); + // if data.description or .type is absent we get an error in html rendering (js) + // unless we set it explicitly (there is probably a nicer way to do this however?) + var context = _.extend(data, { + description: data['description'], type: data['type']}); + return sensorNameHtml(context); + }, + "aTargets": [ 1 ] + }, + { // value + "mRender": function ( data, type, row ) { + var escapedValue = Util.toDisplayString(data); + if (type!='display') + return escapedValue; + + var hasEscapedValue = (escapedValue!=null && (""+escapedValue).length > 0); + sensorName = row[0], + actions = that.getSensorActions(sensorName); + + // NB: the row might not yet exist + var $row = $('tr[id="'+sensorName+'"]'); + + // datatables doesn't seem to expose any way to modify the html in place for a cell, + // so we rebuild + + var result = "<span class='value'>"+(hasEscapedValue ? escapedValue : '')+"</span>"; + + var isSecret = Util.isSecret(sensorName); + if (isSecret) { + result += "<span class='secret-indicator'>(hidden)</span>"; + } + + if (actions.open) + result = "<a href='"+actions.open+"'>" + result + "</a>"; + if (escapedValue==null || escapedValue.length < 3) + // include whitespace so we can click on it, if it's really small + result += " "; + + var existing = $row.find('.dynamic-contents'); + // for the json url, use the full url (relative to window.location.href) + var jsonUrl = actions.json ? new URI(actions.json).resolve(new URI(window.location.href)).toString() : null; + // prefer to update in place, so menus don't disappear, also more efficient + // (but if menu is changed, we do recreate it) + if (existing.length>0) { + if (that.checkFloatMenuUpToDate($row, actions.open, '.actions-open', 'open-target') && + that.checkFloatMenuUpToDate($row, escapedValue, '.actions-copy') && + that.checkFloatMenuUpToDate($row, actions.json, '.actions-json-open', 'open-target') && + that.checkFloatMenuUpToDate($row, jsonUrl, '.actions-json-copy', 'copy-value')) { +// log("updating in place "+sensorName) + existing.html(result); + return $row.find('td.sensor-value').html(); + } + } + + // build the menu - either because it is the first time, or the actions are stale +// log("creating "+sensorName); + + var downMenu = ""; + if (actions.open) + downMenu += "<div class='light-popup-menu-item valueOpen actions-open' open-target='"+actions.open+"'>" + + "Open</div>"; + if (hasEscapedValue) downMenu += + "<div class='light-popup-menu-item handy valueCopy actions-copy clipboard-item'>Copy Value</div>"; + if (actions.json) downMenu += + "<div class='light-popup-menu-item handy valueOpen actions-json-open' open-target='"+actions.json+"'>" + + "Open REST Link</div>"; + if (actions.json && hasEscapedValue) downMenu += + "<div class='light-popup-menu-item handy valueCopy actions-json-copy clipboard-item' copy-value='"+ + jsonUrl+"'>Copy REST Link</div>"; + if (downMenu=="") { +// log("no actions for "+sensorName); + downMenu += + "<div class='light-popup-menu-item'>(no actions)</div>"; + } + downMenu = "<div class='floatDown'><div class='light-popup'><div class='light-popup-body'>" + + downMenu + + "</div></div></div>"; + result = "<span class='hasFloatLeft dynamic-contents'>" + result + + "</span>" + + "<div class='floatLeft'><span class='icon-chevron-down hasFloatDown'></span>" + + downMenu + + "</div>"; + result = "<div class='floatGroup"+ + (isSecret ? " secret-info" : "")+ + "'>" + result + "</div>"; + // also see updateFloatMenus which wires up the JS for these classes + + return result; + }, + "aTargets": [ 2 ] + }, + // ID in column 0 is standard (assumed in ViewUtils) + { "bVisible": false, "aTargets": [ 0 ] } + ] + }); + + this.zeroClipboard = new ZeroClipboard(); + this.zeroClipboard.on( "dataRequested" , function(client) { + try { + // the zeroClipboard instance is a singleton so check our scope first + if (!$(this).closest("#sensors-table").length) return; + var text = $(this).attr('copy-value'); + if (!text) text = $(this).closest('.floatGroup').find('.value').text(); + +// log("Copying sensors text '"+text+"' to clipboard"); + client.setText(text); + + // show the word "copied" for feedback; + // NB this occurs on mousedown, due to how flash plugin works + // (same style of feedback and interaction as github) + // the other "clicks" are now triggered by *mouseup* + var $widget = $(this); + var oldHtml = $widget.html(); + $widget.html('<b>Copied!</b>'); + // use a timeout to restore because mouseouts can leave corner cases (see history) + setTimeout(function() { $widget.html(oldHtml); }, 600); + } catch (e) { + log("Zeroclipboard failure; falling back to prompt mechanism"); + log(e); + Util.promptCopyToClipboard(text); + } + }); + // these seem to arrive delayed sometimes, so we also work with the clipboard-item class events + this.zeroClipboard.on( "mouseover", function() { that.noteFloatMenuZeroClipboardItem(true, this); } ); + this.zeroClipboard.on( "mouseout", function() { that.noteFloatMenuZeroClipboardItem(false, this); } ); + this.zeroClipboard.on( "mouseup", function() { that.closeFloatMenuNow(); } ); + + ViewUtils.addFilterEmptyButton(this.table); + ViewUtils.addAutoRefreshButton(this.table); + ViewUtils.addRefreshButton(this.table); + this.loadSensorMetadata(); + this.updateSensorsPeriodically(); + this.toggleFilterEmpty(); + return this; + }, + + beforeClose: function () { + if (this.zeroClipboard) { + this.zeroClipboard.destroy(); + } + }, + + /* getting the float menu to pop-up and go away with all the right highlighting + * is ridiculous. this is pretty good, but still not perfect. it seems some events + * just don't fire, others occur out of order, and the root cause is that when + * the SWF object (which has to accept the click, for copy to work) gets focus, + * its ancestors *lose* focus - we have to suppress the event which makes the + * float group disappear. i have left logging in, commented out, for more debugging. + * there are notes that ZeroClipboard 2.0 will support hover properly. + * + * see git commit history and review comments in https://github.com/brooklyncentral/brooklyn/pull/1171 + * for more information. + */ + floatMenuActive: false, + lastFloatMenuRowId: null, + lastFloatFocusInTextForEventUnmangling: null, + updateFloatMenus: function() { + $('#sensors-table *[rel="tooltip"]').tooltip(); + this.zeroClipboard.clip( $('.valueCopy') ); + }, + showFloatLeft: function(event) { + this.noteFloatMenuFocusChange(true, event, "show-left"); + this.showFloatLeftOf($(event.currentTarget)); + }, + showFloatLeftOf: function($hasFloatLeft) { + $hasFloatLeft.next('.floatLeft').show(); + }, + enterFloatDown: function(event) { + this.noteFloatMenuFocusChange(true, event, "show-down"); +// log("entering float down"); + var fdTarget = $(event.currentTarget); +// log( fdTarget ); + this.floatDownFocus = fdTarget; + var that = this; + setTimeout(function() { + that.showFloatDownOf( fdTarget ); + }, 200); + }, + exitFloatDown: function(event) { +// log("exiting float down"); + this.floatDownFocus = null; + }, + showFloatDownOf: function($hasFloatDown) { + if ($hasFloatDown != this.floatDownFocus) { +// log("float down did not hover long enough"); + return; + } + var down = $hasFloatDown.next('.floatDown'); + down.show(); + $('.light-popup', down).show(2000); + }, + noteFloatMenuActive: function(focus) { + this.noteFloatMenuFocusChange(true, focus, "menu"); + + // remove dangling zc events (these don't always get removed, apparent bug in zc event framework) + // this causes it to flash sometimes but that's better than leaving the old item highlighted + if (focus.toElement && $(focus.toElement).hasClass('clipboard-item')) { + // don't remove it + } else { + var zc = $(focus.target).closest('.floatGroup').find('div.zeroclipboard-is-hover'); + zc.removeClass('zeroclipboard-is-hover'); + } + }, + noteFloatMenuSeemsInactive: function(focus) { this.noteFloatMenuFocusChange(false, focus, "menu"); }, + noteFloatMenuActiveCI: function(focus) { this.noteFloatMenuFocusChange(true, focus, "menu-clip-item"); }, + noteFloatMenuSeemsInactiveCI: function(focus) { this.noteFloatMenuFocusChange(false, focus, "menu-clip-item"); }, + noteFloatMenuZeroClipboardItem: function(seemsActive,focus) { + this.noteFloatMenuFocusChange(seemsActive, focus, "clipboard"); + if (seemsActive) { + // make the table row highlighted (as the default hover event is lost) + // we remove it when the float group goes away + $(focus).closest('tr').addClass('zeroclipboard-is-hover'); + } else { + // sometimes does not get removed by framework - though this doesn't seem to help + // as you can see by logging this before and after: +// log(""+$(focus).attr('class')) + // the problem is that the framework seems sometime to trigger this event before adding the class + // see in noteFloatMenuActive where we do a different check + $(focus).removeClass('zeroclipboard-is-hover'); + } + }, + noteFloatMenuFocusChange: function(seemsActive, focus, caller) { +// log(""+new Date().getTime()+" note active "+caller+" "+seemsActive); + var delayCheckFloat = true; + var focusRowId = null; + var focusElement = null; + if (focus) { + focusElement = focus.target ? focus.target : focus; + if (seemsActive) { + this.lastFloatFocusInTextForEventUnmangling = $(focusElement).text(); + focusRowId = focus.target ? $(focus.target).closest('tr').attr('id') : $(focus).closest('tr').attr('id'); + if (this.floatMenuActive && focusRowId==this.lastFloatMenuRowId) { + // lastFloatMenuRowId has not changed, when moving within a floatgroup + // (but we still get mouseout events when the submenu changes) +// log("redundant mousein from "+ focusRowId ); + return; + } + } else { + // on mouseout, skip events which are bogus + // first, if the toElement is in the same floatGroup + focusRowId = focus.toElement ? $(focus.toElement).closest('tr').attr('id') : null; + if (focusRowId==this.lastFloatMenuRowId) { + // lastFloatMenuRowId has not changed, when moving within a floatgroup + // (but we still get mouseout events when the submenu changes) +// log("skipping, internal mouseout from "+ focusRowId ); + return; + } + // check (a) it is the 'out' event corresponding to the most recent 'in' + // (because there is a race where it can say in1, in2, out1 rather than in1, out2, in2 + if ($(focusElement).text() != this.lastFloatFocusInTextForEventUnmangling) { +// log("skipping, not most recent mouseout from "+ focusRowId ); + return; + } + if (focus.toElement) { + if ($(focus.toElement).hasClass('global-zeroclipboard-container')) { +// log("skipping out, as we are moving to clipboard container"); + return; + } + if (focus.toElement.name && focus.toElement.name=="global-zeroclipboard-flash-bridge") { +// log("skipping out, as we are moving to clipboard movie"); + return; + } + } + } + } +// log( "moving to "+focusRowId ); + if (seemsActive && focusRowId) { +// log("setting lastFloat when "+this.floatMenuActive + ", from "+this.lastFloatMenuRowId ); + if (this.lastFloatMenuRowId != focusRowId) { + if (this.lastFloatMenuRowId) { + // the floating menu has changed, hide the old +// log("hiding old menu on float-focus change"); + this.closeFloatMenuNow(); + } + } + // now show the new, if possible (might happen multiple times, but no matter + if (focusElement) { +// log("ensuring row "+focusRowId+" is showing on change"); + this.showFloatLeftOf($(focusElement).closest('tr').find('.hasFloatLeft')); + this.lastFloatMenuRowId = focusRowId; + } else { + this.lastFloatMenuRowId = null; + } + } + this.floatMenuActive = seemsActive; + if (!seemsActive) { + this.scheduleCheckFloatMenuNeedsHiding(delayCheckFloat); + } + }, + scheduleCheckFloatMenuNeedsHiding: function(delayCheckFloat) { + if (delayCheckFloat) { + this.checkTime = new Date().getTime()+299; + setTimeout(this.checkFloatMenuNeedsHiding, 300); + } else { + this.checkTime = new Date().getTime()-1; + this.checkFloatMenuNeedsHiding(); + } + }, + closeFloatMenuNow: function() { +// log("closing float menu due do direct call (eg click)"); + this.checkTime = new Date().getTime()-1; + this.floatMenuActive = false; + this.checkFloatMenuNeedsHiding(); + }, + checkFloatMenuNeedsHiding: function() { +// log(""+new Date().getTime()+" checking float menu - "+this.floatMenuActive); + if (new Date().getTime() <= this.checkTime) { +// log("aborting check as another one scheduled"); + return; + } + + // we use a flag to determine whether to hide the float menu + // because the embedded zero-clipboard flash objects cause floatGroup + // to get a mouseout event when the "Copy" menu item is hovered + if (!this.floatMenuActive) { +// log("HIDING FLOAT MENU") + $('.floatLeft').hide(); + $('.floatDown').hide(); + $('.zeroclipboard-is-hover').removeClass('zeroclipboard-is-hover'); + lastFloatMenuRowId = null; + } else { +// log("we're still in") + } + }, + valueOpen: function(event) { + window.open($(event.target).attr('open-target'),'_blank'); + }, + + render: function() { + return this; + }, + checkFloatMenuUpToDate: function($row, actionValue, actionSelector, actionAttribute) { + if (typeof actionValue === 'undefined' || actionValue==null || actionValue=="") { + if ($row.find(actionSelector).length==0) return true; + } else { + if (actionAttribute) { + if ($row.find(actionSelector).attr(actionAttribute)==actionValue) return true; + } else { + if ($row.find(actionSelector).length>0) return true; + } + } + return false; + }, + + /** + * Returns the actions loaded to view.sensorMetadata[name].actions + * for the given name, or an empty object. + */ + getSensorActions: function(sensorName) { + var allMetadata = this.sensorMetadata || {}; + var metadata = allMetadata[sensorName] || {}; + return metadata.actions || {}; + }, + + toggleFilterEmpty: function() { + ViewUtils.toggleFilterEmpty(this.$('#sensors-table'), 2); + return this; + }, + + toggleAutoRefresh: function() { + ViewUtils.toggleAutoRefresh(this); + return this; + }, + + enableAutoRefresh: function(isEnabled) { + this.refreshActive = isEnabled; + return this; + }, + + toggleSecrecyVisibility: function(event) { + $(event.target).closest('.secret-info').toggleClass('secret-revealed'); + }, + + /** + * Loads current values for all sensors on an entity and updates sensors table. + */ + isRefreshActive: function() { return this.refreshActive; }, + updateSensorsNow:function () { + var that = this; + ViewUtils.get(that, that.model.getSensorUpdateUrl(), that.updateWithData, + { enablement: that.isRefreshActive }); + }, + updateSensorsPeriodically:function () { + var that = this; + ViewUtils.getRepeatedlyWithDelay(that, that.model.getSensorUpdateUrl(), function(data) { that.updateWithData(data); }, + { enablement: that.isRefreshActive }); + }, + updateWithData: function (data) { + var that = this; + $table = that.$('#sensors-table'); + var options = {}; + if (that.fullRedraw) { + options.refreshAllRows = true; + that.fullRedraw = false; + } + ViewUtils.updateMyDataTable($table, data, function(value, name) { + var metadata = that.sensorMetadata[name]; + if (metadata==null) { + // kick off reload metadata when this happens (new sensor for which no metadata known) + // but only if we haven't loaded metadata for a while + metadata = { 'name':name }; + that.sensorMetadata[name] = metadata; + that.loadSensorMetadataIfStale(name, 10000); + }; + return [name, metadata, value]; + }, options); + + that.updateFloatMenus(); + }, + + /** + * Loads all information about an entity's sensors. Populates view.sensorMetadata object + * with a map of sensor names to description, actions and type (e.g. java.lang.Long). + */ + loadSensorMetadata: function() { + var url = this.model.getLinkByName('sensors'), + that = this; + that.lastSensorMetadataLoadTime = new Date().getTime(); + $.get(url, function (data) { + _.each(data, function(sensor) { + var actions = {}; + _.each(sensor.links, function(v, k) { + if (k.slice(0, 7) == "action:") { + actions[k.slice(7)] = v; + } + }); + that.sensorMetadata[sensor.name] = { + name: sensor.name, + description: sensor.description, + actions: actions, + type: sensor.type + }; + }); + that.fullRedraw = true; + that.updateSensorsNow(); + that.table.find('*[rel="tooltip"]').tooltip(); + }); + return this; + }, + + loadSensorMetadataIfStale: function(sensorName, recency) { + var that = this; + if (!that.lastSensorMetadataLoadTime || that.lastSensorMetadataLoadTime + recency < new Date().getTime()) { +// log("reloading metadata because new sensor "+sensorName+" identified") + that.loadSensorMetadata(); + } + } + }); + + return EntitySensorsView; +});
