flow-execute-dialog.js

711 lines | 18.829 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.
 */

$.namespace('azkaban');

var flowExecuteDialogView;
azkaban.FlowExecuteDialogView = Backbone.View.extend({
	events: {
		"click .closeExecPanel": "hideExecutionOptionPanel",
		"click #schedule-btn": "scheduleClick",
		"click #execute-btn": "handleExecuteFlow"
	},
	
	initialize: function(settings) {
		this.model.bind('change:flowinfo', this.changeFlowInfo, this);
		$("#overrideSuccessEmails").click(function(evt) {
			if ($(this).is(':checked')) {
				$('#successEmails').attr('disabled', null);
			}
			else {
				$('#successEmails').attr('disabled', "disabled");
			}
		});
				
		$("#overrideFailureEmails").click(function(evt) {
			if ($(this).is(':checked')) {
				$('#failureEmails').attr('disabled', null);
			}
			else {
				$('#failureEmails').attr('disabled', "disabled");
			}
		});
	},
	
	render: function() {
	},
	
	getExecutionOptionData: function() {
		var failureAction = $('#failureAction').val();
		var failureEmails = $('#failureEmails').val();
		var successEmails = $('#successEmails').val();
		var notifyFailureFirst = $('#notifyFailureFirst').is(':checked');
		var notifyFailureLast = $('#notifyFailureLast').is(':checked');
		var failureEmailsOverride = $("#overrideFailureEmails").is(':checked');
		var successEmailsOverride = $("#overrideSuccessEmails").is(':checked');
		
		var flowOverride = {};
		var editRows = $(".editRow");
		for (var i = 0; i < editRows.length; ++i) {
			var row = editRows[i];
			var td = $(row).find('td');
			var key = $(td[0]).text();
			var val = $(td[1]).text();
			
			if (key && key.length > 0) {
				flowOverride[key] = val;
			}
		}
		
		var data = this.model.get("data");
		var disabledList = gatherDisabledNodes(data);
		
		var executingData = {
			projectId: projectId,
			project: this.projectName,
			ajax: "executeFlow",
			flow: this.flowId,
			disabled: JSON.stringify(disabledList),
			failureEmailsOverride:failureEmailsOverride,
			successEmailsOverride:successEmailsOverride,
			failureAction: failureAction,
			failureEmails: failureEmails,
			successEmails: successEmails,
			notifyFailureFirst: notifyFailureFirst,
			notifyFailureLast: notifyFailureLast,
			flowOverride: flowOverride
		};
		
		// Set concurrency option, default is skip

		var concurrentOption = $('input[name=concurrent]:checked').val();
		executingData.concurrentOption = concurrentOption;
		if (concurrentOption == "pipeline") {
			var pipelineLevel = $("#pipelineLevel").val();
			executingData.pipelineLevel = pipelineLevel;
		}
		else if (concurrentOption == "queue") {
			executingData.queueLevel = $("#queueLevel").val();
		}
		
		return executingData;
	},
	changeFlowInfo: function() {
		var successEmails = this.model.get("successEmails");
		var failureEmails = this.model.get("failureEmails");
		var failureActions = this.model.get("failureAction");
		var notifyFailure = this.model.get("notifyFailure");
		var flowParams = this.model.get("flowParams");
		var isRunning = this.model.get("isRunning");
		var concurrentOption = this.model.get("concurrentOption");
		var pipelineLevel = this.model.get("pipelineLevel");
		var pipelineExecutionId = this.model.get("pipelineExecution");
		var queueLevel = this.model.get("queueLevel");
		var nodeStatus = this.model.get("nodeStatus");
		var overrideSuccessEmails = this.model.get("overrideSuccessEmails");
		var overrideFailureEmails = this.model.get("overrideFailureEmails");
		
		if (overrideSuccessEmails) {
			$('#overrideSuccessEmails').attr('checked', true);
		}
		else {
			$('#successEmails').attr('disabled','disabled');
		}
		if (overrideFailureEmails) {
			$('#overrideFailureEmails').attr('checked', true);
		}
		else {
			$('#failureEmails').attr('disabled','disabled');
		}
		
		if (successEmails) {
			$('#successEmails').val(successEmails.join());
		}
		if (failureEmails) {
			$('#failureEmails').val(failureEmails.join());
		}
		if (failureActions) {
		$('#failureAction').val(failureActions);
		}
		
		if (notifyFailure.first) {
		$('#notifyFailureFirst').attr('checked', true);
		}
		if (notifyFailure.last) {
			$('#notifyFailureLast').attr('checked', true);
		}
		
		if (concurrentOption) {
			$('input[value='+concurrentOption+'][name="concurrent"]').attr('checked', true);
		}
		if (pipelineLevel) {
			$('#pipelineLevel').val(pipelineLevel);
		}
		if (queueLevel) {
			$('#queueLevel').val(queueLevel);
		}

		if (flowParams) {
			for (var key in flowParams) {
				editTableView.handleAddRow({
					paramkey: key, 
					paramvalue: flowParams[key]
				});
			}
		}
	},
	
	show: function(data) {
		var projectName = data.project;
		var flowId = data.flow;
		var jobId = data.job;
		
		// ExecId is optional
		var execId = data.execid;
		var exgraph = data.exgraph;
		
		this.projectName = projectName;
		this.flowId = flowId;
		
		var self = this;
		var loadCallback = function() {
			if (jobId) {
				self.showExecuteJob(projectName, flowId, jobId, data.withDep);
			}
			else {
				self.showExecuteFlow(projectName, flowId);
			}
		}
		
		var loadedId = executableGraphModel.get("flowId");
		this.loadGraph(projectName, flowId, exgraph, loadCallback);
		this.loadFlowInfo(projectName, flowId, execId);

	},
	
	showExecuteFlow: function(projectName, flowId) {
		$("#execute-flow-panel-title").text("Execute Flow " + flowId);
		this.showExecutionOptionPanel();

		// Triggers a render
		this.model.trigger("change:graph");
	},
	
	showExecuteJob: function(projectName, flowId, jobId, withDep) {
		sideMenuDialogView.menuSelect($("#flow-option"));
		$("#execute-flow-panel-title").text("Execute Flow " + flowId);
		
		var data = this.model.get("data");
		var disabled = this.model.get("disabled");
		
		// Disable all, then re-enable those you want.
		disableAll();
		
		var jobNode = data.nodeMap[jobId];
		touchNode(jobNode, false);
		
		if (withDep) {
			recurseAllAncestors(jobNode, false);
		}

		this.showExecutionOptionPanel();
		this.model.trigger("change:graph");
	},
	
	showExecutionOptionPanel: function() {
		sideMenuDialogView.menuSelect($("#flow-option"));
		$('#execute-flow-panel').modal();
	},
	
	hideExecutionOptionPanel: function() {
		$('#execute-flow-panel').modal("hide");
	},
	
	scheduleClick: function() {
		console.log("click schedule button.");
		this.hideExecutionOptionPanel();
		schedulePanelView.showSchedulePanel();
	},
	
	loadFlowInfo: function(projectName, flowId, execId) {
		console.log("Loading flow " + flowId);
		fetchFlowInfo(this.model, projectName, flowId, execId);
	},
	
	loadGraph: function(projectName, flowId, exgraph, callback) {
		console.log("Loading flow " + flowId);
		var requestURL = contextURL + "/manager";
		
		var graphModel = executableGraphModel;
		// fetchFlow(this.model, projectName, flowId, true);
		var requestData = {
				"project": projectName, 
				"ajax": "fetchflowgraph", 
				"flow": flowId
			};
		var self = this;
		var successHandler = function(data) {
			console.log("data fetched");
			graphModel.addFlow(data);
			
			if (exgraph) {
				self.assignInitialStatus(data, exgraph);
			}
			
			// Auto disable jobs that are finished.
			disableFinishedJobs(data);
			executingSvgGraphView = new azkaban.SvgGraphView({
				el: $('#flow-executing-graph'), 
				model: graphModel,
				render: false,
				rightClick: { 
					"node": expanelNodeClickCallback,
					"edge": expanelEdgeClickCallback, 
					"graph": expanelGraphClickCallback 
				},
				tooltipcontainer: "#svg-div-custom"
			});
			
			if (callback) {
				callback.call(this);
			}
		};
		$.get(requestURL, requestData, successHandler, "json");
	},
	assignInitialStatus: function(data, statusData) {
		// Copies statuses over from the previous execution if it exists.
		var statusNodeMap = statusData.nodeMap;
		var nodes = data.nodes;
		for(var i=0; i<nodes.length; ++i) {
			var node = nodes[i];
			var statusNode = statusNodeMap[node.id];
			if (statusNode) {
				node.status = statusNode.status;
				if (node.type == "flow" && statusNode.type == "flow") {
					this.assignInitialStatus(node, statusNode);
				}
			}
		}
	},
	handleExecuteFlow: function(evt) {
		console.log("click schedule button.");
		var executeURL = contextURL + "/executor";
		var executingData = this.getExecutionOptionData();
		executeFlow(executingData);
	}
});

var editTableView;
azkaban.EditTableView = Backbone.View.extend({
	events: {
		"click table .addRow": "handleAddRow",
		"click table .editable": "handleEditColumn",
		"click table .removeIcon": "handleRemoveColumn"
	},

	initialize: function(setting) {
	},
	
	handleAddRow: function(data) {
		var name = "";
		if (data.paramkey) {
			name = data.paramkey;
		}
		
		var value = "";
		if (data.paramvalue) {
			value = data.paramvalue;
		}
	
		var tr = document.createElement("tr");
		var tdName = document.createElement("td");
		$(tdName).addClass('property-key');
		var tdValue = document.createElement("td");
		
		var remove = document.createElement("div");
		$(remove).addClass("pull-right").addClass('remove-btn');
		var removeBtn = document.createElement("button");
		$(removeBtn).attr('type', 'button');
		$(removeBtn).addClass('btn').addClass('btn-xs').addClass('btn-danger');
		$(removeBtn).text('Delete');
		$(remove).append(removeBtn);

		var nameData = document.createElement("span");
		$(nameData).addClass("spanValue");
		$(nameData).text(name);
		var valueData = document.createElement("span");
		$(valueData).addClass("spanValue");
		$(valueData).text(value);
						
		$(tdName).append(nameData);
		$(tdName).addClass("editable");
		
		$(tdValue).append(valueData);
		$(tdValue).append(remove);
		$(tdValue).addClass("editable").addClass('value');
		
		$(tr).addClass("editRow");
		$(tr).append(tdName);
		$(tr).append(tdValue);
	 
		$(tr).insertBefore(".addRow");
		return tr;
	},
	
	handleEditColumn: function(evt) {
		var curTarget = evt.currentTarget;
	
		var text = $(curTarget).children(".spanValue").text();
		$(curTarget).empty();
					
		var input = document.createElement("input");
		$(input).attr("type", "text");
		$(input).addClass('form-control').addClass('input-sm');
		$(input).css("width", "100%");
		$(input).val(text);
		$(curTarget).addClass("editing");
		$(curTarget).append(input);
		$(input).focus();
		
		var obj = this;
		$(input).focusout(function(evt) {
			obj.closeEditingTarget(evt);
		});
		
		$(input).keypress(function(evt) {
			if (evt.which == 13) {
				obj.closeEditingTarget(evt);
			}
		});
	},
	
	handleRemoveColumn: function(evt) {
		var curTarget = evt.currentTarget;
		// Should be the table
		var row = curTarget.parentElement.parentElement;
		$(row).remove();
	},
	
	closeEditingTarget: function(evt) {
		var input = evt.currentTarget;
		var text = $(input).val();
		var parent = $(input).parent();
		$(parent).empty();

		var valueData = document.createElement("span");
		$(valueData).addClass("spanValue");
		$(valueData).text(text);

		if ($(parent).hasClass("value")) {
			var remove = document.createElement("div");
			$(remove).addClass("pull-right").addClass('remove-btn');
			var removeBtn = document.createElement("button");
			$(removeBtn).attr('type', 'button');
			$(removeBtn).addClass('btn').addClass('btn-xs').addClass('btn-danger');
			$(removeBtn).text('Delete');
			$(remove).append(removeBtn);
			$(parent).append(remove);
		}
		
		$(parent).removeClass("editing");
		$(parent).append(valueData);
	}
});

var sideMenuDialogView;
azkaban.SideMenuDialogView = Backbone.View.extend({
	events: {
		"click .menu-header": "menuClick"
	},
	
	initialize: function(settings) {
		var children = $(this.el).children();
		for (var i = 0; i < children.length; ++i ) {
			var child = children[i];
			$(child).addClass("menu-header");
			var caption = $(child).find(".menu-caption");
			$(caption).hide();
		}
		this.menuSelect($("#flow-option"));
	},
	
	menuClick: function(evt) {
		this.menuSelect(evt.currentTarget);
	},
	
	menuSelect: function(target) {
		if ($(target).hasClass("active")) {
			return;
		}
		
		$(".side-panel").each(function() {
			$(this).hide();
		});
		
		$(".menu-header").each(function() {
			$(this).find(".menu-caption").slideUp("fast");
			$(this).removeClass("active");
		});
		
		$(target).addClass("active");
		$(target).find(".menu-caption").slideDown("fast");
		var panelName = $(target).attr("viewpanel");
		$("#" + panelName).show();
	}
});

var handleJobMenuClick = function(action, el, pos) {
	var jobid = el[0].jobid;
	
	var requestURL = contextURL + "/manager?project=" + projectName + "&flow=" + flowName + "&job=" + jobid;
	if (action == "open") {
		window.location.href = requestURL;
	}
	else if (action == "openwindow") {
		window.open(requestURL);
	}
}

var executableGraphModel;

/**
 * Disable jobs that need to be disabled
 */
var disableFinishedJobs = function(data) {
	for (var i=0; i < data.nodes.length; ++i) {
		var node = data.nodes[i];
		
		if (node.status == "DISABLED" || node.status == "SKIPPED") {
			node.status = "READY";
			node.disabled = true;
		}
		else if (node.status == "SUCCEEDED" || node.status=="RUNNING") {
			node.disabled = true;
		}
		else if (node.status == "CANCELLED") {
			node.disabled = false;
			node.status="READY";
		}
		else {
			node.disabled = false;
			if (node.type == "flow") {
				disableFinishedJobs(node);
			}
		}
	}
}

/**
 * Enable all jobs. Recurse
 */
var enableAll = function() {
	recurseTree(executableGraphModel.get("data"), false, false);
	executableGraphModel.trigger("change:disabled");
}

var disableAll = function() {
	recurseTree(executableGraphModel.get("data"), true, false);
	executableGraphModel.trigger("change:disabled");
}

var recurseTree = function(data, disabled, recurse) {
	for (var i=0; i < data.nodes.length; ++i) {
		var node = data.nodes[i];
		node.disabled = disabled;
		
		if (node.type == "flow" && recurse) {
			recurseTree(node, disabled);
		}
	}
}

var touchNode = function(node, disable) {
	node.disabled = disable;
	executableGraphModel.trigger("change:disabled");
}

var touchParents = function(node, disable) {
	var inNodes = node.inNodes;

	if (inNodes) {
		for (var key in inNodes) {
			inNodes[key].disabled = disable;
		}
	}

	executableGraphModel.trigger("change:disabled");
}

var touchChildren = function(node, disable) {
	var outNodes = node.outNodes;

	if (outNodes) {
		for (var key in outNodes) {
			outNodes[key].disabled = disable;
		}
	}
	
	executableGraphModel.trigger("change:disabled");
}

var touchAncestors = function(node, disable) {
	recurseAllAncestors(node, disable);
	
	executableGraphModel.trigger("change:disabled");
}

var touchDescendents = function(jobid, disable) {
	recurseAllDescendents(node, disable);
	
	executableGraphModel.trigger("change:disabled");
}

var gatherDisabledNodes = function(data) {
	var nodes = data.nodes;
	var disabled = [];
	
	for (var i = 0; i < nodes.length; ++i) {
		var node = nodes[i];
		if (node.disabled) {
			disabled.push(node.id);
		}
		else {
			if (node.type == "flow") {
				var array = gatherDisabledNodes(node);
				if (array && array.length > 0) {
					disabled.push({id: node.id, children: array});
				}
			}
		}
	}
	
	return disabled;
}

function recurseAllAncestors(node, disable) {
	var inNodes = node.inNodes;
	if (inNodes) {
		for (var key in inNodes) {
			inNodes[key].disabled = disable;
			recurseAllAncestors(inNodes[key], disable);
		}
	}
}

function recurseAllDescendents(node, disable) {
	var outNodes = node.outNodes;
	if (outNodes) {
		for (var key in outNodes) {
			outNodes[key].disabled = disable;
			recurseAllDescendents(outNodes[key], disable);
		}
	}
}

var expanelNodeClickCallback = function(event, model, node) {
	console.log("Node clicked callback");
	var jobId = node.id;
	var flowId = executableGraphModel.get("flowId");
	var type = node.type;
	
	var menu;
	if (type == "flow") {
		var flowRequestURL = contextURL + "/manager?project=" + projectName + "&flow=" + node.flowId;
		if (node.expanded) {
			menu = [
				{title: "Collapse Flow...", callback: function() {model.trigger("collapseFlow", node);}},
				{title: "Open Flow in New Window...", callback: function() {window.open(flowRequestURL);}}
			];
	
		}
		else {
			menu = [
				{title: "Expand Flow...", callback: function() {model.trigger("expandFlow", node);}},
				{title: "Open Flow in New Window...", callback: function() {window.open(flowRequestURL);}}
			];
		}
	}
	else {
		var requestURL = contextURL + "/manager?project=" + projectName + "&flow=" + flowId + "&job=" + jobId;
		menu = [
				{title: "Open Job in New Window...", callback: function() {window.open(requestURL);}},
			];
	}

	$.merge(menu, [
		{break: 1},
		{title: "Enable", callback: function() {touchNode(node, false);}, submenu: [
			{title: "Parents", callback: function(){touchParents(node, false);}},
			{title: "Ancestors", callback: function(){touchAncestors(node, false);}},
			{title: "Children", callback: function(){touchChildren(node, false);}},
			{title: "Descendents", callback: function(){touchDescendents(node, false);}},
			{title: "Enable All", callback: function(){enableAll();}}
		]},
		{title: "Disable", callback: function() {touchNode(node, true)}, submenu: [
			{title: "Parents", callback: function(){touchParents(node, true);}},
			{title: "Ancestors", callback: function(){touchAncestors(node, true);}},
			{title: "Children", callback: function(){touchChildren(node, true);}},
			{title: "Descendents", callback: function(){touchDescendents(node, true);}},
			{title: "Disable All", callback: function(){disableAll();}}
		]},
		{title: "Center Job", callback: function() {model.trigger("centerNode", node);}}
	]);


	contextMenuView.show(event, menu);
}

var expanelEdgeClickCallback = function(event) {
	console.log("Edge clicked callback");
}

var expanelGraphClickCallback = function(event) {
	console.log("Graph clicked callback");
	var flowId = executableGraphModel.get("flowId");
	var requestURL = contextURL + "/manager?project=" + projectName + "&flow=" + flowId;
	
	var menu = [
		{title: "Open Flow in New Window...", callback: function() {window.open(requestURL);}},
		{break: 1},
		{title: "Enable All", callback: function() {enableAll();}},
		{title: "Disable All", callback: function() {disableAll();}},
		{break: 1},
		{title: "Center Graph", callback: function() {executableGraphModel.trigger("resetPanZoom");}}
	];
	
	contextMenuView.show(event, menu);
}

var contextMenuView;
$(function() {
	executableGraphModel = new azkaban.GraphModel();
	flowExecuteDialogView = new azkaban.FlowExecuteDialogView({
		el: $('#execute-flow-panel'), 
		model: executableGraphModel
	});
	
	sideMenuDialogView = new azkaban.SideMenuDialogView({
		el: $('#graph-options')
	});
	editTableView = new azkaban.EditTableView({
		el: $('#editTable')
	});

	contextMenuView = new azkaban.ContextMenuView({
		el: $('#contextMenu')
	});
});