azkaban-memoizeit
Changes
src/web/css/azkaban-graph.css 130(+127 -3)
src/web/js/azkaban.context.menu.js 185(+100 -85)
src/web/js/azkaban.flow.job.view.js 1(+0 -1)
src/web/js/azkaban.flow.view.js 116(+42 -74)
src/web/js/azkaban.job.status.utils.js 26(+0 -26)
src/web/js/azkaban.layout.js 9(+4 -5)
src/web/js/azkaban.staging.flow.view.js 26(+0 -26)
src/web/js/azkaban.svg.flow.loader.js 121(+44 -77)
src/web/js/azkaban.svg.graph.view.js 816(+392 -424)
src/web/js/svgutils.js 70(+70 -0)
Details
diff --git a/src/java/azkaban/webapp/servlet/ProjectManagerServlet.java b/src/java/azkaban/webapp/servlet/ProjectManagerServlet.java
index 2a4458d..3bb7194 100644
--- a/src/java/azkaban/webapp/servlet/ProjectManagerServlet.java
+++ b/src/java/azkaban/webapp/servlet/ProjectManagerServlet.java
@@ -525,9 +525,9 @@ public class ProjectManagerServlet extends LoginAbstractAzkabanServlet {
nodeObj.put("type", node.getType());
if (node.getEmbeddedFlowId() != null) {
nodeObj.put("flowId", node.getEmbeddedFlowId());
-// HashMap<String, Object> embeddedNodeObj = new HashMap<String, Object>();
-// fillFlowInfo2(project, node.getEmbeddedFlowId(), embeddedNodeObj);
-// nodeObj.put("flowData", embeddedNodeObj);
+ HashMap<String, Object> embeddedNodeObj = new HashMap<String, Object>();
+ fillFlowInfo2(project, node.getEmbeddedFlowId(), embeddedNodeObj);
+ nodeObj.put("flowData", embeddedNodeObj);
}
nodeList.add(nodeObj);
diff --git a/src/java/azkaban/webapp/servlet/velocity/flowpage.vm b/src/java/azkaban/webapp/servlet/velocity/flowpage.vm
index 67cde66..6519a3d 100644
--- a/src/java/azkaban/webapp/servlet/velocity/flowpage.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/flowpage.vm
@@ -26,11 +26,14 @@
<script type="text/javascript" src="${context}/js/backbone-0.9.10-min.js"></script>
<script type="text/javascript" src="${context}/js/jquery.simplemodal-1.4.4.js"></script>
+ <script type="text/javascript" src="${context}/js/raphael-min.js"></script>
+ <script type="text/javascript" src="${context}/js/svgutils.js"></script>
<script type="text/javascript" src="${context}/js/azkaban.date.utils.js"></script>
<script type="text/javascript" src="${context}/js/azkaban.ajax.utils.js"></script>
<script type="text/javascript" src="${context}/js/azkaban.nav.js"></script>
<script type="text/javascript" src="${context}/js/azkaban.layout.js"></script>
<script type="text/javascript" src="${context}/js/svgNavigate.js"></script>
+ <script type="text/javascript" src="${context}/js/d3.v3.min.js"></script>
<script type="text/javascript" src="${context}/js/azkaban.svg.flow.loader.js"></script>
<script type="text/javascript" src="${context}/js/azkaban.svg.graph.view.js"></script>
<script type="text/javascript" src="${context}/js/azkaban.flow.extended.view.js"></script>
src/web/css/azkaban-graph.css 130(+127 -3)
diff --git a/src/web/css/azkaban-graph.css b/src/web/css/azkaban-graph.css
index b5b6b57..c78a508 100644
--- a/src/web/css/azkaban-graph.css
+++ b/src/web/css/azkaban-graph.css
@@ -1,3 +1,126 @@
+.nodebox text {
+ pointer-events: none;
+}
+
+.nodebox image {
+ pointer-events: none;
+}
+
+/* Nodes */
+.node:hover {
+ cursor: pointer;
+}
+
+.nodebox > .border:hover {
+ fill-opacity: 0.7;
+}
+
+.nodebox > .flowborder:hover {
+ stroke-opacity: 0.7;
+}
+
+.border {
+ stroke-width: 1;
+}
+
+.flownode .nodebox .flowborder {
+ stroke-width: 1.25;
+ fill: #FFF;
+ fill-opacity: 0.8;
+}
+
+.READY > g > rect {
+ fill: #DDD;
+ stroke: #CCC;
+}
+
+.READY > g > text {
+ fill: #000;
+}
+
+.RUNNING > g > rect {
+ fill: #39b3d7;
+ stroke: #39b3d7;
+}
+
+.RUNNING > g > text {
+ fill: #FFF;
+}
+
+.SUCCEEDED > g > rect {
+ fill: #5cb85c;
+ stroke: #4cae4c;
+}
+
+.SUCCEEDED > g > text {
+ fill: #FFF;
+}
+
+.FAILED > g > rect {
+ fill: #d2322d;
+ stroke: #d2322d;
+}
+
+.FAILED > g > text {
+ fill: #FFF;
+}
+
+.FAILED_FINISHING > g > rect {
+ fill: #ed9c28;
+ stroke: #ed9c28;
+}
+
+.FAILED_FINISHING > g > text {
+ fill: #FFF;
+}
+
+.DISABLED > g > rect {
+ fill: #800000;
+ stroke: #800000;
+}
+
+.DISABLED > g > rect {
+ fill: #800000;
+ stroke: #800000;
+}
+
+.SKIPPED > g > rect {
+ fill: #CCC;
+ stroke: #CCC;
+}
+
+.SKIPPED > g > rect {
+ fill: #CCC;
+ stroke: #CCC;
+}
+
+.DISABLED {
+ opacity: 0.25;
+}
+
+.SKIPPED {
+ opacity: 0.25;
+}
+
+/* Edges */
+.edge {
+ stroke: #CCC;
+ stroke-width: 1.5;
+}
+
+.edge:hover {
+ stroke: #009FC9;
+ stroke-width: 1.5;
+}
+
+/*
+svg text1 {
+ pointer-events: none;
+}
+
+svg g.nodebox1 {
+ pointer-events: none;
+}
svg .edge {
stroke: #BBB;
@@ -97,7 +220,7 @@ svg .selected .nodebox rect {
}
svg .node rect {
- fill: #F8F8F8;
+ fill: #CCC;
stroke: #CCC;
stroke-width: 2;
}
@@ -115,7 +238,7 @@ svg .node:hover rect {
}
svg .READY rect {
- fill: #EEE;
+ fill: #CCC;
}
svg .RUNNING rect {
@@ -244,4 +367,5 @@ svg circle.SKIPPED {
.svgTiny {
width: 100%;
height: 100%;
-}
\ No newline at end of file
+}
+*/
src/web/js/azkaban.context.menu.js 185(+100 -85)
diff --git a/src/web/js/azkaban.context.menu.js b/src/web/js/azkaban.context.menu.js
index 7bca798..d0163a7 100644
--- a/src/web/js/azkaban.context.menu.js
+++ b/src/web/js/azkaban.context.menu.js
@@ -1,97 +1,112 @@
+/*
+ * 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');
azkaban.ContextMenuView = Backbone.View.extend({
- events : {
- },
- initialize : function(settings) {
- var div = this.el;
- $('body').click(function(e) {
- $(".contextMenu").remove();
- });
- $('body').bind("contextmenu", function(e) {$(".contextMenu").remove()});
- },
- show : function(evt, menu) {
- console.log("Show context menu");
- $(".contextMenu").remove();
- var x = evt.pageX;
- var y = evt.pageY;
+ events : {
+ },
+ initialize : function(settings) {
+ var div = this.el;
+ $('body').click(function(e) {
+ $(".contextMenu").remove();
+ });
+ $('body').bind("contextmenu", function(e) {$(".contextMenu").remove()});
+ },
+ show : function(evt, menu) {
+ console.log("Show context menu");
+ $(".contextMenu").remove();
+ var x = evt.pageX;
+ var y = evt.pageY;
- var contextMenu = this.setupMenu(menu);
- $(contextMenu).css({top: y, left: x});
-
- $(this.el).after(contextMenu);
- },
- hide : function(evt) {
- console.log("Hide context menu");
- $(".contextMenu").remove();
- },
- handleClick: function(evt) {
- console.log("handling click");
- },
- setupMenu: function(menu) {
- var contextMenu = document.createElement("div");
- $(contextMenu).addClass("contextMenu");
- var ul = document.createElement("ul");
- $(contextMenu).append(ul);
+ var contextMenu = this.setupMenu(menu);
+ $(contextMenu).css({top: y, left: x});
+ $(this.el).after(contextMenu);
+ },
+ hide : function(evt) {
+ console.log("Hide context menu");
+ $(".contextMenu").remove();
+ },
+ handleClick: function(evt) {
+ console.log("handling click");
+ },
+ setupMenu: function(menu) {
+ var contextMenu = document.createElement("div");
+ $(contextMenu).addClass("contextMenu");
+ var ul = document.createElement("ul");
+ $(contextMenu).append(ul);
- for (var i = 0; i < menu.length; ++i) {
- var menuItem = document.createElement("li");
- if (menu[i].break) {
- $(menuItem).addClass("break");
- }
- else {
- var title = menu[i].title;
- var callback = menu[i].callback;
- $(menuItem).addClass("menuitem");
- $(menuItem).text(title);
- menuItem.callback = callback;
- $(menuItem).click(function() {
- $(contextMenu).hide();
- this.callback.call();});
-
- if (menu[i].submenu) {
- var expandSymbol = document.createElement("div");
- $(expandSymbol).addClass("expandSymbol");
- $(menuItem).append(expandSymbol);
-
- var subMenu = this.setupMenu(menu[i].submenu);
- $(subMenu).addClass("subMenu");
- subMenu.parent = contextMenu;
- menuItem.subMenu = subMenu;
- $(subMenu).hide();
- $(this.el).after(subMenu);
-
- $(menuItem).mouseenter(function() {
- $(".subMenu").hide();
- var menuItem = this;
- menuItem.selected = true;
- setTimeout(function() {
- if (menuItem.selected) {
- var offset = $(menuItem).offset();
- var left = offset.left;
- var top = offset.top;
- var width = $(menuItem).width();
- var subMenu = menuItem.subMenu;
-
- var newLeft = left + width - 5;
- $(subMenu).css({left: newLeft, top: top});
- $(subMenu).show();
- }
- }, 500);
- });
- $(menuItem).mouseleave(function() {this.selected = false;});
- }
- }
+ for (var i = 0; i < menu.length; ++i) {
+ var menuItem = document.createElement("li");
+ if (menu[i].break) {
+ $(menuItem).addClass("break");
+ }
+ else {
+ var title = menu[i].title;
+ var callback = menu[i].callback;
+ $(menuItem).addClass("menuitem");
+ $(menuItem).text(title);
+ menuItem.callback = callback;
+ $(menuItem).click(function() {
+ $(contextMenu).hide();
+ this.callback.call();});
+
+ if (menu[i].submenu) {
+ var expandSymbol = document.createElement("div");
+ $(expandSymbol).addClass("expandSymbol");
+ $(menuItem).append(expandSymbol);
+
+ var subMenu = this.setupMenu(menu[i].submenu);
+ $(subMenu).addClass("subMenu");
+ subMenu.parent = contextMenu;
+ menuItem.subMenu = subMenu;
+ $(subMenu).hide();
+ $(this.el).after(subMenu);
+
+ $(menuItem).mouseenter(function() {
+ $(".subMenu").hide();
+ var menuItem = this;
+ menuItem.selected = true;
+ setTimeout(function() {
+ if (menuItem.selected) {
+ var offset = $(menuItem).offset();
+ var left = offset.left;
+ var top = offset.top;
+ var width = $(menuItem).width();
+ var subMenu = menuItem.subMenu;
+
+ var newLeft = left + width - 5;
+ $(subMenu).css({left: newLeft, top: top});
+ $(subMenu).show();
+ }
+ }, 500);
+ });
+ $(menuItem).mouseleave(function() {this.selected = false;});
+ }
+ }
- $(ul).append(menuItem);
- }
+ $(ul).append(menuItem);
+ }
- return contextMenu;
- }
+ return contextMenu;
+ }
});
var contextMenuView;
$(function() {
- contextMenuView = new azkaban.ContextMenuView({el:$('#contextMenu')});
- contextMenuView.hide();
+ contextMenuView = new azkaban.ContextMenuView({el:$('#contextMenu')});
+ contextMenuView.hide();
});
\ No newline at end of file
src/web/js/azkaban.flow.job.view.js 1(+0 -1)
diff --git a/src/web/js/azkaban.flow.job.view.js b/src/web/js/azkaban.flow.job.view.js
index a85f9a2..55eaa25 100644
--- a/src/web/js/azkaban.flow.job.view.js
+++ b/src/web/js/azkaban.flow.job.view.js
@@ -93,7 +93,6 @@ azkaban.JobListView = Backbone.View.extend({
render: function(self) {
var data = this.model.get("data");
var nodes = data.nodes;
- var edges = data.edges;
this.listNodes = {};
if (nodes.length == 0) {
src/web/js/azkaban.flow.view.js 116(+42 -74)
diff --git a/src/web/js/azkaban.flow.view.js b/src/web/js/azkaban.flow.view.js
index 010291a..a9d6808 100644
--- a/src/web/js/azkaban.flow.view.js
+++ b/src/web/js/azkaban.flow.view.js
@@ -24,32 +24,6 @@ var handleJobMenuClick = function(action, el, pos) {
}
}
-function hasClass(el, name)
-{
- var classes = el.getAttribute("class");
- if (classes == null) {
- return false;
- }
- return new RegExp('(\\s|^)'+name+'(\\s|$)').test(classes);
-}
-
-function addClass(el, name)
-{
- if (!hasClass(el, name)) {
- var classes = el.getAttribute("class");
- classes += classes ? ' ' + name : '' +name;
- el.setAttribute("class", classes);
- }
-}
-
-function removeClass(el, name)
-{
- if (hasClass(el, name)) {
- var classes = el.getAttribute("class");
- el.setAttribute("class", classes.replace(new RegExp('(\\s|^)'+name+'(\\s|$)'),' ').replace(/^\s+|\s+$/g, ''));
- }
-}
-
var flowTabView;
azkaban.FlowTabView= Backbone.View.extend({
events : {
@@ -294,55 +268,49 @@ $(function() {
mainSvgGraphView = new azkaban.SvgGraphView({el:$('#svgDiv'), model: graphModel, rightClick: { "node": nodeClickCallback, "edge": edgeClickCallback, "graph": graphClickCallback }});
jobsListView = new azkaban.JobListView({el:$('#jobList'), model: graphModel, contextMenuCallback: jobClickCallback});
- var requestURL = contextURL + "/manager";
+ setTimeout(
+ function() {
+ processFlowData(jsonFlowObject);
+ graphModel.set({data:jsonFlowObject});
+ graphModel.trigger("change:graph");
+ },
+ 10);
- // Set up the Flow options view. Create a new one every time :p
- $('#executebtn').click( function() {
- closeAllSubDisplays();
- var data = graphModel.get("data");
- var nodes = data.nodes;
-
- var executingData = {
- project: projectName,
- ajax: "executeFlow",
- flow: flowId
- };
-
- flowExecuteDialogView.show(executingData);
- });
- $.get(
- requestURL,
- {"project": projectName, "ajax":"fetchflowgraph", "flow":flowId},
- function(data) {
- parseFlowData(data, graphModel);
- graphModel.trigger("change:graph");
-
- // Handle the hash changes here so the graph finishes rendering first.
- if (window.location.hash) {
- var hash = window.location.hash;
-
- if (hash == "#executions") {
- flowTabView.handleExecutionLinkClick();
- }
- else if (hash == "#graph") {
- // Redundant, but we may want to change the default.
- selected = "graph";
- }
- else {
- if ("#page" == hash.substring(0, "#page".length)) {
- var page = hash.substring("#page".length, hash.length);
- console.log("page " + page);
- flowTabView.handleExecutionLinkClick();
- executionModel.set({"page": parseInt(page)});
- }
- else {
- selected = "graph";
- }
- }
- }
- },
- "json"
- );
+//
+// $.get(
+// requestURL,
+// {},
+// function(data) {
+// processFlowData(data);
+// graphModel.set({data:data});
+// graphModel.trigger("change:graph");
+//
+// // Handle the hash changes here so the graph finishes rendering first.
+// if (window.location.hash) {
+// var hash = window.location.hash;
+//
+// if (hash == "#executions") {
+// flowTabView.handleExecutionLinkClick();
+// }
+// else if (hash == "#graph") {
+// // Redundant, but we may want to change the default.
+// selected = "graph";
+// }
+// else {
+// if ("#page" == hash.substring(0, "#page".length)) {
+// var page = hash.substring("#page".length, hash.length);
+// console.log("page " + page);
+// flowTabView.handleExecutionLinkClick();
+// executionModel.set({"page": parseInt(page)});
+// }
+// else {
+// selected = "graph";
+// }
+// }
+// }
+// },
+// "json"
+// );
});
src/web/js/azkaban.job.status.utils.js 26(+0 -26)
diff --git a/src/web/js/azkaban.job.status.utils.js b/src/web/js/azkaban.job.status.utils.js
index 7d4b45d..bc2d3bf 100644
--- a/src/web/js/azkaban.job.status.utils.js
+++ b/src/web/js/azkaban.job.status.utils.js
@@ -13,29 +13,3 @@ var statusStringMap = {
"UNKNOWN": "Unknown",
"PAUSED": "Paused"
};
-
-function hasClass(el, name)
-{
- var classes = el.getAttribute("class");
- if (classes == null) {
- return false;
- }
- return new RegExp('(\\s|^)'+name+'(\\s|$)').test(classes);
-}
-
-function addClass(el, name)
-{
- if (!hasClass(el, name)) {
- var classes = el.getAttribute("class");
- classes += classes ? ' ' + name : '' +name;
- el.setAttribute("class", classes);
- }
-}
-
-function removeClass(el, name)
-{
- if (hasClass(el, name)) {
- var classes = el.getAttribute("class");
- el.setAttribute("class", classes.replace(new RegExp('(\\s|^)'+name+'(\\s|$)'),' ').replace(/^\s+|\s+$/g, ''));
- }
-}
\ No newline at end of file
src/web/js/azkaban.layout.js 9(+4 -5)
diff --git a/src/web/js/azkaban.layout.js b/src/web/js/azkaban.layout.js
index 07debec..ba02b06 100644
--- a/src/web/js/azkaban.layout.js
+++ b/src/web/js/azkaban.layout.js
@@ -18,7 +18,6 @@ var idSort = function(a, b) {
function prepareLayout(nodes, hmargin, layers, nodeMap) {
var maxLayer = 0;
- var numLayer = 0;
var nodeQueue = new Array();
// Find start layers first
for (var i=0; i < nodes.length; ++i) {
@@ -61,8 +60,6 @@ function prepareLayout(nodes, hmargin, layers, nodeMap) {
// Assign to layers
for (var i = 0; i < nodes.length; ++i) {
- numLayer = Math.max(numLayer, nodes[i].level);
-
var width = nodes[i].width ? nodes[i].width : nodes[i].label.length * 11.5 + 4;
var height = nodes[i].height ? nodes[i].height : 1;
var node = { id: nodes[i].id, node: nodes[i], level: nodes[i].level, in:[], out:[], width: width + hmargin, x:0, height:height };
@@ -76,10 +73,13 @@ function prepareLayout(nodes, hmargin, layers, nodeMap) {
layers[node.level].push(node);
}
- layers.numLayer = numLayer;
layers.maxLayer = maxLayer;
}
+function respaceGraph(nodes, edges) {
+
+}
+
function layoutGraph(nodes, edges, hmargin) {
var startLayer = [];
@@ -92,7 +92,6 @@ function layoutGraph(nodes, edges, hmargin) {
prepareLayout(nodes, hmargin, layers, nodeMap);
var maxLayer = layers.maxLayer;
- var numLayer = layers.numLayer;
// Create dummy nodes
var edgeDummies = {};
src/web/js/azkaban.staging.flow.view.js 26(+0 -26)
diff --git a/src/web/js/azkaban.staging.flow.view.js b/src/web/js/azkaban.staging.flow.view.js
index 3f417bc..7e12947 100644
--- a/src/web/js/azkaban.staging.flow.view.js
+++ b/src/web/js/azkaban.staging.flow.view.js
@@ -152,32 +152,6 @@ function recurseAllDescendents(nodes, disabledMap, id, disable) {
}
}
-function hasClass(el, name)
-{
- var classes = el.getAttribute("class");
- if (classes == null) {
- return false;
- }
- return new RegExp('(\\s|^)'+name+'(\\s|$)').test(classes);
-}
-
-function addClass(el, name)
-{
- if (!hasClass(el, name)) {
- var classes = el.getAttribute("class");
- classes += classes ? ' ' + name : '' +name;
- el.setAttribute("class", classes);
- }
-}
-
-function removeClass(el, name)
-{
- if (hasClass(el, name)) {
- var classes = el.getAttribute("class");
- el.setAttribute("class", classes.replace(new RegExp('(\\s|^)'+name+'(\\s|$)'),' ').replace(/^\s+|\s+$/g, ''));
- }
-}
-
var flowTabView;
azkaban.FlowTabView= Backbone.View.extend({
events : {
src/web/js/azkaban.svg.flow.loader.js 121(+44 -77)
diff --git a/src/web/js/azkaban.svg.flow.loader.js b/src/web/js/azkaban.svg.flow.loader.js
index 801877c..88e46cf 100644
--- a/src/web/js/azkaban.svg.flow.loader.js
+++ b/src/web/js/azkaban.svg.flow.loader.js
@@ -2,46 +2,6 @@ var extendedViewPanels = {};
var extendedDataModels = {};
var openJobDisplayCallback = function(nodeId, flowId, evt) {
console.log("Open up data");
- var target = evt.currentTarget;
- var node = target.nodeobj;
-
- // If target panel exists, than we display and skip.
- var targetPanel = node.panel;
- if (targetPanel) {
- $("#flowInfoBase").before(targetPanel);
- targetPanel.showExtendedView(evt);
- }
- else {
- var targetModel = node.dataModel;
- var flowId = flowId;
-
- if (!targetModel) {
- var requestURL = contextURL + "/manager";
- var newParentPath = node.parentPath ? node.parentPath + ":" + flowId : flowId;
- node.parentPath = newParentPath;
-
- $.get(
- requestURL,
- {"project": projectName, "ajax":"fetchflownodedata", "flow":flowId, "node": node.id},
- function(data) {
- var graphModel = new azkaban.GraphModel();
- graphModel.set({id: data.id, flow: data.flowData, type: data.type, props: data.props});
-
- var flowData = data.flowData;
- if (flowData) {
- parseFlowData(flowData, graphModel, newParentPath);
- }
-
- node.dataModel = graphModel;
- createNewPanel(node, graphModel, evt);
- },
- "json"
- );
- }
- else {
- createNewPanel(node, targetModel, evt);
- }
- }
/*
$("#flowInfoBase").before(cloneStuff);
@@ -74,7 +34,7 @@ var createNewPanel = function(node, model, evt) {
var nodeInfoPanelID = parentPath ? parentPath + ":" + node.id + "-info" : node.id + "-info";
var cloneStuff = $("#flowInfoBase").clone();
- cloneStuff.nodeobj = node;
+ cloneStuff.data = node;
$(cloneStuff).attr("id", nodeInfoPanelID);
$("#flowInfoBase").before(cloneStuff);
@@ -83,15 +43,20 @@ var createNewPanel = function(node, model, evt) {
backboneView.showExtendedView(evt);
}
-var parseFlowData = function(data, model, parentPath) {
+/**
+ * Processes the flow data from Javascript
+ */
+var processFlowData = function(data) {
var nodes = {};
var edges = new Array();
+
+ //Create a node map
for (var i=0; i < data.nodes.length; ++i) {
var node = data.nodes[i];
nodes[node.id] = node;
}
- var nodeQueue = new Array();
+ // Create each node in and out nodes. Create an edge list.
for (var i=0; i < data.nodes.length; ++i) {
var node = data.nodes[i];
if (node.in) {
@@ -109,54 +74,53 @@ var parseFlowData = function(data, model, parentPath) {
edges.push({to: node.id, from: fromNode.id});
}
}
- else {
- // Queue used for breath first.
- nodeQueue.push(node);
- }
}
- // Iterate over the nodes again
- var embeddedFlows = {};
- var newParentPath = parentPath ? parentPath + ":" + data.flow : data.flow;
-
+ // Iterate over the nodes again. Parse the data if they're embedded flow data.
+ // Assign each nodes to the parent flow data.
for (var key in nodes) {
var node = nodes[key];
- node.parentPath = newParentPath;
+ node.parent = data;
if (node.type == "flow" && node.flowData) {
- var graphModel = new azkaban.GraphModel();
-
- node.flowData.id = node.id;
- node.flowData.flowId = node.flowId;
- parseFlowData(node.flowData, graphModel, newParentPath);
- graphModel.set({id: node.id, flow: node.flowData, type: node.type, props: node.props});
- graphModel.set({isEmbedded: true});
- node.dataModel = graphModel;
+ processFlowData(node.flowData);
+ // Weird cycle. Evaluate whether we can instead unwrap these things.
+ node.flowData.node = node;
}
}
+ // Assign the node map and the edge list
console.log("data fetched");
- model.set({flow: data.flow});
- model.set({data: data});
- model.set({nodes: nodes});
- model.set({edges: edges});
- model.set({disabled: {}});
+ data.nodeMap = nodes;
+ data.edges = edges;
+ data.disabled = {};
}
var closeAllSubDisplays = function() {
$(".flowExtendedView").hide();
}
-var nodeClickCallback = function(event, model, type) {
+var nodeClickCallback = function(event, model, node) {
console.log("Node clicked callback");
+
var target = event.currentTarget;
- var jobId = target.jobid;
- var flowId = model.get("flow");
+ var type = node.type;
+ var flowId = node.flowId;
+ var jobId = node.id;
+
var requestURL = contextURL + "/manager?project=" + projectName + "&flow=" + flowId + "&job=" + jobId;
+ var menu = [];
- if (event.currentTarget.jobtype == "flow") {
+ if (type == "flow") {
var flowRequestURL = contextURL + "/manager?project=" + projectName + "&flow=" + event.currentTarget.flowId;
- menu = [
- {title: "View Flow...", callback: function() {openJobDisplayCallback(jobId, flowId, event)}},
+ if (node.expanded) {
+ menu = [{title: "Collapse Flow...", callback: function() {model.trigger("collapseFlow", node);}}];
+ }
+ else {
+ menu = [{title: "Expand Flow...", callback: function() {model.trigger("expandFlow", node);}}];
+ }
+
+ $.merge(menu, [
+ {title: "View Properties...", callback: function() {openJobDisplayCallback(jobId, flowId, event)}},
{break: 1},
{title: "Open Flow...", callback: function() {window.location.href=flowRequestURL;}},
{title: "Open Flow in New Window...", callback: function() {window.open(flowRequestURL);}},
@@ -164,17 +128,17 @@ var nodeClickCallback = function(event, model, type) {
{title: "Open Properties...", callback: function() {window.location.href=requestURL;}},
{title: "Open Properties in New Window...", callback: function() {window.open(requestURL);}},
{break: 1},
- {title: "Center Flow", callback: function() {model.trigger("centerNode", jobId)}}
- ];
+ {title: "Center Flow", callback: function() {model.trigger("centerNode", node);}}
+ ]);
}
else {
menu = [
- {title: "View Job...", callback: function() {openJobDisplayCallback(jobId, flowId, event)}},
+ {title: "View Properties...", callback: function() {openJobDisplayCallback(jobId, flowId, event)}},
{break: 1},
{title: "Open Job...", callback: function() {window.location.href=requestURL;}},
{title: "Open Job in New Window...", callback: function() {window.open(requestURL);}},
{break: 1},
- {title: "Center Job", callback: function() {model.trigger("centerNode", jobId)}}
+ {title: "Center Job", callback: function() {model.trigger("centerNode", node)}}
];
}
contextMenuView.show(event, menu);
@@ -183,6 +147,7 @@ var nodeClickCallback = function(event, model, type) {
var jobClickCallback = function(event, model) {
console.log("Node clicked callback");
var jobId = event.currentTarget.jobid;
+ var node = target.data;
var requestURL = contextURL + "/manager?project=" + projectName + "&flow=" + flowId + "&job=" + jobId;
var menu;
@@ -197,7 +162,7 @@ var jobClickCallback = function(event, model) {
{title: "Open Properties...", callback: function() {window.location.href=requestURL;}},
{title: "Open Properties in New Window...", callback: function() {window.open(requestURL);}},
{break: 1},
- {title: "Center Flow", callback: function() {model.trigger("centerNode", jobId)}}
+ {title: "Center Flow", callback: function() {model.trigger("centerNode", node)}}
];
}
else {
@@ -207,7 +172,7 @@ var jobClickCallback = function(event, model) {
{title: "Open Job...", callback: function() {window.location.href=requestURL;}},
{title: "Open Job in New Window...", callback: function() {window.open(requestURL);}},
{break: 1},
- {title: "Center Job", callback: function() {graphModel.trigger("centerNode", jobId)}}
+ {title: "Center Job", callback: function() {graphModel.trigger("centerNode", node)}}
];
}
contextMenuView.show(event, menu);
@@ -219,6 +184,7 @@ var edgeClickCallback = function(event, model) {
var graphClickCallback = function(event, model) {
console.log("Graph clicked callback");
+
var jobId = event.currentTarget.jobid;
var flowId = model.get("flowId");
var requestURL = contextURL + "/manager?project=" + projectName + "&flow=" + flowId;
@@ -232,3 +198,4 @@ var graphClickCallback = function(event, model) {
contextMenuView.show(event, menu);
}
+
src/web/js/azkaban.svg.graph.view.js 816(+392 -424)
diff --git a/src/web/js/azkaban.svg.graph.view.js b/src/web/js/azkaban.svg.graph.view.js
index 1bd78de..eecc5cc 100644
--- a/src/web/js/azkaban.svg.graph.view.js
+++ b/src/web/js/azkaban.svg.graph.view.js
@@ -1,36 +1,11 @@
-function hasClass(el, name)
-{
- var classes = el.getAttribute("class");
- if (classes == null) {
- return false;
- }
- return new RegExp('(\\s|^)'+name+'(\\s|$)').test(classes);
-}
-
-function addClass(el, name)
-{
- if (!hasClass(el, name)) {
- var classes = el.getAttribute("class");
- classes += classes ? ' ' + name : '' +name;
- el.setAttribute("class", classes);
- }
-}
-
-function removeClass(el, name)
-{
- if (hasClass(el, name)) {
- var classes = el.getAttribute("class");
- el.setAttribute("class", classes.replace(new RegExp('(\\s|^)'+name+'(\\s|$)'),' ').replace(/^\s+|\s+$/g, ''));
- }
-}
-
+$.namespace('azkaban');
azkaban.SvgGraphView = Backbone.View.extend({
events: {
- "click g" : "clickGraph",
- "contextmenu" : "handleRightClick",
- "contextmenu g" : "handleRightClick",
- "contextmenu polyline": "handleRightClick"
+
+ },
+ test: function() {
+ console.log("test");
},
initialize: function(settings) {
this.model.bind('change:selected', this.changeSelected, this);
@@ -40,7 +15,9 @@ azkaban.SvgGraphView = Backbone.View.extend({
this.model.bind('change:update', this.handleStatusUpdate, this);
this.model.bind('change:disabled', this.handleDisabledChange, this);
this.model.bind('change:updateAll', this.handleUpdateAllStatus, this);
-
+ this.model.bind('expandFlow', this.expandFlow, this);
+ this.model.bind('collapseFlow', this.collapseFlow, this);
+
this.graphMargin = settings.graphMargin ? settings.graphMargin : 25;
this.svgns = "http://www.w3.org/2000/svg";
this.xlinksn = "http://www.w3.org/1999/xlink";
@@ -48,91 +25,75 @@ azkaban.SvgGraphView = Backbone.View.extend({
var graphDiv = this.el[0];
var svg = $(this.el).find('svg')[0];
this.svgGraph = svg;
-
+ $(this.svgGraph).svg();
+ this.svg = $(svg).svg('get');
+
+ // Create mainG node
var gNode = document.createElementNS(this.svgns, 'g');
+ gNode.setAttribute("class", "main graph");
svg.appendChild(gNode);
this.mainG = gNode;
+
if (settings.rightClick) {
this.rightClick = settings.rightClick;
}
$(svg).svgNavigate();
+ var self = this;
+ if (self.rightClick && self.rightClick.graph) {
+ $(svg).on("contextmenu", function(evt) {
+ console.log("graph click");
+ var currentTarget = evt.currentTarget;
+
+ self.rightClick.graph(evt, self.model, currentTarget.data);
+ return false;
+ });
+ }
+
if (settings.render) {
this.render();
}
},
- initializeDefs: function(self) {
- var def = document.createElementNS(svgns, 'defs');
- def.setAttribute("id", "buttonDefs");
-
- // ArrowHead
- var arrowHeadMarker = document.createElementNS(svgns, 'marker');
- arrowHeadMarker.setAttribute("id", "triangle");
- arrowHeadMarker.setAttribute("viewBox", "0 0 10 10");
- arrowHeadMarker.setAttribute("refX", "5");
- arrowHeadMarker.setAttribute("refY", "5");
- arrowHeadMarker.setAttribute("markerUnits", "strokeWidth");
- arrowHeadMarker.setAttribute("markerWidth", "4");
- arrowHeadMarker.setAttribute("markerHeight", "3");
- arrowHeadMarker.setAttribute("orient", "auto");
- var path = document.createElementNS(svgns, 'polyline');
- arrowHeadMarker.appendChild(path);
- path.setAttribute("points", "0,0 10,5 0,10 1,5");
-
- def.appendChild(arrowHeadMarker);
-
- this.svgGraph.appendChild(def);
+ render: function() {
+ console.log("graph render");
+
+ this.graphBounds = this.renderGraph(this.model.get("data"), this.mainG);
+ this.resetPanZoom(0);
},
- render: function(self) {
+ renderGraph: function(data, g) {
console.log("graph render");
- // Clean everything
- while (this.mainG.lastChild) {
- this.mainG.removeChild(this.mainG.lastChild);
- }
-
- var data = this.model.get("data");
+ g.data = data;
var nodes = data.nodes;
- var edges = this.model.get("edges");
-
+ var edges = data.edges;
+ var nodeMap = data.nodeMap;
+
+ // Create a g node for edges, so that they're forced in the back.
+ var edgeG = this.svg.group(g);
if (nodes.length == 0) {
console.log("No results");
return;
};
-
- var bounds = {};
- this.nodes = {};
+
+ // Assign labels
for (var i = 0; i < nodes.length; ++i) {
- this.nodes[nodes[i].id] = nodes[i];
nodes[i].label = nodes[i].id;
}
- this.gNodes = {};
for (var i = 0; i < nodes.length; ++i) {
- this.drawNode(this, nodes[i]);
+ this.drawNode(this, nodes[i], g);
}
-
+
// layout
layoutGraph(nodes, edges, 10);
- this.moveNodes(bounds);
+ var bounds = this.calculateBounds(nodes);
+ this.moveNodes(nodes);
for (var i = 0; i < edges.length; ++i) {
- var inNodes = this.nodes[edges[i].to].inNodes;
- if (!inNodes) {
- inNodes = {};
- this.nodes[edges[i].to].inNodes = inNodes;
- }
- inNodes[edges[i].from] = this.nodes[edges[i].from];
-
- var outNodes = this.nodes[edges[i].from].outNodes;
- if (!outNodes) {
- outNodes = {};
- this.nodes[edges[i].from].outNodes = outNodes;
- }
- outNodes[edges[i].to] = this.nodes[edges[i].to];
-
- this.drawEdge(this, edges[i]);
+ edges[i].toNode = nodeMap[edges[i].to];
+ edges[i].fromNode = nodeMap[edges[i].from];
+ this.drawEdge(this, edges[i], edgeG);
}
this.model.set({"flowId":data.flowId, "edges": edges});
@@ -143,16 +104,56 @@ azkaban.SvgGraphView = Backbone.View.extend({
bounds.maxX = bounds.maxX ? bounds.maxX + margin : margin;
bounds.maxY = bounds.maxY ? bounds.maxY + margin : margin;
- this.assignInitialStatus(self);
+ this.assignInitialStatus(this, data);
- if (this.model.get("disabled")) {
+ if (data.disabled && data.disabled.length > 0) {
this.handleDisabledChange(self);
}
- else {
- this.model.set({"disabled":[]})
+
+/*
+ if (this.rightClick) {
+ var callbacks = this.rightClick;
+ var currentTarget = self.currentTarget;
+ if (callbacks.node && currentTarget.jobid) {
+ callbacks.node(self, this.model, currentTarget.nodeobj);
+ }
+ else if (callbacks.edge && (currentTarget.nodeName == "polyline" || currentTarget.nodeName == "line")) {
+ callbacks.edge(self, this.model);
+ }
+ else if (callbacks.graph) {
+ callbacks.graph(self, this.model);
+ }
+ return false;
}
- this.graphBounds = bounds;
- this.resetPanZoom(0);
+
+*/
+
+ var self = this;
+ if (self.rightClick) {
+ if (self.rightClick.node) {
+ // Proper children selectors don't work properly on svg
+ for (var i = 0; i < nodes.length; ++i) {
+ $(nodes[i].gNode).on("contextmenu", function(evt) {
+ console.log("node click");
+ var currentTarget = evt.currentTarget;
+ self.rightClick.node(evt, self.model, currentTarget.data);
+ return false;
+ });
+ }
+ }
+ if (this.rightClick.graph) {
+ $(g).on("contextmenu", function(evt) {
+ console.log("graph click");
+ var currentTarget = evt.currentTarget;
+
+ self.rightClick.graph(evt, self.model, currentTarget.data);
+ return false;
+ });
+ }
+
+ };
+
+ return bounds;
},
handleDisabledChange: function(evt) {
var disabledMap = this.model.get("disabled");
@@ -169,17 +170,14 @@ azkaban.SvgGraphView = Backbone.View.extend({
}
}
},
- assignInitialStatus: function(evt) {
- var data = this.model.get("data");
+ assignInitialStatus: function(evt, data) {
for (var i = 0; i < data.nodes.length; ++i) {
var updateNode = data.nodes[i];
- var g = this.gNodes[updateNode.id];
- if (updateNode.status) {
- addClass(g, updateNode.status);
- }
- else {
- addClass(g, "READY");
- }
+ var g = updateNode.gNode;
+ var initialStatus = updateNode.status ? updateNode.status : "READY";
+
+ addClass(g, initialStatus);
+ $(g).attr("title", initialStatus);
}
},
changeSelected: function(self) {
@@ -220,6 +218,7 @@ azkaban.SvgGraphView = Backbone.View.extend({
this.handleRemoveAllStatus(g);
addClass(g, updateNode.status);
+ $(g).attr("title", updateNode.status);
}
}
},
@@ -240,7 +239,7 @@ azkaban.SvgGraphView = Backbone.View.extend({
var callbacks = this.rightClick;
var currentTarget = self.currentTarget;
if (callbacks.node && currentTarget.jobid) {
- callbacks.node(self, this.model);
+ callbacks.node(self, this.model, currentTarget.nodeobj);
}
else if (callbacks.edge && (currentTarget.nodeName == "polyline" || currentTarget.nodeName == "line")) {
callbacks.edge(self, this.model);
@@ -253,367 +252,322 @@ azkaban.SvgGraphView = Backbone.View.extend({
return true;
},
- drawEdge: function(self, edge) {
- var svg = self.svgGraph;
+ drawEdge: function(self, edge, g) {
+ var svg = this.svg;
var svgns = self.svgns;
- var startNode = this.nodes[edge.from];
- var endNode = this.nodes[edge.to];
+ var startNode = edge.fromNode;
+ var endNode = edge.toNode;
- var startPointY = startNode.y + startNode.height/2 - 3;
- var endPointY = endNode.y - endNode.height/2 + 3;
- if (edge.guides) {
- var pointString = "" + startNode.x + "," + startPointY + " ";
+ var startPointY = startNode.y + startNode.height/2;
+ var endPointY = endNode.y - endNode.height/2;
+ if (edge.guides) {
+ // Create guide array
+ var pointArray = new Array();
+ pointArray.push([startNode.x, startPointY]);
for (var i = 0; i < edge.guides.length; ++i ) {
- edgeGuidePoint = edge.guides[i];
- pointString += edgeGuidePoint.x + "," + edgeGuidePoint.y + " ";
+ var edgeGuidePoint = edge.guides[i];
+ pointArray.push([edgeGuidePoint.x, edgeGuidePoint.y]);
}
-
- pointString += endNode.x + "," + endPointY;
- var polyLine = document.createElementNS(svgns, "polyline");
- polyLine.setAttribute("class", "edge");
- polyLine.setAttribute("points", pointString);
- polyLine.setAttribute("style", "fill:none;stroke-width:3");
- $(self.mainG).prepend(polyLine);
+ pointArray.push([endNode.x, endPointY]);
+
+ edge.line = svg.polyline(g, pointArray, {class:"edge", fill:"none"});
+ edge.line.data = edge;
+ edge.oldpoints = pointArray;
}
else {
- var line = document.createElementNS(svgns, 'line');
- line.setAttribute("class", "edge");
- line.setAttribute("x1", startNode.x);
- line.setAttribute("y1", startPointY);
- line.setAttribute("x2", endNode.x);
- line.setAttribute("y2", endPointY);
- line.setAttribute("style", "stroke-width:3");
- $(self.mainG).prepend(line);
+ edge.line = svg.line(g, startNode.x, startPointY, endNode.x, endPointY, {class:"edge"});
+ edge.line.data = edge;
}
},
- drawNode: function(self, node) {
+ drawNode: function(self, node, g) {
if (node.type == 'flow') {
- this.drawFlowNode(self, node);
+ this.drawFlowNode(self, node, g);
}
else {
- this.drawBoxNode(self,node);
- //this.drawCircleNode2(self,node);
+ this.drawBoxNode(self, node, g);
}
+//
+// var boundingBox = node.gNode.getBBox();
+// node.width = boundingBox.width;
+// node.height = boundingBox.height;
+// node.centerX = node.width/2;
+// node.centerY = node.height/2;
},
- moveNodes: function(bounds) {
- var nodes = this.nodes;
- var gNodes = this.gNodes;
-
- for (var key in nodes) {
- var node = nodes[key];
- var gNode = gNodes[node.id];
- var centerX = node.centerX;
- var centerY = node.centerY;
+ moveNodes: function(nodes) {
+ var svg = this.svg;
+ for (var i = 0; i < nodes.length; ++i) {
+ var node = nodes[i];
+ var gNode = node.gNode;
- gNode.setAttribute("transform", "translate(" + node.x + "," + node.y + ")");
- this.addBounds(bounds, {minX:node.x - centerX, minY: node.y - centerY, maxX: node.x + centerX, maxY: node.y + centerY});
+ svg.change(gNode, {"transform": translateStr(node.x, node.y)});
}
},
- drawFlowNode: function(self, node) {
- var svg = self.svgGraph;
- var svgns = self.svgns;
+ expandFlow: function(node) {
+ var svg = this.svg;
+ var gnode = node.gNode;
+ node.expanded = true;
- var height = 26;
- var nodeG = document.createElementNS(svgns, "g");
- nodeG.setAttribute("class", "jobnode");
- nodeG.setAttribute("font-family", "helvetica");
- nodeG.setAttribute("transform", "translate(" + node.x + "," + node.y + ")");
- this.gNodes[node.id] = nodeG;
-
- var innerG = document.createElementNS(svgns, "g");
- innerG.setAttribute("class", "nodebox");
-
- var rect = document.createElementNS(svgns, 'rect');
- rect.setAttribute("rx", 3);
- rect.setAttribute("ry", 5);
- rect.setAttribute("style", "width:inherit;stroke-opacity:1");
- //rect.setAttribute("class", "nodecontainer");
-
- var text = document.createElementNS(svgns, 'text');
- var textLabel = document.createTextNode(node.label);
- text.appendChild(textLabel);
- text.setAttribute("y", 1);
- text.setAttribute("height", 10);
-
- var flowIdText = document.createElementNS(svgns, 'text');
- var flowIdLabel = document.createTextNode(node.flowId);
- flowIdText.appendChild(flowIdLabel);
- flowIdText.setAttribute("y", 11);
- flowIdText.setAttribute("font-size", 8);
+ var innerG = gnode.innerG;
+ var borderRect = innerG.borderRect;
+ var labelG = innerG.labelG;
+ var flowData = node.flowData;
+
+ var bbox;
+ if (!innerG.expandedFlow) {
+ var topmargin= 30, bottommargin=5;
+ var hmargin = 10;
+
+ var expandedFlow = svg.group(innerG, "", {class: "expandedGraph"});
+ this.renderGraph(flowData, expandedFlow);
+ innerG.expandedFlow = expandedFlow;
+ removeClass(innerG, "collapsed");
+ addClass(innerG, "expanded");
+ node.expandedWidth = node.width;
+ node.expandedHeight = node.height;
+ }
+ else {
+ $(innerG.expandedFlow).show();
+ removeClass(innerG, "collapsed");
+ addClass(innerG, "expanded");
+ node.width = node.expandedWidth;
+ node.height = node.expandedHeight;
+ }
- var iconHeight = 20;
- var iconWidth = 21;
- var iconMargin = 4;
- var iconNode = document.createElementNS(svgns, 'image');
- iconNode.setAttribute("width", iconHeight);
- iconNode.setAttribute("height", iconWidth);
- iconNode.setAttribute("x", 0);
- iconNode.setAttribute("y", -10);
- iconNode.setAttributeNS('http://www.w3.org/1999/xlink', "xlink:href", contextURL + "/images/graph-icon.png");
-
- innerG.appendChild(rect);
- innerG.appendChild(text);
- innerG.appendChild(flowIdText);
- innerG.appendChild(iconNode);
- innerG.jobid = node.id;
- innerG.jobtype = "flow";
- innerG.flowId = node.flowId;
- innerG.nodeobj = node;
-
- nodeG.appendChild(innerG);
- self.mainG.appendChild(nodeG);
-
- var horizontalMargin = 8;
- var verticalMargin = 2;
+ this.relayoutFlow(node);
- // Need to get text width after attaching to SVG.
- var subLabelTextLength = flowIdText.getComputedTextLength();
-
- var computeTextLength = text.getComputedTextLength();
- var computeTextHeight = 22;
-
- var width = computeTextLength > subLabelTextLength ? computeTextLength : subLabelTextLength;
- width += iconWidth + iconMargin;
- var halfWidth = width/2;
- var halfHeight = height/2;
-
- // Margin for surrounding box.
- var boxWidth = width + horizontalMargin * 2;
- var boxHeight = height + verticalMargin * 2;
-
- node.width = boxWidth;
- node.height = boxHeight;
- node.centerX = boxWidth/2;
- node.centerY = boxHeight/2;
-
- var textXOffset = -halfWidth + iconWidth + iconMargin;
- iconNode.setAttribute("x", -halfWidth);
- text.setAttribute("x", textXOffset);
- flowIdText.setAttribute("x",textXOffset);
- rect.setAttribute("x", -node.centerX);
- rect.setAttribute("y", -node.centerY);
- rect.setAttribute("width", node.width);
- rect.setAttribute("height", node.height);
-
- nodeG.setAttribute("class", "node");
- nodeG.jobid=node.id;
- nodeG.jobobj=node;
+ var bounds = this.calculateBounds(this.model.get("data").nodes);
+
+ var margin = this.graphMargin;
+ bounds.minX = bounds.minX ? bounds.minX - margin : -margin;
+ bounds.minY = bounds.minY ? bounds.minY - margin : -margin;
+ bounds.maxX = bounds.maxX ? bounds.maxX + margin : margin;
+ bounds.maxY = bounds.maxY ? bounds.maxY + margin : margin;
+ this.graphBounds = bounds;
},
- drawBoxNode: function(self, node) {
- var svg = self.svgGraph;
- var svgns = self.svgns;
+ collapseFlow: function(node) {
+ console.log("Collapsing flow");
+ var svg = this.svg;
+ var gnode = node.gNode;
+ node.expanded = false;
+
+ var innerG = gnode.innerG;
+ var borderRect = innerG.borderRect;
+ var labelG = innerG.labelG;
+ var flowData = node.flowData;
- var height = 18;
- var nodeG = document.createElementNS(svgns, "g");
- nodeG.setAttribute("class", "jobnode");
- nodeG.setAttribute("font-family", "helvetica");
- nodeG.setAttribute("transform", "translate(" + node.x + "," + node.y + ")");
- this.gNodes[node.id] = nodeG;
-
- var innerG = document.createElementNS(svgns, "g");
- innerG.setAttribute("class", "nodebox");
-
- var rect = document.createElementNS(svgns, 'rect');
- rect.setAttribute("rx", 3);
- rect.setAttribute("ry", 5);
- rect.setAttribute("style", "width:inherit;stroke-opacity:1");
- //rect.setAttribute("class", "nodecontainer");
-
- var text = document.createElementNS(svgns, 'text');
- var textLabel = document.createTextNode(node.label);
- text.appendChild(textLabel);
- text.setAttribute("y", 6);
- text.setAttribute("height", 10);
+ removeClass(innerG, "expanded");
+ addClass(innerG, "collapsed");
- //this.addBounds(bounds, {minX:node.x - xOffset, minY: node.y - yOffset, maxX: node.x + xOffset, maxY: node.y + yOffset});
+ node.height = node.collapsedHeight;
+ node.width = node.collapsedWidth;
- innerG.appendChild(rect);
- innerG.appendChild(text);
- innerG.jobid = node.id;
- innerG.nodeobj = node;
-
- nodeG.appendChild(innerG);
- self.mainG.appendChild(nodeG);
+ $(innerG.expandedFlow).hide();
+ this.relayoutFlow(node);
+
+ var bounds = this.calculateBounds(this.model.get("data").nodes);
+
+ var margin = this.graphMargin;
+ bounds.minX = bounds.minX ? bounds.minX - margin : -margin;
+ bounds.minY = bounds.minY ? bounds.minY - margin : -margin;
+ bounds.maxX = bounds.maxX ? bounds.maxX + margin : margin;
+ bounds.maxY = bounds.maxY ? bounds.maxY + margin : margin;
+ this.graphBounds = bounds;
- var horizontalMargin = 8;
- var verticalMargin = 2;
-
- // Need to get text width after attaching to SVG.
- var computeText = text.getComputedTextLength();
- var computeTextHeight = 22;
- var halfWidth = computeText/2;
- var halfHeight = height/2;
-
- // Margin for surrounding box.
- var boxWidth = computeText + horizontalMargin * 2;
- var boxHeight = height + verticalMargin * 2;
-
- node.width = boxWidth;
- node.height = boxHeight;
- node.centerX = boxWidth/2;
- node.centerY = boxHeight/2;
-
- text.setAttribute("x", -halfWidth);
- rect.setAttribute("x", -node.centerX);
- rect.setAttribute("y", -node.centerY);
- rect.setAttribute("width", node.width);
- rect.setAttribute("height", node.height);
-
- nodeG.setAttribute("class", "node");
- nodeG.jobid=node.id;
- nodeG.j=node;
},
- drawCircleNode: function(self, node, bounds) {
- var svg = self.svgGraph;
- var svgns = self.svgns;
+ relayoutFlow: function(node) {
+ if (node.expanded) {
+ this.layoutExpandedFlowNode(node);
+ }
- var xOffset = 10;
- var yOffset = 10;
-
- var height = 18;
- var nodeG = document.createElementNS(svgns, "g");
- nodeG.setAttribute("class", "jobnode");
- nodeG.setAttribute("font-family", "helvetica");
- nodeG.setAttribute("transform", "translate(" + node.x + "," + node.y + ")");
- this.gNodes[node.id] = nodeG;
-
- var innerG = document.createElementNS(svgns, "g");
- innerG.setAttribute("transform", "translate(-10,-10)");
+ var parent = node.parent;
+ if (parent) {
+ layoutGraph(parent.nodes, parent.edges, 10);
+ if (parent.node) {
+ this.relayoutFlow(parent.node);
+ }
+ }
- var circle = document.createElementNS(svgns, 'circle');
- circle.setAttribute("cy", 10);
- circle.setAttribute("cx", 10);
- circle.setAttribute("r", 12);
- circle.setAttribute("style", "width:inherit;stroke-opacity:1");
+ // Move all points again.
+ this.moveNodeEdges(parent.nodes, parent.edges);
+ this.animateExpandedFlowNode(node, 250);
- //circle.setAttribute("class", "border");
- //circle.setAttribute("class", "nodecontainer");
+ },
+ moveNodeEdges: function(nodes, edges) {
+ var svg = this.svg;
- var text = document.createElementNS(svgns, 'text');
- var textLabel = document.createTextNode(node.label);
- text.appendChild(textLabel);
- text.setAttribute("x", 0);
- text.setAttribute("y", 0);
+ for (var i = 0; i < nodes.length; ++i) {
+ var node = nodes[i];
+ var gNode = node.gNode;
+
+ $(gNode).animate({"svgTransform": translateStr(node.x, node.y)}, 250);
+ }
+
+ for (var j = 0; j < edges.length; ++j) {
+ var edge = edges[j];
+ var startNode = edge.fromNode;
+ var endNode = edge.toNode;
+ var line = edge.line;
+
+ var startPointY = startNode.y + startNode.height/2;
+ var endPointY = endNode.y - endNode.height/2;
+
+ if (edge.guides) {
+ // Create guide array
+ var pointArray = new Array();
+ pointArray.push([startNode.x, startPointY]);
+ for (var i = 0; i < edge.guides.length; ++i ) {
+ var edgeGuidePoint = edge.guides[i];
+ pointArray.push([edgeGuidePoint.x, edgeGuidePoint.y]);
+ }
+ pointArray.push([endNode.x, endPointY]);
+
+ animatePolylineEdge(svg, edge, pointArray, 250);
+ edge.oldpoints = pointArray;
+ }
+ else {
+ $(line).animate({svgX1: startNode.x, svgY1: startPointY, svgX2: endNode.x, svgY2: endPointY});
+ }
+ }
- //this.addBounds(bounds, {minX:node.x - xOffset, minY: node.y - yOffset, maxX: node.x + xOffset, maxY: node.y + yOffset});
-
- var backRect = document.createElementNS(svgns, 'rect');
- backRect.setAttribute("x", 0);
- backRect.setAttribute("y", 2);
- backRect.setAttribute("class", "backboard");
- backRect.setAttribute("width", 10);
- backRect.setAttribute("height", 15);
-
- innerG.appendChild(circle);
- innerG.appendChild(backRect);
- innerG.appendChild(text);
- innerG.jobid = node.id;
+ },
+ calculateBounds: function(nodes) {
+ var bounds = {};
+ var node = nodes[0];
+ bounds.minX = node.x - 10;
+ bounds.minY = node.y - 10;
+ bounds.maxX = node.x + 10;
+ bounds.maxY = node.y + 10;
- nodeG.appendChild(innerG);
- self.mainG.appendChild(nodeG);
+ for (var i = 0; i < nodes.length; ++i) {
+ node = nodes[i];
+ var centerX = node.width/2;
+ var centerY = node.height/2;
+ var minX = node.x - centerX;
+ var minY = node.y - centerY;
+ var maxX = node.x + centerX;
+ var maxY = node.y + centerY;
+
+ bounds.minX = Math.min(bounds.minX, minX);
+ bounds.minY = Math.min(bounds.minY, minY);
+ bounds.maxX = Math.max(bounds.maxX, maxX);
+ bounds.maxY = Math.max(bounds.maxY, maxY);
+ }
+ bounds.width = bounds.maxX - bounds.minX;
+ bounds.height = bounds.maxY - bounds.minY;
+ return bounds;
+ },
+ drawBoxNode: function(self, node, g) {
+ var svg = this.svg;
var horizontalMargin = 8;
var verticalMargin = 2;
- // Need to get text width after attaching to SVG.
- var computeText = text.getComputedTextLength();
- var halfWidth = computeText/2;
- text.setAttribute("x", -halfWidth + 10);
- backRect.setAttribute("x", -halfWidth);
- backRect.setAttribute("width", computeText + 20);
-
- // Margin for surrounding box.
- var boxWidth = computeText + horizontalMargin * 2;
- var boxHeight = height + verticalMargin * 2;
-
- node.width = boxWidth;
- node.height = boxHeight;
- node.centerX = boxWidth/2;
- node.centerY = boxHeight/2;
-
- nodeG.setAttribute("class", "node");
- nodeG.nodeobj=node;
- nodeG.jobid=node.id;
- },
- drawCircleNode2: function(self, node, bounds) {
- var svg = self.svgGraph;
- var svgns = self.svgns;
-
- var xOffset = 10;
- var yOffset = 10;
-
- var height = 18;
- var nodeG = document.createElementNS(svgns, "g");
- nodeG.setAttribute("class", "jobnode");
- nodeG.setAttribute("font-family", "helvetica");
- nodeG.setAttribute("transform", "translate(" + node.x + "," + node.y + ")");
- this.gNodes[node.id] = nodeG;
- var innerG = document.createElementNS(svgns, "g");
- innerG.setAttribute("transform", "translate(-10,-10)");
+ var nodeG = svg.group(g, "", {class:"node jobnode"});
- var circle = document.createElementNS(svgns, 'circle');
- circle.setAttribute("cy", 10);
- circle.setAttribute("cx", 10);
- circle.setAttribute("r", 12);
- circle.setAttribute("style", "width:inherit;stroke-opacity:1;stroke:rgb(100,100,100);stroke-width:5");
- //circle.setAttribute("class", "border");
- //circle.setAttribute("class", "nodecontainer");
-
- var text = document.createElementNS(svgns, 'text');
- var textLabel = document.createTextNode(node.label);
- text.appendChild(textLabel);
- text.setAttribute("x", 0);
- text.setAttribute("y", 0);
+ var innerG = svg.group(nodeG, "", {class:"nodebox"});
+ var borderRect = svg.rect(innerG, 0, 0, 10, 10, 3, 3, {class: "border"});
+ var jobNameText = svg.text(innerG, horizontalMargin, 16, node.label);
+ nodeG.innerG = innerG;
+ innerG.borderRect = borderRect;
- //this.addBounds(bounds, {minX:node.x - xOffset, minY: node.y - yOffset, maxX: node.x + xOffset, maxY: node.y + yOffset});
+ var labelBBox = jobNameText.getBBox();
+ var totalWidth = labelBBox.width + 2*horizontalMargin;
+ var totalHeight = labelBBox.height + 2*verticalMargin;
+ svg.change(borderRect, {width: totalWidth, height: totalHeight});
+ svg.change(jobNameText, {y: (totalHeight + labelBBox.height)/2 - 3});
+ svg.change(innerG, {transform: translateStr(-totalWidth/2, -totalHeight/2)});
+
+ node.width=totalWidth;
+ node.height=totalHeight;
+
+ node.gNode = nodeG;
+ nodeG.data = node;
+ },
+ drawFlowNode: function(self, node, g) {
+ var svg = this.svg;
- var backRect = document.createElementNS(svgns, 'rect');
- backRect.setAttribute("x", 0);
- backRect.setAttribute("y", 2);
- backRect.setAttribute("class", "backboard");
- backRect.setAttribute("width", 10);
- backRect.setAttribute("height", 15);
+ // Base flow node
+ var nodeG = svg.group(g, "", {"class": "node flownode"});
- innerG.appendChild(circle);
- innerG.appendChild(backRect);
-// innerG.appendChild(text);
- innerG.jobid = node.id;
+ // Create all the elements
+ var innerG = svg.group(nodeG, "", {class: "nodebox collapsed"});
+ var borderRect = svg.rect(innerG, 0, 0, 10, 10, 3, 3, {class: "flowborder"});
+
+ // Create label
+ var labelG = svg.group(innerG);
+ var iconHeight = 20;
+ var iconWidth = 21;
+ var textOffset = iconWidth + 4;
+ var jobNameText = svg.text(labelG, textOffset, 1, node.label);
+ var flowIdText = svg.text(labelG, textOffset, 11, node.flowId, {"font-size": 8})
+ var tempLabelG = labelG.getBBox();
+ var iconImage = svg.image(labelG, 0, -iconHeight/2, iconWidth, iconHeight, contextURL + "/images/graph-icon.png", {});
- nodeG.appendChild(innerG);
- self.mainG.appendChild(nodeG);
+ // Assign key values to make searching quicker
+ node.gNode=nodeG;
+ nodeG.data=node;
-
+ // Do this because jquery svg selectors don't work
+ nodeG.innerG = innerG;
+ innerG.borderRect = borderRect;
+ innerG.labelG = labelG;
+
+ // Layout everything in the node
+ this.layoutFlowNode(self, node);
+ },
+ layoutFlowNode: function(self, node) {
+ var svg = this.svg;
var horizontalMargin = 8;
var verticalMargin = 2;
- // Need to get text width after attaching to SVG.
- var computeText = 150;
- var halfWidth = computeText/2;
- text.setAttribute("x", -halfWidth + 10);
- backRect.setAttribute("x", -halfWidth);
- backRect.setAttribute("width", computeText + 20);
-
- // Margin for surrounding box.
- var boxWidth = computeText + horizontalMargin * 2;
- var boxHeight = height + verticalMargin * 2;
-
-// innerG.removeChild(text);
-
- node.width = boxWidth;
- node.height = boxHeight;
- node.centerX = boxWidth/2;
- node.centerY = boxHeight/2;
-
- nodeG.setAttribute("class", "node");
- nodeG.jobid=node.id;
+
+ var gNode = node.gNode;
+ var innerG = gNode.innerG;
+ var borderRect = innerG.borderRect;
+ var labelG = innerG.labelG;
+
+ var labelBBox = labelG.getBBox();
+ var totalWidth = labelBBox.width + 2*horizontalMargin;
+ var totalHeight = labelBBox.height + 2*verticalMargin;
+
+ svg.change(labelG, {transform: translateStr(horizontalMargin, labelBBox.height/2 + verticalMargin)});
+ svg.change(innerG, {transform: translateStr(-totalWidth/2, -totalHeight/2)});
+ svg.change(borderRect, {width: totalWidth, height: totalHeight});
+
+ node.height = totalHeight;
+ node.width = totalWidth;
+ node.collapsedHeight = totalHeight;
+ node.collapsedWidth = totalWidth;
+ },
+ layoutExpandedFlowNode: function(node) {
+ var svg = this.svg;
+ var topmargin= 30, bottommargin=5;
+ var hmargin = 10;
+
+ var gNode = node.gNode;
+ var innerG = gNode.innerG;
+ var borderRect = innerG.borderRect;
+ var labelG = innerG.labelG;
+ var expandedFlow = innerG.expandedFlow;
+
+ var bound = this.calculateBounds(node.flowData.nodes);
+
+ node.height = bound.height + topmargin + bottommargin;
+ node.width = bound.width + hmargin*2;
+ svg.change(expandedFlow, {transform: translateStr(-bound.minX + hmargin, -bound.minY + topmargin)});
+ //$(innerG).animate({svgTransform: translateStr(-node.width/2, -node.height/2)}, 50);
+ //$(borderRect).animate({svgWidth: node.width, svgHeight: node.height}, 50);
},
- addBounds: function(toBounds, addBounds) {
- toBounds.minX = toBounds.minX ? Math.min(toBounds.minX, addBounds.minX) : addBounds.minX;
- toBounds.minY = toBounds.minY ? Math.min(toBounds.minY, addBounds.minY) : addBounds.minY;
- toBounds.maxX = toBounds.maxX ? Math.max(toBounds.maxX, addBounds.maxX) : addBounds.maxX;
- toBounds.maxY = toBounds.maxY ? Math.max(toBounds.maxY, addBounds.maxY) : addBounds.maxY;
+ animateExpandedFlowNode: function(node, time) {
+ var gNode = node.gNode;
+ var innerG = gNode.innerG;
+ var borderRect = innerG.borderRect;
+
+ $(innerG).animate({svgTransform: translateStr(-node.width/2, -node.height/2)}, time);
+ $(borderRect).animate({svgWidth: node.width, svgHeight: node.height}, time);
+ $(borderRect).animate({svgFill: 'white'}, time);
},
resetPanZoom : function(duration) {
var bounds = this.graphBounds;
@@ -621,18 +575,32 @@ azkaban.SvgGraphView = Backbone.View.extend({
this.panZoom(param);
},
- centerNode: function(jobId) {
- var node = this.nodes[jobId];
-
- var offset = 150;
- var widthHeight = offset*2;
- var x = node.x - offset;
- var y = node.y - offset;
+ centerNode: function(node) {
+ // The magic of affine transformation.
+ // Multiply the inverse root matrix with the current matrix to get the node position.
+ // Rather do this than to traverse backwards through the scene graph.
+ var ctm = node.gNode.getCTM();
+ var transform = node.gNode.getTransformToElement();
+ var globalCTM = this.mainG.getCTM().inverse();
+ var otherTransform = globalCTM.multiply(ctm);
+ // Also a beauty of affine transformation. The translate is always the left most column of the matrix.
+ var x = otherTransform.e - node.width/2;
+ var y = otherTransform.f - node.height/2;
+
+ this.panZoom({x: x, y: y, width: node.width, height: node.height});
+ },
+ globalNodePosition: function(gNode) {
+ if (node.parent.node) {
- this.panZoom({x: x, y: y, width: widthHeight, height: widthHeight});
+ var parentPos = this.globalNodePosition(node.parent.node);
+ return {x: parentPos.x + node.x, y: parentPos.y + node.y};
+ }
+ else {
+ return {x: node.x, y: node.y};
+ }
},
panZoom: function(params) {
params.maxScale = 2;
$(this.svgGraph).svgNavigate("transformToBox", params);
}
-});
\ No newline at end of file
+});
src/web/js/svgutils.js 70(+70 -0)
diff --git a/src/web/js/svgutils.js b/src/web/js/svgutils.js
new file mode 100644
index 0000000..33dea78
--- /dev/null
+++ b/src/web/js/svgutils.js
@@ -0,0 +1,70 @@
+function hasClass(el, name)
+{
+ var classes = el.getAttribute("class");
+ if (classes == null) {
+ return false;
+ }
+ return new RegExp('(\\s|^)'+name+'(\\s|$)').test(classes);
+}
+
+function addClass(el, name)
+{
+ if (!hasClass(el, name)) {
+ var classes = el.getAttribute("class");
+ if (classes) {
+ classes += ' ' + name;
+ }
+ else {
+ classes = name;
+ }
+ el.setAttribute("class", classes);
+ }
+}
+
+function removeClass(el, name)
+{
+ if (hasClass(el, name)) {
+ var classes = el.getAttribute("class");
+ el.setAttribute("class", classes.replace(new RegExp('(\\s|^)'+name+'(\\s|$)'),' ').replace(/^\s+|\s+$/g, ''));
+ }
+}
+
+function translateStr(x, y) {
+ return "translate(" + x + "," + y + ")";
+}
+
+function animatePolylineEdge(svg, edge, newPoints, time) {
+ var oldEdgeGuides = edge.oldpoints;
+
+ var interval = 10;
+ var numsteps = time/interval;
+
+ var deltaEdges = new Array();
+ for (var i=0; i < oldEdgeGuides.length; ++i) {
+ var startPoint = oldEdgeGuides[i];
+ var endPoint = newPoints[i];
+
+ var deltaX = (endPoint[0] - startPoint[0])/numsteps;
+ var deltaY = (endPoint[1] - startPoint[1])/numsteps;
+ deltaEdges.push([deltaX, deltaY]);
+ }
+
+ animatePolyLineLoop(svg, edge, oldEdgeGuides, deltaEdges, numsteps, 25);
+}
+
+function animatePolyLineLoop(svg, edge, lastPoints, deltaEdges, step, time) {
+ for (var i=0; i < deltaEdges.length; ++i) {
+ lastPoints[i][0] += deltaEdges[i][0];
+ lastPoints[i][1] += deltaEdges[i][1];
+ }
+
+ svg.change(edge.line, {points: lastPoints});
+ if (step > 0) {
+ setTimeout(
+ function(){
+ animatePolyLineLoop(svg, edge, lastPoints, deltaEdges, step - 1);
+ },
+ time
+ );
+ }
+}
\ No newline at end of file