blob: 779a09e60dc94e3f503761842401ab783ee44cab [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.
*/
/**
* Sub-View to render the Application tree.
* @type {*}
*/
define([
"underscore", "jquery", "backbone", "view/viewutils",
"model/app-tree", "text!tpl/apps/tree-item.html", "text!tpl/apps/tree-empty.html"
], function (_, $, Backbone, ViewUtils,
AppTree, TreeItemHtml, EmptyTreeHtml) {
var emptyTreeTemplate = _.template(EmptyTreeHtml);
var treeItemTemplate = _.template(TreeItemHtml);
var findAllTreeboxes = function(id, $scope) {
return $('.tree-box[data-entity-id="' + _.escape(id) + '"]', $scope);
};
var findRootTreebox = function(id, $scope) {
return $('.lozenge-app-tree-wrapper', $scope).children('.tree-box[data-entity-id="' + _.escape(id) + '"]', this.$el);
};
var findChildTreebox = function(id, $parentTreebox) {
return $parentTreebox.children('.node-children').children('.tree-box[data-entity-id="' + _.escape(id) + '"]');
};
var findMasterTreebox = function(id, $scope) {
return $('.tree-box[data-entity-id="' + _.escape(id) + '"]:not(.indirect)', $scope);
};
var sortKeyOfIdName = function(id, name) {
return (name ? name.toLowerCase() : "~~~") + " " + id.toLowerCase();
};
var sortKeyOfEntity = function(entity) {
return sortKeyOfIdName(entity.id, entity.get('name'));
};
var insertSorted = function($treebox, $domParent) {
var placed = false;
var contender = $(".toggler-group", $domParent).first();
var sortKey = $treebox.data('sort-key');
while (contender.length && !placed) {
var contenderKey = contender.data("sort-key");
if (sortKey < contenderKey) {
contender.before($treebox);
placed = true;
} else {
contender = contender.next(".toggler-group", $domParent);
}
}
if (!placed) {
$domParent.append($treebox);
}
return $treebox;
};
var createEntityTreebox = function(id, name, $domParent, depth, indirect) {
// Tildes in sort key force entities with no name to bottom of list (z < ~).
var sortKey = sortKeyOfIdName(id, name);
// Create the wrapper.
var $treebox = $(
'<div data-entity-id="'+_.escape(id)+'" data-sort-key="'+_.escape(sortKey)+'" data-depth="'+_.escape(depth)+'" ' +
'class="tree-box toggler-group' +
(indirect ? " indirect" : "") +
(depth == 0 ? " outer" : " inner " + (depth % 2 ? " depth-odd" : " depth-even")+
(depth == 1 ? " depth-first" : "")) + '">'+
'<div class="entity_tree_node_wrapper"></div>'+
'<div class="node-children toggler-target hide"></div>'+
'</div>');
// Insert into the passed DOM parent, maintaining sort order relative to siblings: name then id.
return insertSorted($treebox, $domParent);
}
var getOrCreateApplicationTreebox = function(id, name, treeView, $el) {
if (!($el)) $el = treeView.$el;
var $treebox = findRootTreebox(id, $el);
if (!$treebox.length) {
var $insertionPoint = $('.lozenge-app-tree-wrapper', $el);
if (!$insertionPoint.length) {
// entire view must be created
treeView.$el.html(
'<div class="navbar_main_wrapper treeloz">'+
'<div id="tree-list" class="navbar_main treeloz">'+
'<div class="lozenge-app-tree-wrapper">'+
'</div></div></div>');
$insertionPoint = $('.lozenge-app-tree-wrapper', $el);
}
$treebox = createEntityTreebox(id, name, $insertionPoint, 0, false);
}
return $treebox;
};
var getOrCreateChildTreebox = function(id, name, isIndirect, $parentTreebox) {
var $treebox = findChildTreebox(id, $parentTreebox);
if (!$treebox.length) {
$treebox = createEntityTreebox(id, name, $parentTreebox.children('.node-children'), $parentTreebox.data("depth") + 1, isIndirect);
}
return $treebox;
};
var updateTreeboxContent = function(entity, $treebox, treeView) {
oldSortKey = $treebox.data('sort-key');
newSortKey = sortKeyOfEntity(entity);
var $newContent = $(treeView.template({
id: entity.get('id'),
parentId: entity.get('parentId'),
model: entity,
statusIconUrl: ViewUtils.computeStatusIconInfo(entity.get("serviceUp"), entity.get("serviceState")).url,
indirect: $treebox.hasClass('indirect'),
}));
var $wrapper = $treebox.children('.entity_tree_node_wrapper');
// Preserve old display status (just chevron direction at present).
if ($wrapper.find('.tree-node-state').hasClass('icon-chevron-down')) {
$newContent.find('.tree-node-state').removeClass('icon-chevron-right').addClass('icon-chevron-down');
}
$wrapper.html($newContent);
addEventsToNode($treebox, treeView);
if (newSortKey !== oldSortKey) {
// move this treebox to the right place in its parent
$treebox.data('sort-key', newSortKey);
$parent = $treebox.parent();
$treebox.detach();
insertSorted($treebox, $parent);
}
};
var addEventsToNode = function($node, treeView) {
// show the "light-popup" (expand / expand all / etc) menu
// if user hovers for 500ms. surprising there is no option for this (hover delay).
// also, annoyingly, clicks around the time the animation starts don't seem to get handled
// if the click is in an overlapping reason; this is why we position relative top: 12px in css
$('.light-popup', $node).parent().parent().hover(
function(parent) {
treeView.cancelHoverTimer();
treeView.hoverTimer = setTimeout(function() {
var menu = $(parent.currentTarget).find('.light-popup');
menu.show();
}, 500);
},
function(parent) {
treeView.cancelHoverTimer();
$('.light-popup').hide();
}
);
};
var ensureTreeboxVisible = function($treebox, treeView) {
if (!($treebox.length)) return false;
if ($treebox.is(':visible')) return true;
var $parentTreebox = $treebox.parent().closest('.tree-box');
if (!($parentTreebox.length)) return true; //means item is being rendered offscreen
if ($parentTreebox==$treebox) return false; //should never happen
if (!ensureTreeboxVisible($parentTreebox, treeView)) return false;
treeView.showChildrenOf($parentTreebox, false);
return true;
};
var selectTreebox = function(id, $treebox, treeView) {
treeView.requestedEntityId = id;
$('.entity_tree_node_wrapper', treeView.$el).removeClass('active');
$treebox.children('.entity_tree_node_wrapper').addClass('active');
var entity = treeView.collection.get(id);
if (entity) {
if (ensureTreeboxVisible($treebox, treeView)) {
treeView.selectedEntityId = id;
treeView.requestedEntityId = null;
treeView.trigger('entitySelected', entity);
return true;
} else {
// children DOM nodes not yet existing; probably the model is not populated,
// e.g. using browser forward/back, maybe also if invoked before selected;
// callers should populate then re-select
return false;
}
} else {
// probably needs a load
return false;
}
};
return Backbone.View.extend({
template: treeItemTemplate,
hoverTimer: null,
events: {
'click span.entity_tree_node .tree-change': 'treeChange',
'click span.entity_tree_node': 'nodeClicked'
},
initialize: function() {
this.collection.on('add', this.entityAdded, this);
this.collection.on('change', this.entityChanged, this);
this.collection.on('remove', this.entityRemoved, this);
this.collection.on('reset', this.renderFull, this);
_.bindAll(this);
},
beforeClose: function() {
this.collection.off("reset", this.renderFull);
},
entityAdded: function(entity) {
// Called when the full entity model is fetched into our collection, at which time we can replace
// the empty contents of any placeholder tree nodes (.tree-box) that were created earlier.
// The entity may have multiple 'treebox' views (in the case of group members).
var $treebox;
var parentId = entity.get('parentId');
if (!parentId) {
// If the new entity is an application, we must create its placeholder in the DOM.
$treebox = getOrCreateApplicationTreebox(entity.id, entity.get('name'), this);
// Select the new app if there's no current selection.
if (!this.selectedEntityId && !this.requestedEntityId) selectTreebox(entity.id, $treebox, this);
} else {
// else create in parent
var parent = this.collection.get(parentId);
if (!parent) {
return null;
}
$parentTreebox = this.entityAdded(parent);
if ($parentTreebox==null) {
return null;
}
$treebox = getOrCreateChildTreebox(entity.id, entity.name, $parentTreebox.hasClass("indirect"), $parentTreebox);
}
this.entityChanged(entity);
return $treebox;
},
entityChanged: function(entity) {
// The entity may have multiple 'treebox' views (in the case of group members).
var that = this;
findAllTreeboxes(entity.id).each(function() {
if ($(this).children('.node-children').is(':visible')) {
// children are being shown
if (that.collection.includeEntities(_.union(entity.get('children'), entity.get('members')))) {
that.collection.fetch();
}
}
updateTreeboxContent(entity, $(this), that);
});
},
entityRemoved: function(entity) {
// The entity may have multiple 'treebox' views (in the case of group members).
findAllTreeboxes(entity.id, this.$el).remove();
// Collection seems sometimes to retain children of the removed node;
// not sure why, but that's okay for now.
if (this.collection.getApplications().length == 0)
this.renderFull();
},
nodeClicked: function(event) {
var $treebox = $(event.currentTarget).closest('.tree-box');
var id = $treebox.data('entityId');
selectTreebox(id, $treebox, this);
return false;
},
selectEntity: function(id) {
var $treebox = findMasterTreebox(id, this.$el);
return selectTreebox(id, $treebox, this);
},
getTreebox: function(id) {
return findMasterTreebox(id, this.$el);
},
renderFull: function() {
var that = this;
// build up the new element in a hidden node
this.$el.empty();
// Display tree and highlight the selected entity.
if (this.collection.getApplications().length == 0) {
this.$el.append(emptyTreeTemplate());
} else {
_.each(this.collection.getApplications(), function(appId) {
var entity = that.collection.get(appId);
var $treebox = getOrCreateApplicationTreebox(entity.id, entity.name, that);
updateTreeboxContent(entity, $treebox, that);
});
// expand to show selected or requested id
var target = this.selectedEntityId;
if (!target) target = this.requestedEntityId;
if (target) {
// ensure this treebox is added and reselected
var item = this.collection.get(target);
if (item) {
this.initialRender = true;
this.entityAdded(item);
this.selectEntity(target);
this.initialRender = false;
}
}
}
return this;
},
cancelHoverTimer: function() {
if (this.hoverTimer != null) {
clearTimeout(this.hoverTimer);
this.hoverTimer = null;
}
},
treeChange: function(event) {
var $target = $(event.currentTarget);
var $treeBox = $target.closest('.tree-box');
if ($target.hasClass('tr-expand')) {
this.showChildrenOf($treeBox, false);
} else if ($target.hasClass('tr-expand-all')) {
this.showChildrenOf($treeBox, true);
} else if ($target.hasClass('tr-collapse')) {
this.hideChildrenOf($treeBox, false);
} else if ($target.hasClass('tr-collapse-all')) {
this.hideChildrenOf($treeBox, true);
} else {
// default - toggle
if ($treeBox.children('.node-children').is(':visible')) {
this.hideChildrenOf($treeBox, false);
} else {
this.showChildrenOf($treeBox, false);
}
}
// hide the popup menu
this.cancelHoverTimer();
$('.light-popup').hide();
// don't let other events interfere
return false;
},
showChildrenOf: function($treeBox, recurse, excludedEntityIds) {
excludedEntityIds = excludedEntityIds || [];
var idToExpand = $treeBox.data('entityId');
var $wrapper = $treeBox.children('.entity_tree_node_wrapper');
var $childContainer = $treeBox.children('.node-children');
var model = this.collection.get(idToExpand);
if (model == null) {
// not yet loaded; parallel thread should load
return;
}
var that = this;
var children = model.get('children'); // entity summaries: {id: ..., name: ...}
var renderChildrenAsIndirect = $treeBox.hasClass("indirect");
_.each(children, function(child) {
var $treebox = getOrCreateChildTreebox(child.id, child.name, renderChildrenAsIndirect, $treeBox);
var model = that.collection.get(child.id);
if (model) {
updateTreeboxContent(model, $treebox, that);
}
});
var members = model.get('members'); // entity summaries: {id: ..., name: ...}
_.each(members, function(member) {
var $treebox = getOrCreateChildTreebox(member.id, member.name, true, $treeBox);
var model = that.collection.get(member.id);
if (model) {
updateTreeboxContent(model, $treebox, that);
}
});
// Avoid infinite recursive expansion using a "taboo list" of indirect entities already expanded in this
// operation. Example: a group that contains itself or one of its own ancestors. Such cycles can only
// originate via "indirect" subordinates.
var expandIfNotExcluded = function($treebox, excludedEntityIds, defer) {
if ($treebox.hasClass('indirect')) {
var id = $treebox.data('entityId');
if (_.contains(excludedEntityIds, id))
return;
excludedEntityIds.push(id);
}
var doExpand = function() { that.showChildrenOf($treebox, recurse, excludedEntityIds); };
if (defer) _.defer(doExpand);
else doExpand();
};
if (this.collection.includeEntities(_.union(children, members))) {
// we have to load entities before we can proceed
this.collection.fetch({
success: function() {
if (recurse) {
$childContainer.children('.tree-box').each(function () {
expandIfNotExcluded($(this), excludedEntityIds, true);
});
}
}
});
}
$childContainer.slideDown(this.initialRender ? 0 : 300);
$wrapper.find('.tree-node-state').removeClass('icon-chevron-right').addClass('icon-chevron-down');
if (recurse) {
$childContainer.children('.tree-box').each(function () {
expandIfNotExcluded($(this), excludedEntityIds, false);
});
}
},
hideChildrenOf: function($treeBox, recurse) {
var $wrapper = $treeBox.children('.entity_tree_node_wrapper');
var $childContainer = $treeBox.children('.node-children');
if (recurse) {
var that = this;
$childContainer.children('.tree-box').each(function () {
that.hideChildrenOf($(this), recurse);
});
}
$childContainer.slideUp(300);
$wrapper.find('.tree-node-state').removeClass('icon-chevron-down').addClass('icon-chevron-right');
},
});
});