| /* |
| * 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 += " "; |
| |
| 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; |
| }); |