blob: b0ef98993cbc98976b9424233db757a5f92403bb [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 config tab.
*
* @type {*}
*/
define([
"underscore", "jquery", "backbone", "brooklyn-utils", "zeroclipboard", "view/viewutils",
"model/config-summary", "text!tpl/apps/config.html", "text!tpl/apps/config-name.html",
"jquery-datatables", "datatables-extensions"
], function (_, $, Backbone, Util, ZeroClipboard, ViewUtils, ConfigSummary, ConfigHtml, ConfigNameHtml) {
// TODO consider extracting all such usages to a shared ZeroClipboard wrapper?
ZeroClipboard.config({ moviePath: './assets/img/zeroclipboard/ZeroClipboard.swf' });
var configHtml = _.template(ConfigHtml),
configNameHtml = _.template(ConfigNameHtml);
// TODO refactor to share code w entity-sensors.js
// in meantime, see notes there!
var EntityConfigView = Backbone.View.extend({
template: configHtml,
configMetadata:{},
refreshActive:true,
zeroClipboard: null,
events:{
'click .refresh':'updateConfigNow',
'click .filterEmpty':'toggleFilterEmpty',
'click .toggleAutoRefresh':'toggleAutoRefresh',
'click #config-table div.secret-info':'toggleSecrecyVisibility',
'mouseup .valueOpen':'valueOpen',
'mouseover #config-table tbody tr':'noteFloatMenuActive',
'mouseout #config-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',
},
initialize:function () {
_.bindAll(this);
this.$el.html(this.template());
var that = this,
$table = this.$('#config-table');
that.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','config-value');
});
return nRow;
},
"aoColumnDefs": [
{ // name (with tooltip)
"mRender": function ( data, type, row ) {
// name (column 1) should have tooltip title
var actions = that.getConfigActions(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 configNameHtml(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);
configName = row[0],
actions = that.getConfigActions(configName);
// NB: the row might not yet exist
var $row = $('tr[id="'+_.escape(configName)+'"]');
// 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(configName);
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 "+configName)
existing.html(result);
return $row.find('td.config-value').html();
}
}
// build the menu - either because it is the first time, or the actions are stale
// log("creating "+configName);
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 "+configName);
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("#config-table").length) return;
var text = $(this).attr('copy-value');
if (!text) text = $(this).closest('.floatGroup').find('.value').text();
// log("Copying config 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.loadConfigMetadata();
this.updateConfigPeriodically();
this.toggleFilterEmpty();
return this;
},
beforeClose: function () {
if (this.zeroClipboard) {
this.zeroClipboard.destroy();
}
},
floatMenuActive: false,
lastFloatMenuRowId: null,
lastFloatFocusInTextForEventUnmangling: null,
updateFloatMenus: function() {
$('#config-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.configMetadata[name].actions
* for the given name, or an empty object.
*/
getConfigActions: function(configName) {
var allMetadata = this.configMetadata || {};
var metadata = allMetadata[configName] || {};
return metadata.actions || {};
},
toggleFilterEmpty: function() {
ViewUtils.toggleFilterEmpty(this.$('#config-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 config on an entity and updates config table.
*/
isRefreshActive: function() { return this.refreshActive; },
updateConfigNow:function () {
var that = this;
ViewUtils.get(that, that.model.getConfigUpdateUrl(), that.updateWithData,
{ enablement: that.isRefreshActive });
},
updateConfigPeriodically:function () {
var that = this;
ViewUtils.getRepeatedlyWithDelay(that, that.model.getConfigUpdateUrl(), function(data) { that.updateWithData(data); },
{ enablement: that.isRefreshActive });
},
updateWithData: function (data) {
var that = this;
$table = that.$('#config-table');
var options = {};
if (that.fullRedraw) {
options.refreshAllRows = true;
that.fullRedraw = false;
}
ViewUtils.updateMyDataTable($table, data, function(value, name) {
var metadata = that.configMetadata[name];
if (metadata==null) {
// kick off reload metadata when this happens (new config for which no metadata known)
// but only if we haven't loaded metadata for a while
metadata = { 'name':name };
that.configMetadata[name] = metadata;
that.loadConfigMetadataIfStale(name, 10000);
}
return [name, metadata, value];
}, options);
that.updateFloatMenus();
},
loadConfigMetadata: function() {
var url = this.model.getLinkByName('config'),
that = this;
that.lastConfigMetadataLoadTime = new Date().getTime();
$.get(url, function (data) {
_.each(data, function(config) {
var actions = {};
_.each(config.links, function(v, k) {
if (k.slice(0, 7) == "action:") {
actions[k.slice(7)] = v;
}
});
that.configMetadata[config.name] = {
name: config.name,
description: config.description,
actions: actions,
type: config.type
};
});
that.fullRedraw = true;
that.updateConfigNow();
that.table.find('*[rel="tooltip"]').tooltip();
});
return this;
},
loadConfigMetadataIfStale: function(configName, recency) {
var that = this;
if (!that.lastConfigMetadataLoadTime || that.lastConfigMetadataLoadTime + recency < new Date().getTime()) {
// log("reloading metadata because new config "+configName+" identified")
that.loadConfigMetadata();
}
}
});
return EntityConfigView;
});