job-list.js

334 lines | 8.378 kB Blame History Raw Download
/*
 * Copyright 2012 LinkedIn Corp.
 * 
 * Licensed 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.
 */

/*
 * Job list sidebar view to accompany SVG graph.
 */

azkaban.JobListView = Backbone.View.extend({
	events: {
		"keyup input": "filterJobs",
		"click li.listElement": "handleJobClick",
		"click #resetPanZoomBtn": "handleResetPanZoom",
		"click #autoPanZoomBtn": "handleAutoPanZoom",
		"contextmenu li.listElement": "handleContextMenuClick",
		"click .expandarrow": "handleToggleMenuExpand"
	},
	
	initialize: function(settings) {
		this.model.bind('change:selected', this.handleSelectionChange, this);
		this.model.bind('change:disabled', this.handleDisabledChange, this);
		this.model.bind('change:graph', this.render, this);
		this.model.bind('change:update', this.handleStatusUpdate, this);
		
		this.filterInput = $(this.el).find("#filter");
		this.list = $(this.el).find("#joblist");
		this.contextMenu = settings.contextMenuCallback;
		this.listNodes = {};
	},
	
	filterJobs: function(self) {
		var filter = this.filterInput.val();
		// Clear all filters first
		if (!filter || filter.trim() == "") {
			this.unfilterAll(self);
			return;
		}
		
		this.hideAll(self);
		var showList = {};
		
		// find the jobs that need to be exposed.
		for (var key in this.listNodes) {
			var li = this.listNodes[key];
			var node = li.node;
			var nodeName = node.id;
			node.listElement = li;

			var index = nodeName.indexOf(filter);
			if (index == -1) {
				continue;
			}

			var spanlabel = $(li).find("> a > span");
			
			var endIndex = index + filter.length;
			var newHTML = nodeName.substring(0, index) + "<span class=\"filterHighlight\">" + 
					nodeName.substring(index, endIndex) + "</span>" + 
					nodeName.substring(endIndex, nodeName.length);
			$(spanlabel).html(newHTML);
			
			// Apply classes to all the included embedded flows.
			var pIndex = key.length;
			while ((pIndex = key.lastIndexOf(":", pIndex - 1)) > 0) {
				var parentId = key.substr(0, pIndex);
				var parentLi = this.listNodes[parentId];
				$(parentLi).show();
				$(parentLi).addClass("subFilter");
			}
			
			$(li).show();
		}
	},
	
	hideAll: function(self) {
		for (var key in this.listNodes) {
			var li = this.listNodes[key];
			var label = $(li).find("> a > span");
			$(label).text(li.node.id);
			$(li).removeClass("subFilter");
			$(li).hide();
		}
	},
	
	unfilterAll: function(self) {
		for (var key in this.listNodes) {
			var li = this.listNodes[key];
			var label = $(li).find("> a > span");
			$(label).text(li.node.id);
			$(li).removeClass("subFilter");
			$(li).show();
		}
	},
	
	handleStatusUpdate: function(evt) {
		var data = this.model.get("data");
		this.changeStatuses(data);
	},
	
	changeStatuses: function(data) {
		for (var i = 0; i < data.nodes.length; ++i) {
			var node = data.nodes[i];

			// Confused? In updates, a node reference is given to the update node.
			var liElement = node.listElement;
			var child = $(liElement).children("a");
			if (!$(child).hasClass(node.status)) {
				$(child).removeClass(statusList.join(' '));
				$(child).addClass(node.status);
				$(child).attr("title", node.status + " (" + node.type + ")");
			}
			if (node.nodes) {
				this.changeStatuses(node);
			}
		}
	},
	
	render: function(self) {
		var data = this.model.get("data");
		var nodes = data.nodes;
		
		this.renderTree(this.list, data);
		
		//this.assignInitialStatus(self);
		this.handleDisabledChange(self);
		this.changeStatuses(data);
	},
	
	renderTree: function(el, data, prefix) {
		var nodes = data.nodes;
		if (nodes.length == 0) {
			console.log("No results");
			return;
		};
		if (!prefix) {
			prefix = "";
		}
	
		var nodeArray = nodes.slice(0);
		nodeArray.sort(function(a, b) {
			var diff = a.y - b.y;
			if (diff == 0) {
				return a.x - b.x;
			}
			else {
				return diff;
			}
		});
		
		var ul = document.createElement('ul');
		$(ul).addClass("tree-list");
		for (var i = 0; i < nodeArray.length; ++i) {
			var li = document.createElement("li");
			$(li).addClass("listElement");
			$(li).addClass("tree-list-item");
			
			// This is used for the filter step.
			var listNodeName = prefix + nodeArray[i].id;
			this.listNodes[listNodeName]=li;
			li.node = nodeArray[i];
			li.node.listElement = li;

			var a = document.createElement("a");
			var iconDiv = document.createElement('div');
			$(iconDiv).addClass('icon');
			
			$(a).append(iconDiv);
			
			var span = document.createElement("span");
			$(span).text(nodeArray[i].id);
			$(span).addClass("jobname");
			$(a).append(span);
			$(li).append(a);
			$(ul).append(li);
			
			if (nodeArray[i].type == "flow") {
				// Add the up down
				var expandDiv = document.createElement("div");
				$(expandDiv).addClass("expandarrow glyphicon glyphicon-chevron-down");
				$(a).append(expandDiv);
				
				// Create subtree
				var subul = this.renderTree(li, nodeArray[i], listNodeName + ":");
				$(subul).hide();
			}
		}
		
		$(el).append(ul);
		return ul;
	},
	
	handleMenuExpand: function(li) {
		var expandArrow = $(li).find("> a > .expandarrow");
		var submenu = $(li).find("> ul");
		
		$(expandArrow).removeClass("glyphicon-chevron-down");
		$(expandArrow).addClass("glyphicon-chevron-up");
		$(submenu).slideDown();
	},
	
	handleMenuCollapse: function(li) {
		var expandArrow = $(li).find("> a > .expandarrow");
		var submenu = $(li).find("> ul");
		
		$(expandArrow).removeClass("glyphicon-chevron-up");
		$(expandArrow).addClass("glyphicon-chevron-down");
		$(submenu).slideUp();
	},
	
	handleToggleMenuExpand: function(evt) {
		var expandarrow = evt.currentTarget;
		var li = $(evt.currentTarget).closest("li.listElement");
		var submenu = $(li).find("> ul");

		if ($(submenu).is(":visible")) {
			this.handleMenuCollapse(li);
		}
		else {
			this.handleMenuExpand(li);
		}
		
		evt.stopImmediatePropagation();
	},
	
	handleContextMenuClick: function(evt) {
		if (this.contextMenu) {
			this.contextMenu(evt, this.model, evt.currentTarget.node);
			return false;
		}
	},
	
	handleJobClick: function(evt) {
		console.log("Job clicked");
		var li = $(evt.currentTarget).closest("li.listElement");
		var node = li[0].node;
		if (!node) {
			return;
		}
		
		if (this.model.has("selected")) {
			var selected = this.model.get("selected");
			if (selected == node) {
				this.model.unset("selected");
			}
			else {
				this.model.set({"selected": node});
			}
		}
		else {
			this.model.set({"selected": node});
		}
		
		evt.stopPropagation();
		evt.cancelBubble = true;
	},
	
	handleDisabledChange: function(evt) {
		this.changeDisabled(this.model.get('data'));
	},
	
	changeDisabled: function(data) {
		for (var i =0; i < data.nodes; ++i) {
			var node = data.nodes[i];
			if (node.disabled = true) {
				removeClass(node.listElement, "nodedisabled");
				if (node.type=='flow') {
					this.changeDisabled(node);
				}
			}
			else {
				addClass(node.listElement, "nodedisabled");
			}
		}
	},
	
	handleSelectionChange: function(evt) {
		if (!this.model.hasChanged("selected")) {
			return;
		}
		
		var previous = this.model.previous("selected");
		var current = this.model.get("selected");

		if (previous) {
			$(previous.listElement).removeClass("active");
		}
		
		if (current) {
			$(current.listElement).addClass("active");
			this.propagateExpansion(current.listElement);
		}
	},
	
	propagateExpansion: function(li) {
		var li = $(li).parent().closest("li.listElement")[0];
		if (li) {
			this.propagateExpansion(li);
			this.handleMenuExpand(li);
		}
	},
	
	handleResetPanZoom: function(evt) {
		this.model.trigger("resetPanZoom");
	},
	
	handleAutoPanZoom: function(evt) {
		var target = evt.currentTarget;
		if ($(target).hasClass('btn-default')) {
			$(target).removeClass('btn-default');
			$(target).addClass('btn-info');
		}
		else if ($(target).hasClass('btn-info')) {
			$(target).removeClass('btn-info');
			$(target).addClass('btn-default');
		}
	 
		// Using $().hasClass('active') does not use here because it appears that
		// this is called before the Bootstrap toggle completes.
		this.model.set({"autoPanZoom": $(target).hasClass('btn-info')});
	}
});