| /* |
| * 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. |
| */ |
| define([ |
| "underscore", "jquery", "brooklyn" |
| ], function (_, $, BrooklynConfig) { |
| |
| var ViewUtils = { |
| myDataTable:function($table, extra) { |
| $.fn.dataTableExt.sErrMode = 'throw'; |
| var settings = { |
| "bDestroy": true, |
| "iDisplayLength": 25, |
| "bDeferRender": true, |
| "sPaginationType": "full_numbers", |
| "sDom": "fp<'brook-db-top-toolbar'>tilp<'brook-db-bot-toolbar'>", |
| "oLanguage": { |
| "sSearch": "", |
| "sInfo": "Showing _START_ - _END_ of _TOTAL_ ", |
| "sInfoEmpty": "<i>No data</i> ", |
| "sEmptyTable": "<i>No matching records available</i>", |
| "sZeroRecords": "<i>No matching records found</i>", |
| "oPaginate": { |
| "sFirst": "<<", |
| "sPrevious": "<", |
| "sNext": ">", |
| "sLast": ">>" |
| }, |
| "sInfoFiltered": "(of _MAX_)", |
| "sLengthMenu": '( <select>' + |
| '<option value="10">10</option>' + |
| '<option value="25">25</option>' + |
| '<option value="50">50</option>' + |
| '<option value="-1">all</option>' + |
| '</select> / page )' |
| } |
| }; |
| _.extend(settings, extra); |
| |
| ViewUtils.fadeToIndicateInitialLoad($table); |
| |
| return $table.dataTable(settings); |
| }, |
| myDataTableToolbarAddHtml: function($table,html) { |
| $('.brook-db-bot-toolbar', $table.parent().parent()).append(html) |
| $('.brook-db-top-toolbar', $table.parent().parent()).append(html) |
| }, |
| addRefreshButton: function($table) { |
| this.myDataTableToolbarAddHtml($table, |
| '<i class="refresh table-toolbar-icon bootstrap-glyph icon-refresh handy smallpadside" rel="tooltip" title="Reload content immediately"></i>'); |
| }, |
| addFilterEmptyButton: function($table) { |
| this.myDataTableToolbarAddHtml($table, |
| '<i class="filterEmpty table-toolbar-icon bootstrap-glyph icon-eye-open handy bottom smallpadside" rel="tooltip" title="Show/hide empty records"></i>'); |
| }, |
| addAutoRefreshButton: function($table) { |
| this.myDataTableToolbarAddHtml($table, |
| '<i class="toggleAutoRefresh table-toolbar-icon bootstrap-glyph icon-pause handy smallpadside" rel="tooltip" title="Toggle auto-refresh"></i>'); |
| }, |
| /* fnConvertData takes the entries in collection (value, optionalKeyOrIndex) and returns a list |
| * whose first element is the ID (hidden first column of table) |
| * and other elements are the other columns in the table; |
| * alternatively it can return null if the entry should be excluded |
| * |
| * option refreshAllRows can be passed to force all rows to be re-rendered; |
| * useful if rendering data may have changed even if value has not |
| */ |
| updateMyDataTable: function(table, collection, fnConvertData, options) { |
| if (table==null) return; |
| if (options==null) options = {} |
| var oldDisplayDataList = [] |
| try { |
| oldDisplayDataList = table.dataTable().fnGetData(); |
| } catch (e) { |
| // (used to) sometimes get error accessing column 1 of row 0, though table seems empty |
| // caused by previous attempt to refresh from a closed view |
| log("WARNING: could not fetch data; clearing") |
| log(e) |
| log(e.stack) |
| table.dataTable().fnClearTable() |
| } |
| var oldDisplayIndexMap = {} |
| var oldDisplayData = {} |
| for (var idx in oldDisplayDataList) { |
| var data = oldDisplayDataList[idx] |
| oldDisplayIndexMap[data[0]] = idx |
| oldDisplayData[data[0]] = data |
| } |
| var newDisplayData = {} |
| var updateDisplayData = [] |
| ViewUtils.each(collection, function(data,index) { |
| var newRow = fnConvertData(data, index) |
| if (newRow!=null) { |
| var id = newRow[0] |
| |
| var displayIndex = oldDisplayIndexMap[id]; |
| if (displayIndex!=null) { |
| updateDisplayData[displayIndex] = newRow |
| delete oldDisplayIndexMap[id] |
| } else { |
| newDisplayData[id] = newRow |
| } |
| } |
| }) |
| // first update (so indices don't change) |
| for (var prop in updateDisplayData) { |
| var rowProps = updateDisplayData[prop] |
| var oldProps = oldDisplayData[rowProps[0]] |
| for (idx in rowProps) { |
| var v = rowProps[idx] |
| if (options['refreshAllRows'] || !_.isEqual(v,oldProps[idx])) { |
| // update individual columns as values change |
| try { |
| table.fnUpdate( v, Number(prop), idx, false, false ) |
| } catch (e) { |
| // often occurs if we haven't properly closed view, e.g. on entity switch |
| log("WARNING: cannot update row") |
| log(e) |
| log(e.stack) |
| log(v) |
| log(prop) |
| log(idx) |
| } |
| } else { |
| // log("NO CHANGE") |
| } |
| } |
| } |
| // then delete old ones |
| for (var prop in oldDisplayIndexMap) { |
| var index = oldDisplayIndexMap[prop] |
| table.fnDeleteRow( Number(index), null, false ) |
| } |
| // and now add new ones |
| for (var prop in newDisplayData) { |
| try { |
| table.fnAddData( newDisplayData[prop], false ) |
| } catch (e) { |
| // errors sometimes if we load async |
| log("WARNING: cannot add to row") |
| log(e) |
| log(e.stack) |
| log(prop) |
| log(newDisplayData[prop]) |
| } |
| } |
| try { |
| // redraw, but keeping pagination |
| table.fnStandingRedraw(); |
| } catch (e) { |
| log("WARNING: could not redraw") |
| log(e) |
| log(e.stack) |
| } |
| ViewUtils.cancelFadeOnceLoaded(table) |
| }, |
| toggleFilterEmpty: function($table, column) { |
| var hideEmpties = $('.filterEmpty', $table.parent().parent()).toggleClass('icon-eye-open icon-eye-close').hasClass('icon-eye-close'); |
| if (hideEmpties) { |
| $table.dataTable().fnFilter('.+', column, true); |
| } else { |
| $table.dataTable().fnFilter('.*', column, true); |
| } |
| }, |
| toggleAutoRefresh: function(pane) { |
| var isEnabled = $('.toggleAutoRefresh', pane.$el).toggleClass('icon-pause icon-play').hasClass('icon-pause'); |
| pane.enableAutoRefresh(isEnabled); |
| }, |
| attachToggler: function($scope) { |
| var $togglers; |
| if ($scope === undefined) { |
| $togglers = $(".toggler-header"); |
| } else { |
| $togglers = $(".toggler-header", $scope); |
| } |
| $togglers.click(this.onTogglerClick); |
| }, |
| onTogglerClick: function(event) { |
| ViewUtils.onTogglerClickElement($(event.currentTarget).closest(".toggler-header")); |
| }, |
| onTogglerClickElement: function(root) { |
| root.toggleClass("user-hidden"); |
| $(".toggler-icon", root).toggleClass("icon-chevron-left").toggleClass("icon-chevron-down"); |
| var next = root.next(); |
| if (root.hasClass("user-hidden")) { |
| next.slideUp('fast'); |
| } else { |
| next.slideDown('fast'); |
| } |
| }, |
| showTogglerClickElement: function(root) { |
| root.removeClass("user-hidden"); |
| $(".toggler-icon", root).removeClass("icon-chevron-left").addClass("icon-chevron-down"); |
| root.next().slideDown('fast'); |
| }, |
| updateTextareaWithData: function($div, data, showIfEmpty, doSlideDown, minPx, maxPx) { |
| var $ta = $("textarea", $div); |
| var show = showIfEmpty; |
| if (data !== undefined) { |
| $ta.val(data); |
| show = true; |
| } else { |
| $ta.val(""); |
| } |
| if (show) { |
| this.setHeightAutomatically($ta, minPx, maxPx, false); |
| if (doSlideDown) { $div.slideDown(100); } |
| } else { |
| $div.hide(); |
| } |
| }, |
| setHeightAutomatically: function($ta, minPx, maxPx, deferred) { |
| var height = $ta.prop("scrollHeight"), that = this; |
| if ($ta.css("padding-top")) height -= parseInt($ta.css("padding-top"), 10) |
| if ($ta.css("padding-bottom")) height -= parseInt($ta.css("padding-bottom"), 10) |
| // log("scroll height "+height+" - old real height "+$ta.css("height")) |
| if (height==0 && !deferred) { |
| _.defer(function() { that.setHeightAutomatically($ta, minPx, maxPx, true) }) |
| } else { |
| height = Math.min(height, maxPx); |
| height = Math.max(height, minPx); |
| $ta.css("height", height); |
| } |
| return height; |
| }, |
| each: function(collection, fn) { |
| if (_.isFunction(collection.each)) { |
| // some objects (such as backbone collections) are not iterable |
| // (either by "for x in" or "_.each") so call the "each" method explicitly on them |
| return collection.each(fn) |
| } else { |
| // try underscore |
| return _.each(collection, fn); |
| } |
| }, |
| // makes tooltips appear as white-on-black-bubbles rather than boring black-on-yellow-boxes |
| // but NB if the html is updated the tooltip can remain visible until page refresh |
| processTooltips: function($el) { |
| $el.find('*[rel="tooltip"]').tooltip(); |
| }, |
| fadeToIndicateInitialLoad: function($el) { |
| // in case the server response time is low, fade out while it refreshes |
| // (since we can't show updated details until we've retrieved app + entity details) |
| try { |
| $el.fadeTo(1000, 0.3) |
| // .queue( |
| // function() { |
| // // does nothing yet -- see comment in brooklyn.css on .view_not_available |
| // $el.append('<div class="view_not_available"></div>') |
| // }); |
| // above works to insert the div, though we don't have styling on it |
| // but curiously it also causes the parent to go to opacity 0 !?! |
| } catch (e) { |
| // ignore - normal during tests |
| } |
| }, |
| cancelFadeOnceLoaded: function($el) { |
| try { |
| // $el.children('.view_not_available').remove(); |
| $el.stop(true, false).fadeTo(200, 1); |
| } catch (e) { |
| // ignore - normal during tests |
| } |
| }, |
| |
| |
| |
| // TODO the get and fetch methods below should possibly be on a BrooklynView prototype |
| // see also notes in router.js |
| // (perhaps as part of that introduce a callWithFixedDelay method which does the tracking, |
| // so we can cleanly unregister, and perhaps an onServerFailure method, and with that we |
| // could perhaps get rid of, or at least dramatically simplify, the get/fetch) |
| |
| /* variant of $.get with automatic failure handling and recovery; |
| * options should be omitted except by getRepeatedlyWithDelay */ |
| get: function(view, url, success, options) { |
| if (view.viewIsClosed) return ; |
| |
| if (!options) options = {} |
| if (!options.count) options.count = 1 |
| else options.count++; |
| // log("getting, count "+options.count+", delay "+period+": "+url) |
| |
| var disabled = (options['enablement'] && !options['enablement']()) |
| || !BrooklynConfig.view.refresh |
| if (options.count > 1 && disabled) { |
| // not enabled, just requeue |
| if (options['period']) |
| setTimeout(function() { ViewUtils.get(view, url, success, options)}, options['period']) |
| return; |
| } |
| |
| /* inspects the status object returned from an ajax call in a view; |
| * if not valid, it fades the view and increases backoff delays and resubmits; |
| * if it is valid, it returns true so the caller can continue |
| * (restoring things such as the view, timer, etc, if they were disabled); |
| * |
| * takes some of the options as per fetchRepeatedlyWithDelay |
| * (though they are less well tested here) |
| * |
| * note that the status text object is rarely useful; normally the fail(handler) is invoked, |
| * as above (#get) |
| */ |
| var checkAjaxStatusObject = function(status, view, options) { |
| if (view.viewIsClosed) return false; |
| if (status == "success" || status == "notmodified") { |
| // unfade and restore |
| if (view._loadingProblem) { |
| log("getting view data is back to normal - "+url) |
| log(view) |
| view._loadingProblem = false; |
| |
| var fadeTarget = view.$el; |
| if ("fadeTarget" in options) { |
| fadeTarget = options["fadeTarget"] |
| } |
| if (fadeTarget) ViewUtils.cancelFadeOnceLoaded(fadeTarget) |
| |
| if (options['originalPeriod']) |
| options.period = options['originalPeriod']; |
| } |
| |
| return true; |
| } |
| if (status == "error" || status == "timeout" || status == "parsererror") { |
| // fade and log problem |
| if (!view._loadingProblem) { |
| log("error getting view data from "+url+" - is the server reachable?") |
| view._loadingProblem = true; |
| } |
| // fade the view, on error |
| var fadeTarget = view.$el; |
| if ("fadeTarget" in options) { |
| fadeTarget = options["fadeTarget"] |
| } |
| if (fadeTarget) ViewUtils.fadeToIndicateInitialLoad(fadeTarget) |
| |
| if (options['period']) { |
| if (!options['originalPeriod']) options.originalPeriod = options['period']; |
| var period = options['period']; |
| |
| // attempt exponential backoff up to every 15m |
| period *= 2; |
| var max = (options['backoffMaxPeriod'] || 15*60*1000); |
| if (period > max) period = max; |
| options.period = period |
| setTimeout(function() { ViewUtils.get(view, url, success, options)}, period) |
| } |
| |
| return false; |
| } |
| return true; |
| } |
| |
| return $.get(url, function(data, status) { |
| if (!checkAjaxStatusObject(status, view, options)) { |
| return; |
| } |
| if (success) success(data); |
| if (options['period']) |
| setTimeout(function() { ViewUtils.get(view, url, success, options)}, options['period']) |
| }).fail(function() { |
| checkAjaxStatusObject("error", view, options) |
| }) |
| }, |
| /** invokes a get against the given url repeatedly, with fading and backoff on failures, |
| * cf fetchRepeatedlyWithDelay, but here the user's callback function is invoked on success |
| */ |
| getRepeatedlyWithDelay: function(view, url, success, options) { |
| if (!options) options = {} |
| if (!options['period']) options.period = 3000 |
| ViewUtils.get(view, url, success, options) |
| }, |
| |
| /** As fetchRepeatedlyWithDelay(view, model, options), but without updating a view. */ |
| fetchModelRepeatedlyWithDelay: function(model, options) { |
| this.fetchRepeatedlyWithDelay(undefined, model, options); |
| }, |
| |
| /* invokes fetch on the model, associated with the view. |
| * automatically closes when view closes, |
| * and fades display and exponentially-backs off on problems. |
| * options include: |
| * |
| * enablement (function returning t/f whether the invocation is enabled) |
| * period (millis, currently 3000 = 3s default); |
| * originalPeriod (millis, becomes the period if successful; primarily for internal use); |
| * backoffMaxPeriod (millis, max time to wait between retries, currently 15*60*1000 = 10m default); |
| * |
| * doitnow (if true, kicks off a run immediately, else only after the timer) |
| * |
| * fadeTarget (jquery element to fade; defaults to view.$el; null can be set to prevent fade); |
| * |
| * fetchOptions (additional options to pass to fetch; however success and error should not be present); |
| * success (function to invoke on success, before re-queueing); |
| * error (optional function to invoke on error, before requeueing); |
| */ |
| fetchRepeatedlyWithDelay: function(view, model, options) { |
| if (view && view.viewIsClosed) return; |
| |
| if (!options) options = {} |
| if (!options.count) options.count = 1 |
| else options.count++; |
| |
| var period = options['period'] || 3000 |
| var originalPeriod = options['originalPeriod'] || period |
| // log("fetching, count "+options.count+", delay "+period+": "+model.url) |
| |
| var fetcher = function() { |
| if (view && view.viewIsClosed) return; |
| var disabled = (options['enablement'] && !options['enablement']()) |
| || !BrooklynConfig.view.refresh |
| if (options.count > 1 && disabled) { |
| // not enabled, just requeue |
| ViewUtils.fetchRepeatedlyWithDelay(view, model, options); |
| return; |
| } |
| var fetchOptions = options['fetchOptions'] ? _.clone(options['fetchOptions']) : {} |
| fetchOptions.success = function(modelR,response,optionsR) { |
| var fn = options['success'] |
| if (fn) fn(modelR,response,optionsR); |
| if (view && view._loadingProblem) { |
| log("fetching view data is back to normal - "+model.url) |
| view._loadingProblem = false; |
| |
| var fadeTarget = view.$el; |
| if ("fadeTarget" in options) { |
| fadeTarget = options["fadeTarget"] |
| } |
| if (fadeTarget) ViewUtils.cancelFadeOnceLoaded(fadeTarget) |
| } |
| options.period = originalPeriod; |
| ViewUtils.fetchRepeatedlyWithDelay(view, model, options); |
| } |
| fetchOptions.error = function(modelR,response,optionsR) { |
| var fn = options['error'] |
| if (fn) fn(modelR,response,optionsR); |
| if (view && !view._loadingProblem) { |
| log("error fetching view data from "+model.url+" - is the server reachable?") |
| log(response) |
| view._loadingProblem = true; |
| } |
| // fade the view, on error |
| if (view) { |
| var fadeTarget = view.$el; |
| if ("fadeTarget" in options) { |
| fadeTarget = options["fadeTarget"] |
| } |
| if (fadeTarget) ViewUtils.fadeToIndicateInitialLoad(fadeTarget) |
| } |
| // attempt exponential backoff up to every 15m |
| period *= 2; |
| var max = (options['backoffMaxPeriod'] || 15*60*1000); |
| if (period > max) period = max; |
| options = _.clone(options) |
| options.originalPeriod = originalPeriod; |
| options.period = period; |
| ViewUtils.fetchRepeatedlyWithDelay(view, model, options); |
| }; |
| model.fetch(fetchOptions) |
| }; |
| if (options['doitnow']) { |
| options.doitnow = false; |
| fetcher(); |
| } else { |
| setTimeout(fetcher, period); |
| } |
| }, |
| /** @deprecated since 0.7.0 use computeStatusIconInfo */ |
| computeStatusIcon: function(serviceUp, lifecycleState) { |
| return this.computeStatusIconInfo(serviceUp, lifecycleState).url; |
| }, |
| /** returns object with properties: |
| * String word; |
| * String url; |
| * boolean problem; |
| */ |
| computeStatusIconInfo: function(serviceUp, lifecycleState) { |
| var result = {}; |
| |
| if (lifecycleState != null) |
| lifecycleState = lifecycleState.toLowerCase(); |
| |
| if (serviceUp===false || serviceUp=="false") serviceUp=false; |
| else if (serviceUp===true || serviceUp=="true") serviceUp=true; |
| else { |
| if (serviceUp!=null && serviceUp !== "" && serviceUp !== undefined && serviceUp.toLowerCase().indexOf("loading")<0) { |
| log("Unknown 'serviceUp' value:") |
| log(serviceUp) |
| } |
| serviceUp = null; |
| } |
| var mode = null; |
| var imgext = "png"; |
| var problem = false; |
| |
| if (lifecycleState=="running") { |
| if (serviceUp==false) { |
| mode = "running-onfire"; |
| problem = true; |
| } else { |
| mode = "running"; |
| } |
| } else if (lifecycleState=="stopped" || lifecycleState=="created") { |
| if (serviceUp==true) { |
| mode = "stopped-onfire"; |
| problem = true; |
| } else { |
| mode = "stopped"; |
| } |
| } else if (lifecycleState=="starting") { |
| mode = "starting"; |
| imgext = "gif"; //animated |
| } else if (lifecycleState=="stopping") { |
| mode = "stopping"; |
| imgext = "gif"; //animated |
| } else if (lifecycleState=="on_fire" || /* just in case */ lifecycleState=="on-fire" || lifecycleState=="onfire") { |
| mode = "onfire"; |
| problem = true; |
| } |
| |
| if (mode==null) { |
| // no lifecycle state, rely on serviceUp |
| if (lifecycleState!=null && lifecycleState !== "" && lifecycleState !== undefined && lifecycleState.toLowerCase().indexOf("loading")<0) { |
| log("Unknown 'lifecycleState' value:") |
| log(lifecycleState) |
| } |
| if (serviceUp) mode = "running"; |
| else if (serviceUp===false) mode = "stopped"; |
| } |
| |
| result.word = mode; |
| result.problem = problem; |
| if (mode==null) { |
| // no status info at all |
| result.url = null; |
| } else { |
| result.url = "./assets/img/"+"icon-status-"+mode+"."+imgext; |
| } |
| |
| return result; |
| } |
| }; |
| return ViewUtils; |
| }); |