/*
* 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 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 statusView;
azkaban.StatusView = Backbone.View.extend({
initialize: function(settings) {
this.model.bind('change:graph', this.render, this);
this.model.bind('change:update', this.statusUpdate, this);
},
render: function(evt) {
var data = this.model.get("data");
var user = data.submitUser;
$("#submitUser").text(user);
this.statusUpdate(evt);
},
statusUpdate: function(evt) {
var data = this.model.get("data");
statusItem = $("#flowStatus");
for (var j = 0; j < statusList.length; ++j) {
var status = statusList[j];
statusItem.removeClass(status);
}
$("#flowStatus").addClass(data.status);
$("#flowStatus").text(data.status);
var startTime = data.startTime;
var endTime = data.endTime;
if (startTime == -1) {
$("#startTime").text("-");
}
else {
var date = new Date(startTime);
$("#startTime").text(getDateFormat(date));
var lastTime = endTime;
if (endTime == -1) {
var currentDate = new Date();
lastTime = currentDate.getTime();
}
var durationString = getDuration(startTime, lastTime);
$("#duration").text(durationString);
}
if (endTime == -1) {
$("#endTime").text("-");
}
else {
var date = new Date(endTime);
$("#endTime").text(getDateFormat(date));
}
}
});
var flowTabView;
azkaban.FlowTabView = Backbone.View.extend({
events: {
"click #graphViewLink": "handleGraphLinkClick",
"click #jobslistViewLink": "handleJobslistLinkClick",
"click #flowLogViewLink": "handleLogLinkClick",
"click #cancelbtn": "handleCancelClick",
"click #executebtn": "handleRestartClick",
"click #pausebtn": "handlePauseClick",
"click #resumebtn": "handleResumeClick",
"click #retrybtn": "handleRetryClick"
},
initialize: function(settings) {
$("#cancelbtn").hide();
$("#executebtn").hide();
$("#pausebtn").hide();
$("#resumebtn").hide();
$("#retrybtn").hide();
this.model.bind('change:graph', this.handleFlowStatusChange, this);
this.model.bind('change:update', this.handleFlowStatusChange, this);
var selectedView = settings.selectedView;
if (selectedView == "jobslist") {
this.handleJobslistLinkClick();
}
else {
this.handleGraphLinkClick();
}
},
render: function() {
console.log("render graph");
},
handleGraphLinkClick: function(){
$("#jobslistViewLink").removeClass("active");
$("#graphViewLink").addClass("active");
$("#flowLogViewLink").removeClass("active");
$("#jobListView").hide();
$("#graphView").show();
$("#flowLogView").hide();
},
handleJobslistLinkClick: function() {
$("#graphViewLink").removeClass("active");
$("#jobslistViewLink").addClass("active");
$("#flowLogViewLink").removeClass("active");
$("#graphView").hide();
$("#jobListView").show();
$("#flowLogView").hide();
},
handleLogLinkClick: function() {
$("#graphViewLink").removeClass("active");
$("#jobslistViewLink").removeClass("active");
$("#flowLogViewLink").addClass("active");
$("#graphView").hide();
$("#jobListView").hide();
$("#flowLogView").show();
},
handleFlowStatusChange: function() {
var data = this.model.get("data");
$("#cancelbtn").hide();
$("#executebtn").hide();
$("#pausebtn").hide();
$("#resumebtn").hide();
$("#retrybtn").hide();
if (data.status == "SUCCEEDED") {
$("#executebtn").show();
}
else if (data.status == "FAILED") {
$("#executebtn").show();
}
else if (data.status == "FAILED_FINISHING") {
$("#cancelbtn").show();
$("#executebtn").hide();
$("#retrybtn").show();
}
else if (data.status == "RUNNING") {
$("#cancelbtn").show();
$("#pausebtn").show();
}
else if (data.status == "PAUSED") {
$("#cancelbtn").show();
$("#resumebtn").show();
}
else if (data.status == "WAITING") {
$("#cancelbtn").show();
}
else if (data.status == "KILLED") {
$("#executebtn").show();
}
},
handleCancelClick: function(evt) {
var requestURL = contextURL + "/executor";
var requestData = {"execid": execId, "ajax": "cancelFlow"};
var successHandler = function(data) {
console.log("cancel clicked");
if (data.error) {
showDialog("Error", data.error);
}
else {
showDialog("Cancelled", "Flow has been cancelled.");
setTimeout(function() {updateStatus();}, 1100);
}
};
ajaxCall(requestURL, requestData, successHandler);
},
handleRetryClick: function(evt) {
var graphData = graphModel.get("data");
var requestURL = contextURL + "/executor";
var requestData = {"execid": execId, "ajax": "retryFailedJobs"};
var successHandler = function(data) {
console.log("cancel clicked");
if (data.error) {
showDialog("Error", data.error);
}
else {
showDialog("Retry", "Flow has been retried.");
setTimeout(function() {updateStatus();}, 1100);
}
};
ajaxCall(requestURL, requestData, successHandler);
},
handleRestartClick: function(evt) {
console.log("handleRestartClick");
var data = graphModel.get("data");
var nodes = data.nodes;
var executingData = {
project: projectName,
ajax: "executeFlow",
flow: flowId,
execid: execId
};
flowExecuteDialogView.show(executingData);
},
handlePauseClick: function(evt) {
var requestURL = contextURL + "/executor";
var requestData = {"execid": execId, "ajax":"pauseFlow"};
var successHandler = function(data) {
console.log("pause clicked");
if (data.error) {
showDialog("Error", data.error);
}
else {
showDialog("Paused", "Flow has been paused.");
setTimeout(function() {updateStatus();}, 1100);
}
};
ajaxCall(requestURL, requestData, successHandler);
},
handleResumeClick: function(evt) {
var requestURL = contextURL + "/executor";
var requestData = {"execid": execId, "ajax":"resumeFlow"};
var successHandler = function(data) {
console.log("pause clicked");
if (data.error) {
showDialog("Error", data.error);
}
else {
showDialog("Resumed", "Flow has been resumed.");
setTimeout(function() {updateStatus();}, 1100);
}
};
ajaxCall(requestURL, requestData, successHandler);
}
});
var showDialog = function(title, message) {
$('#messageTitle').text(title);
$('#messageBox').text(message);
$('#messageDialog').modal();
}
var jobListView;
var mainSvgGraphView;
var executionListView;
azkaban.ExecutionListView = Backbone.View.extend({
events: {
//"click .flow-progress-bar": "handleProgressBoxClick"
},
initialize: function(settings) {
this.model.bind('change:graph', this.renderJobs, this);
this.model.bind('change:update', this.updateJobs, this);
},
renderJobs: function(evt) {
var data = this.model.get("data");
var lastTime = data.endTime == -1 ? (new Date()).getTime() : data.endTime;
this.updateJobRow(data.nodes);
this.updateProgressBar(data);
},
/*handleProgressBoxClick: function(evt) {
var target = evt.currentTarget;
var job = target.job;
var attempt = target.attempt;
var data = this.model.get("data");
var node = data.nodes[job];
var jobId = event.currentTarget.jobid;
var requestURL = contextURL + "/manager?project=" + projectName + "&execid=" + execId + "&job=" + job + "&attempt=" + attempt;
var menu = [
{title: "Open Job...", callback: function() {window.location.href=requestURL;}},
{title: "Open Job in New Window...", callback: function() {window.open(requestURL);}}
];
contextMenuView.show(evt, menu);
},*/
updateJobs: function(evt) {
var data = this.model.get("update");
var lastTime = data.endTime == -1 ? (new Date()).getTime() : data.endTime;
this.updateJobRow(data.nodes);
this.updateProgressBar(this.model.get("data"));
},
updateJobRow: function(nodes) {
var executingBody = $("#executableBody");
nodes.sort(function(a,b) { return a.startTime - b.startTime; });
for (var i = 0; i < nodes.length; ++i) {
var node = nodes[i];
if (node.startTime < 0) {
continue;
}
var nodeId = node.id.replace(".", "\\\\.");
var row = document.getElementById(nodeId + "-row");
if (!row) {
this.addNodeRow(node);
}
var div = $("#" + nodeId + "-status-div");
div.text(statusStringMap[node.status]);
$(div).attr("class", "status " + node.status);
var startdate = new Date(node.startTime);
$("#" + nodeId + "-start").text(getDateFormat(startdate));
var endTime = node.endTime;
if (node.endTime == -1) {
$("#" + nodeId + "-end").text("-");
endTime = node.startTime + 1;
}
else {
var enddate = new Date(node.endTime);
$("#" + nodeId + "-end").text(getDateFormat(enddate));
}
var progressBar = $("#" + nodeId + "-progressbar");
if (!progressBar.hasClass(node.status)) {
for (var j = 0; j < statusList.length; ++j) {
var status = statusList[j];
progressBar.removeClass(status);
}
progressBar.addClass(node.status);
}
// Create past attempts
if (node.pastAttempts) {
for (var a = 0; a < node.pastAttempts.length; ++a) {
var attemptBarId = nodeId + "-progressbar-" + a;
var attempt = node.pastAttempts[a];
if ($("#" + attemptBarId).length == 0) {
var attemptBox = document.createElement("div");
$(attemptBox).attr("id", attemptBarId);
$(attemptBox).addClass("flow-progress-bar");
$(attemptBox).addClass("attempt");
$(attemptBox).addClass(attempt.status);
$(attemptBox).css("float","left");
$(attemptBox).bind("contextmenu", attemptRightClick);
$(progressBar).before(attemptBox);
attemptBox.job = nodeId;
attemptBox.attempt = a;
}
}
}
if (node.endTime == -1) {
//$("#" + node.id + "-elapse").text("0 sec");
$("#" + nodeId + "-elapse").text(getDuration(node.startTime, (new Date()).getTime()));
}
else {
$("#" + nodeId + "-elapse").text(getDuration(node.startTime, node.endTime));
}
}
},
updateProgressBar: function(data) {
if (data.startTime == -1) {
return;
}
var flowLastTime = data.endTime == -1 ? (new Date()).getTime() : data.endTime;
var flowStartTime = data.startTime;
var outerWidth = $(".flow-progress").css("width");
if (outerWidth) {
if (outerWidth.substring(outerWidth.length - 2, outerWidth.length) == "px") {
outerWidth = outerWidth.substring(0, outerWidth.length - 2);
}
outerWidth = parseInt(outerWidth);
}
var nodes = data.nodes;
var diff = flowLastTime - flowStartTime;
var factor = outerWidth/diff;
for (var i = 0; i < nodes.length; ++i) {
var node = nodes[i];
var nodeId = node.id.replace(".", "\\\\.");
// calculate the progress
var elem = $("#" + node.id + "-progressbar");
var offsetLeft = 0;
var minOffset = 0;
elem.attempt = 0;
// Add all the attempts
if (node.pastAttempts) {
var logURL = contextURL + "/executor?execid=" + execId + "&job=" + node.id + "&attempt=" + node.pastAttempts.length;
var aId = node.id + "-log-link";
$("#" + aId).attr("href", logURL);
elem.attempt = node.pastAttempts.length;
// Calculate the node attempt bars
for (var p = 0; p < node.pastAttempts.length; ++p) {
var pastAttempt = node.pastAttempts[p];
var pastAttemptBox = $("#" + nodeId + "-progressbar-" + p);
var left = (pastAttempt.startTime - flowStartTime)*factor;
var width = Math.max((pastAttempt.endTime - pastAttempt.startTime)*factor, 3);
var margin = left - offsetLeft;
$(pastAttemptBox).css("margin-left", left - offsetLeft);
$(pastAttemptBox).css("width", width);
$(pastAttemptBox).attr("title", "attempt:" + p + " start:" + getHourMinSec(new Date(pastAttempt.startTime)) + " end:" + getHourMinSec(new Date(pastAttempt.endTime)));
offsetLeft += width + margin;
}
}
var nodeLastTime = node.endTime == -1 ? (new Date()).getTime() : node.endTime;
var left = Math.max((node.startTime-flowStartTime)*factor, minOffset);
var margin = left - offsetLeft;
var width = Math.max((nodeLastTime - node.startTime)*factor, 3);
width = Math.min(width, outerWidth);
elem.css("margin-left", left)
elem.css("width", width);
elem.attr("title", "attempt:" + elem.attempt + " start:" + getHourMinSec(new Date(node.startTime)) + " end:" + getHourMinSec(new Date(node.endTime)));
}
},
addNodeRow: function(node) {
var executingBody = $("#executableBody");
var tr = document.createElement("tr");
var tdName = document.createElement("td");
var tdTimeline = document.createElement("td");
var tdStart = document.createElement("td");
var tdEnd = document.createElement("td");
var tdElapse = document.createElement("td");
var tdStatus = document.createElement("td");
var tdDetails = document.createElement("td");
$(tr).append(tdName);
$(tr).append(tdTimeline);
$(tr).append(tdStart);
$(tr).append(tdEnd);
$(tr).append(tdElapse);
$(tr).append(tdStatus);
$(tr).append(tdDetails);
$(tr).attr("id", node.id + "-row");
$(tdTimeline).attr("id", node.id + "-timeline");
$(tdStart).attr("id", node.id + "-start");
$(tdEnd).attr("id", node.id + "-end");
$(tdElapse).attr("id", node.id + "-elapse");
$(tdStatus).attr("id", node.id + "-status");
var outerProgressBar = document.createElement("div");
$(outerProgressBar).attr("id", node.id + "-outerprogressbar");
$(outerProgressBar).addClass("flow-progress");
var progressBox = document.createElement("div");
progressBox.job = node.id;
$(progressBox).attr("id", node.id + "-progressbar");
$(progressBox).addClass("flow-progress-bar");
$(outerProgressBar).append(progressBox);
$(tdTimeline).append(outerProgressBar);
$(tdTimeline).addClass("timeline");
var requestURL = contextURL + "/manager?project=" + projectName + "&job=" + node.id + "&history";
var a = document.createElement("a");
$(a).attr("href", requestURL);
$(a).text(node.id);
$(tdName).append(a);
var status = document.createElement("div");
$(status).addClass("status");
$(status).attr("id", node.id + "-status-div");
tdStatus.appendChild(status);
var logURL = contextURL + "/executor?execid=" + execId + "&job=" + node.id;
if (node.attempt) {
logURL += "&attempt=" + node.attempt;
}
var a = document.createElement("a");
$(a).attr("href", logURL);
$(a).attr("id", node.id + "-log-link");
$(a).text("Details");
$(tdDetails).addClass("details");
$(tdDetails).append(a);
executingBody.append(tr);
}
});
var flowLogView;
azkaban.FlowLogView = Backbone.View.extend({
events: {
"click #updateLogBtn" : "handleUpdate"
},
initialize: function(settings) {
this.model.set({"offset": 0});
this.handleUpdate();
},
handleUpdate: function(evt) {
var offset = this.model.get("offset");
var requestURL = contextURL + "/executor";
var model = this.model;
console.log("fetchLogs offset is " + offset)
$.ajax({
async: false,
url: requestURL,
data: {
"execid": execId,
"ajax": "fetchExecFlowLogs",
"offset": offset,
"length": 50000
},
success: function(data) {
console.log("fetchLogs");
if (data.error) {
console.log(data.error);
}
else {
var log = $("#logSection").text();
if (!log) {
log = data.data;
}
else {
log += data.data;
}
var newOffset = data.offset + data.length;
$("#logSection").text(log);
model.set({"offset": newOffset, "log": log});
$(".logViewer").scrollTop(9999);
}
}
});
}
});
var graphModel;
azkaban.GraphModel = Backbone.Model.extend({});
var logModel;
azkaban.LogModel = Backbone.Model.extend({});
var updateStatus = function() {
var requestURL = contextURL + "/executor";
var oldData = graphModel.get("data");
var nodeMap = graphModel.get("nodeMap");
var requestData = {
"execid": execId,
"ajax": "fetchexecflowupdate",
"lastUpdateTime": updateTime
};
var successHandler = function(data) {
console.log("data updated");
updateTime = data.updateTime;
oldData.submitTime = data.submitTime;
oldData.startTime = data.startTime;
oldData.endTime = data.endTime;
oldData.status = data.status;
for (var i = 0; i < data.nodes.length; ++i) {
var node = data.nodes[i];
var oldNode = nodeMap[node.id];
oldNode.startTime = node.startTime;
oldNode.updateTime = node.updateTime;
oldNode.endTime = node.endTime;
oldNode.status = node.status;
oldNode.attempt = node.attempt;
if (oldNode.attempt > 0) {
oldNode.pastAttempts = node.pastAttempts;
}
}
graphModel.set({"update": data});
graphModel.trigger("change:update");
};
ajaxCall(requestURL, requestData, successHandler);
}
var updateTime = -1;
var updaterFunction = function() {
var oldData = graphModel.get("data");
var keepRunning =
oldData.status != "SUCCEEDED" &&
oldData.status != "FAILED" &&
oldData.status != "KILLED";
if (keepRunning) {
updateStatus();
var data = graphModel.get("data");
if (data.status == "UNKNOWN" ||
data.status == "WAITING" ||
data.status == "PREPARING") {
setTimeout(function() {updaterFunction();}, 1000);
}
else if (data.status != "SUCCEEDED" && data.status != "FAILED") {
// 5 sec updates
setTimeout(function() {updaterFunction();}, 5000);
}
else {
console.log("Flow finished, so no more updates");
}
}
else {
console.log("Flow finished, so no more updates");
}
}
var logUpdaterFunction = function() {
var oldData = graphModel.get("data");
var keepRunning =
oldData.status != "SUCCEEDED" &&
oldData.status != "FAILED" &&
oldData.status != "KILLED";
if (keepRunning) {
// update every 30 seconds for the logs until finished
flowLogView.handleUpdate();
setTimeout(function() {logUpdaterFunction();}, 30000);
}
else {
flowLogView.handleUpdate();
}
}
var exNodeClickCallback = function(event) {
console.log("Node clicked callback");
var jobId = event.currentTarget.jobid;
var requestURL = contextURL + "/manager?project=" + projectName + "&flow=" + flowId + "&job=" + jobId;
var visualizerURL = contextURL + "/pigvisualizer?execid=" + execId + "&jobid=" + jobId;
var menu = [
{title: "Open Job...", callback: function() {window.location.href = requestURL;}},
{title: "Open Job in New Window...", callback: function() {window.open(requestURL);}},
{title: "Visualize Job...", callback: function() {window.location.href = visualizerURL;}}
];
contextMenuView.show(event, menu);
}
var exJobClickCallback = function(event) {
console.log("Node clicked callback");
var jobId = event.currentTarget.jobid;
var requestURL = contextURL + "/manager?project=" + projectName + "&flow=" + flowId + "&job=" + jobId;
var visualizerURL = contextURL + "/pigvisualizer?execid=" + execId + "&jobid=" + jobId;
var menu = [
{title: "Open Job...", callback: function() {window.location.href = requestURL;}},
{title: "Open Job in New Window...", callback: function() {window.open(requestURL);}},
{title: "Visualize Job...", callback: function() {window.location.href = visualizerURL;}}
];
contextMenuView.show(event, menu);
}
var exEdgeClickCallback = function(event) {
console.log("Edge clicked callback");
}
var exGraphClickCallback = function(event) {
console.log("Graph clicked callback");
var requestURL = contextURL + "/manager?project=" + projectName + "&flow=" + flowId;
var menu = [
{title: "Open Flow...", callback: function() {window.location.href=requestURL;}},
{title: "Open Flow in New Window...", callback: function() {window.open(requestURL);}},
{break: 1},
{title: "Center Graph", callback: function() {graphModel.trigger("resetPanZoom");}}
];
contextMenuView.show(event, menu);
}
var attemptRightClick = function(event) {
var target = event.currentTarget;
var job = target.job;
var attempt = target.attempt;
var jobId = event.currentTarget.jobid;
var requestURL = contextURL + "/executor?project=" + projectName + "&execid=" + execId + "&job=" + job + "&attempt=" + attempt;
var menu = [
{title: "Open Attempt Log...", callback: function() {window.location.href=requestURL;}},
{title: "Open Attempt Log in New Window...", callback: function() {window.open(requestURL);}}
];
contextMenuView.show(event, menu);
return false;
}
$(function() {
var selected;
graphModel = new azkaban.GraphModel();
logModel = new azkaban.LogModel();
flowTabView = new azkaban.FlowTabView({
el: $('#headertabs'),
model: graphModel
});
mainSvgGraphView = new azkaban.SvgGraphView({
el: $('#svgDiv'),
model: graphModel,
rightClick: {
"node": exNodeClickCallback,
"edge": exEdgeClickCallback,
"graph": exGraphClickCallback
}
});
jobsListView = new azkaban.JobListView({
el: $('#jobList'),
model: graphModel,
contextMenuCallback: exJobClickCallback
});
flowLogView = new azkaban.FlowLogView({
el: $('#flowLogView'),
model: logModel
});
statusView = new azkaban.StatusView({
el: $('#flow-status'),
model: graphModel
});
executionListView = new azkaban.ExecutionListView({
el: $('#jobListView'),
model: graphModel
});
var requestURL = contextURL + "/executor";
var requestData = {"execid": execId, "ajax":"fetchexecflow"};
var successHandler = function(data) {
console.log("data fetched");
graphModel.set({data: data});
graphModel.set({disabled: {}});
graphModel.trigger("change:graph");
updateTime = Math.max(updateTime, data.submitTime);
updateTime = Math.max(updateTime, data.startTime);
updateTime = Math.max(updateTime, data.endTime);
var nodeMap = {};
for (var i = 0; i < data.nodes.length; ++i) {
var node = data.nodes[i];
nodeMap[node.id] = node;
updateTime = Math.max(updateTime, node.startTime);
updateTime = Math.max(updateTime, node.endTime);
}
for (var i = 0; i < data.edges.length; ++i) {
var edge = data.edges[i];
if (!nodeMap[edge.target].in) {
nodeMap[edge.target].in = {};
}
var targetInMap = nodeMap[edge.target].in;
targetInMap[edge.from] = nodeMap[edge.from];
if (!nodeMap[edge.from].out) {
nodeMap[edge.from].out = {};
}
var sourceOutMap = nodeMap[edge.from].out;
sourceOutMap[edge.target] = nodeMap[edge.target];
}
graphModel.set({nodeMap: nodeMap});
if (window.location.hash) {
var hash = window.location.hash;
if (hash == "#jobslist") {
flowTabView.handleJobslistLinkClick();
}
else if (hash == "#log") {
flowTabView.handleLogLinkClick();
}
}
else {
flowTabView.handleGraphLinkClick();
}
updaterFunction();
logUpdaterFunction();
};
ajaxCall(requestURL, requestData, successHandler);
});