blob: 61a8ff16b7e62051f927e81a4f0cb5e92d1af669 [file] [log] [blame]
/*
* 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: './assets/img/zeroclipboard/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="'+_.escape(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='"+encodeURI(actions.open)+"'>" + result + "</a>";
if (escapedValue==null || escapedValue.length < 3)
// include whitespace so we can click on it, if it's really small
result += "&nbsp;&nbsp;&nbsp;&nbsp;";
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 ],
mRender: function(data) { return _.escape(data); } }
]
});
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;
});