tez-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From jeag...@apache.org
Subject tez git commit: TEZ-1973. Dag View (Sreenath Somarajapuram via jeagles)
Date Fri, 30 Jan 2015 16:53:34 GMT
Repository: tez
Updated Branches:
  refs/heads/master a1c851897 -> 1784f0150


TEZ-1973. Dag View (Sreenath Somarajapuram via jeagles)


Project: http://git-wip-us.apache.org/repos/asf/tez/repo
Commit: http://git-wip-us.apache.org/repos/asf/tez/commit/1784f015
Tree: http://git-wip-us.apache.org/repos/asf/tez/tree/1784f015
Diff: http://git-wip-us.apache.org/repos/asf/tez/diff/1784f015

Branch: refs/heads/master
Commit: 1784f0150a5decb235b77716cc2eb9b418a5d60a
Parents: a1c8518
Author: Jonathan Eagles <jeagles@gmail.com>
Authored: Fri Jan 30 10:53:13 2015 -0600
Committer: Jonathan Eagles <jeagles@gmail.com>
Committed: Fri Jan 30 10:53:13 2015 -0600

----------------------------------------------------------------------
 CHANGES.txt                                     |   1 +
 .../scripts/components/dag-view-component.js    |  78 ++
 .../components/dag-view/data-processor.js       | 683 +++++++++++++++
 .../scripts/components/dag-view/graph-view.js   | 870 +++++++++++++++++++
 .../app/scripts/components/dag-view/tip.js      | 190 ++++
 .../scripts/controllers/dag-view-controller.js  |  94 ++
 .../app/scripts/controllers/dag_controller.js   |   1 +
 .../app/scripts/controllers/dags_controller.js  |   2 +-
 .../main/webapp/app/scripts/helpers/dialogs.js  |  11 +-
 .../webapp/app/scripts/helpers/fullscreen.js    |  55 ++
 .../src/main/webapp/app/scripts/helpers/misc.js |   7 +
 .../app/scripts/mixins/column-selector-mixin.js |   4 +-
 .../app/scripts/models/TimelineRestAdapter.js   |   1 +
 .../src/main/webapp/app/scripts/models/dag.js   |   1 +
 tez-ui/src/main/webapp/app/scripts/router.js    |   1 +
 tez-ui/src/main/webapp/app/styles/dag-view.less | 347 ++++++++
 tez-ui/src/main/webapp/app/styles/main.less     |   1 +
 .../app/templates/components/dag-view.hbs       | 108 +++
 .../src/main/webapp/app/templates/dag/view.hbs  |  29 +
 19 files changed, 2479 insertions(+), 5 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/tez/blob/1784f015/CHANGES.txt
----------------------------------------------------------------------
diff --git a/CHANGES.txt b/CHANGES.txt
index f1d67d0..c7ab55e 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -55,6 +55,7 @@ Release 0.6.1: Unreleased
 INCOMPATIBLE CHANGES
 
 ALL CHANGES:
+  TEZ-1973. Dag View
   TEZ-2010. History payload generated from conf has ${var} placeholders.
   TEZ-1946. Tez UI: add source & sink views, add counters to vertices/all task views.
   TEZ-1987. Tez UI non-standalone mode uses invalid protocol.

http://git-wip-us.apache.org/repos/asf/tez/blob/1784f015/tez-ui/src/main/webapp/app/scripts/components/dag-view-component.js
----------------------------------------------------------------------
diff --git a/tez-ui/src/main/webapp/app/scripts/components/dag-view-component.js b/tez-ui/src/main/webapp/app/scripts/components/dag-view-component.js
new file mode 100644
index 0000000..72eb0ed
--- /dev/null
+++ b/tez-ui/src/main/webapp/app/scripts/components/dag-view-component.js
@@ -0,0 +1,78 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * 'License'); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+App.DagViewComponent = Em.Component.extend({
+  layoutName: 'components/dag-view',
+
+  classNames: ['dag-view-container'],
+
+  errMessage: null,
+
+  isHorizontal: false,
+  hideAdditionals: false,
+  isFullscreen: false,
+
+  _onOrientationChange: function () {
+  }.observes('isHorizontal'),
+
+  _onTglAdditionals: function () {
+    App.DagViewComponent.graphView.additionalDisplay(this.get('hideAdditionals'));
+  }.observes('hideAdditionals'),
+
+  _onTglFullScreen: function () {
+    App.Helpers.fullscreen.toggle(this.get('element'));
+  }.observes('isFullscreen'),
+
+  actions: {
+    tglOrientation: function() {
+      var isTopBottom = App.DagViewComponent.graphView.toggleLayouts();
+      this.set('isHorizontal', !isTopBottom);
+    },
+    tglAdditionals: function() {
+      this.set('hideAdditionals', !this.get('hideAdditionals'));
+    },
+    fullscreen: function () {
+      this.set('isFullscreen', !this.get('isFullscreen'));
+    },
+    fitGraph: function () {
+      App.DagViewComponent.graphView.fitGraph();
+    },
+    configure: function () {
+      this.sendAction('configure');
+    }
+  },
+
+  didInsertElement: function () {
+    var result = App.DagViewComponent.dataProcessor.graphifyData(this.get('data')),
+        maxBreadth = 0;
+
+    if(typeof result === "string") {
+      this.set('errMessage', result);
+    }
+    else {
+      App.DagViewComponent.graphView.create(
+        this,
+        this.get('element'),
+        result
+      );
+    }
+  }
+
+});
+
+Em.Handlebars.helper('dag-view-component', App.DagViewComponent);
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/tez/blob/1784f015/tez-ui/src/main/webapp/app/scripts/components/dag-view/data-processor.js
----------------------------------------------------------------------
diff --git a/tez-ui/src/main/webapp/app/scripts/components/dag-view/data-processor.js b/tez-ui/src/main/webapp/app/scripts/components/dag-view/data-processor.js
new file mode 100644
index 0000000..fbbaec1
--- /dev/null
+++ b/tez-ui/src/main/webapp/app/scripts/components/dag-view/data-processor.js
@@ -0,0 +1,683 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * 'License'); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * The data processing part of Dag View.
+ *
+ * Converts raw DAG-plan into an internal data representation as shown below.
+ * Data processor exposes just a functions and an enum to the outside world, everything else
+ * happens inside the main closure:
+ *   - types (Enum of node types)
+ *   - graphifyData
+ *
+ * Links, Edges:
+ * --------------
+ * d3 layout & graph-view uses the term links, and dag-plan uses edges. Hence you would
+ * see both getting used in this file.
+ *
+ * Graphify Data
+ * -------------
+ *  graphifyData function is a translator that translates the dagPlan object send by timeline server
+ * into another object that graph-view and in turn d3.layout.tree can digest.
+ *
+ * Input object(dag-plan as it is from the timeline server):
+ *  {
+ *    dagName, version,
+ *    vertices: [ // Array of vertex objects with following properties
+ *      {
+ *        vertexName, processorClass, outEdgeIds {Array}, additionalInputs {Array}
+ *      }
+ *    ],
+ *    edges: [ // Array of edge objects with following properties
+ *      {
+ *        edgeId, inputVertexName, outputVertexName, dataMovementType, dataSourceType
+ *        schedulingType, edgeSourceClass, edgeDestinationClass
+ *      }
+ *    ],
+ *    vertexGroups: [ // Array of vectorGroups objects with following properties
+ *      {
+ *        groupName, groupMembers {Array}, edgeMergedInputs {Array}
+ *      }
+ *    ]
+ *  }
+ *
+ * Output object:
+ *  We are having a graph that must be displayed like a tree. Hence data processor was created
+ *  to make a tree structure out of the available data. The tree structure is made by creating
+ *  DataNodes instances and populating their children array with respective child DataNodes
+ *   - tree: Represents the tree structure with each node being a DataNodes instance
+ *   - links: Represents the connections between the nodes to create the graph
+ *    {
+ *      tree: { // This object points to the RootDataNode instance
+ *        children {Array} // Array of DataNodes under the node, as expected by d3.layout.tree
+ *        + Other custom properties including data that needs to be displayed
+ *      }
+ *      links: [ // An array of all links in the tree
+ *        {
+ *          sourceId // Source vertex name
+ *          targetId // Target vertex name
+ *          + Other custom properties including data to be displayed
+ *        }
+ *      ]
+ *      maxDepth, inputCount
+ *    }
+ *
+ * Data Nodes:
+ * -----------
+ *  To make the implementation simpler each node in the graph will be represented as an
+ *  instance of any of the 4 inherited classes of Data Node abstract class.
+ *  DataNode
+ *    |-- RootDataNode
+ *    |-- VertexDataNode
+ *    |-- InputDataNode
+ *    +-- OutputDataNode
+ *
+ * Extra Nodes:
+ * ------------
+ * Root Node (Invisible):
+ *  Dag view support very complex DAGs, even those without interconnections and backward links.
+ *  Hence to fit it into a tree layout I have inserted an invisible root node.
+ *
+ * Dummy Node (Invisible):
+ *  Sinks of a vertex are added at the same level of its parent node, Hence to ensure that all 
+ *  nodes come under the root, a dummy node was added as the child of the root. The visible tree
+ *  would be added as child of dummy node.
+ *  Dummy also ensures the view symmetry when multiple outputs are present at the dummy level.
+ *
+ * Sample Structure, inverted tree representation:
+ *
+ *            As in the view
+ *
+ *               Source_m1
+ *                  |
+ *   Source_m2      M1----------+
+ *      |           |           |
+ *      +-----------M2      Sink_M1
+ *                  |
+ *      +-----------R1----------+
+ *      |                       |
+ *   Sink1_R1               Sink2_R1
+ *
+ *
+ *        Internal representation
+ *
+ *               Source_m1
+ *                  |
+ *   Source_m2      M1
+ *      |           |
+ *      +-----------M2      Sink_M1
+ *                  |           |
+ *                  R1----------+
+ *                  |
+ *   Sink1_R1     Dummy     Sink2_R1
+ *      |           |           |
+ *      +-----------+-----------+
+ *                  |
+ *                 Root
+ *
+ *     Internal data representation
+ *
+ *     Root
+ *      |
+ *      +-- children[Sink1_R1, Dummy, Sink2_R1]
+ *                              |
+ *                              +-- children[R1]
+ *                                            |
+ *                                            +-- children[M2, Sink_M1]
+ *                                                          |
+ *                                                          +-- children[Source_m2, M1]
+ *                                                                                   |
+ *                                                                                   +-- children[Source_m1]
+ *
+ * Steps:
+ * ------
+ * The job is done in 4 steps, and is modularized using 4 separate recursive functions.
+ * 1. _treefyData      : Get the tree structure in place with vertices and inputs/sources
+ * 2. _addOutputs      : Add outputs/sinks. A separate step had to be created as outputs
+ *                       are represented in the opposite direction of inputs.
+ * 3. _cacheChildren   : Make a cache of children in allChildren property for later use
+ * 4. _getGraphDetails : Get a graph object with all the required details
+ *
+ */
+App.DagViewComponent.dataProcessor = (function (){
+  /**
+   * Enum of various node types
+   */
+  var types = {
+    ROOT: 'root',
+    DUMMY: 'dummy',
+    VERTEX: 'vertex',
+    INPUT: 'input',
+    OUTPUT: 'output'
+  };
+
+  /**
+   * Abstract class for all types of data nodes
+   */
+  var DataNode = Em.Object.extend({
+        init: function (data) {
+          this._super(data);
+          this._init(data);
+        },
+        _init: function () {
+          // Initialize data members
+          this.setProperties({
+            /**
+             * Children that would be displayed in the view, to hide a child it would be removed from this array.
+             * Not making this a computed property because - No d3 support, low performance.
+             */
+            children: null,
+            allChildren: null, // All children under this node
+            treeParent: null,  // Direct parent DataNode in our tree structure
+          });
+        },
+
+        /**
+         * Private function.
+         * Set the child array as it is. Created because of performance reasons.
+         * @param children {Array} Array to be set
+         */
+        _setChildren: function (children) {
+          this.set('children', children && children.length > 0 ? children : null);
+        },
+        /**
+         * Public function.
+         * Set the child array after filtering
+         * @param children {Array} Array of DataNodes to be set
+         */
+        setChildren: function (children) {
+          this._setChildren(this.get('allChildren').filter(function (child) {
+            return children.indexOf(child) != -1; // true if child is in children
+          }));
+        },
+        /**
+         * Filter out the given children from the children array.
+         * @param childrenToRemove {Array} Array of DataNodes to be removed
+         */
+        removeChildren: function (childrenToRemove) {
+          var children = this.get('children');
+          if(children) {
+            children = children.filter(function (child) {
+              return childrenToRemove.indexOf(child) == -1; // false if child is in children
+            });
+            this._setChildren(children);
+          }
+        },
+
+        /**
+         * Return true if this DataNode is same as or the ancestor of vertex
+         * @param vertex {DataNode}
+         */
+        isSelfOrAncestor: function (vertex) {
+          while(vertex) {
+            if(vertex === this) return true;
+            vertex = vertex.treeParent;
+          }
+          return false;
+        },
+
+        /**
+         * If the property is available, expects it to be an array and iterate over
+         * its elements using the callback.
+         * @param vertex {DataNode}
+         * @param callback {Function}
+         * @param thisArg {} Will be value of this inside the callback
+         */
+        ifForEach: function (property, callback, thisArg) {
+          if(this.get(property)) {
+            this.get(property).forEach(callback, thisArg);
+          }
+        },
+        /**
+         * Recursively call the function specified in all children
+         * its elements using the callback.
+         * @param functionName {String} Name of the function to be called
+         */
+        recursivelyCall: function (functionName) {
+          if(this[functionName]) {
+            this[functionName]();
+          }
+          this.ifForEach('children', function (child) {
+            child.recursivelyCall(functionName);
+          });
+        }
+      }),
+      RootDataNode = DataNode.extend({
+        type: types.ROOT,
+        vertexName: 'root',
+        dummy: null, // Dummy node used in the tree, check top comments for explanation
+        depth: 0,    // Depth of the node in the tree structure
+
+        _init: function () {
+          this._setChildren([this.get('dummy')]);
+        }
+      }),
+      VertexDataNode = DataNode.extend({
+        type: types.VERTEX,
+
+        _additionalsIncluded: true,
+
+        _init: function () {
+          this._super();
+
+          // Initialize data members
+          this.setProperties({
+            id: this.get('vertexName'),
+            inputs: [], // Array of sources
+            outputs: [] // Array of sinks
+          });
+
+          this.ifForEach('additionalInputs', function (input) {
+            this.inputs.push(InputDataNode.instantiate(this, input));
+          }, this);
+
+          this.ifForEach('additionalOutputs', function (output) {
+            this.outputs.push(OutputDataNode.instantiate(this, output));
+          }, this);
+        },
+
+        /**
+         * Sets depth of the vertex and all its input children
+         * @param depth {Number}
+         */
+        setDepth: function (depth) {
+          this.set('depth', depth);
+
+          depth++;
+          this.get('inputs').forEach(function (input) {
+            input.set('depth', depth);
+          });
+        },
+
+        /**
+         * Sets vertex tree parents
+         * @param parent {DataNode}
+         */
+        setParent: function (parent) {
+          this.set('treeParent', parent);
+        },
+
+        /**
+         * Include sources and sinks in the children list, so that they are displayed
+         */
+        includeAdditionals: function() {
+          this.setChildren(this.get('inputs').concat(this.get('children') || []));
+
+          var ancestor = this.get('parent.parent');
+          if(ancestor) {
+            ancestor.setChildren(this.get('outputs').concat(ancestor.get('children') || []));
+          }
+          this.set('_additionalsIncluded', true);
+        },
+        /**
+         * Exclude sources and sinks in the children list, so that they are hidden
+         */
+        excludeAdditionals: function() {
+          this.removeChildren(this.get('inputs'));
+
+          var ancestor = this.get('parent.parent');
+          if(ancestor) {
+            ancestor.removeChildren(this.get('outputs'));
+          }
+          this.set('_additionalsIncluded', false);
+        },
+        /**
+         * Toggle inclusion/display of sources and sinks.
+         */
+        toggleAdditionalInclusion: function () {
+          var include = !this.get('_additionalsIncluded');
+          this.set('_additionalsIncluded', include);
+
+          if(include) {
+            this.includeAdditionals();
+          }
+          else {
+            this.excludeAdditionals();
+          }
+        }
+      }),
+      InputDataNode = $.extend(DataNode.extend({
+        type: types.INPUT,
+        vertex: null, // The vertex DataNode to which this node is linked
+
+        _init: function () {
+          var vertex = this.get('vertex');
+          this._super();
+
+          // Initialize data members
+          this.setProperties({
+            id: vertex.get('vertexName') + this.get('name'),
+            depth: vertex.get('depth') + 1
+          });
+        }
+      }), {
+        /**
+         * Initiate an InputDataNode
+         * @param vertex {DataNode}
+         * @param data {Object}
+         */
+        instantiate: function (vertex, data) {
+          return InputDataNode.create($.extend(data, {
+            treeParent: vertex,
+            vertex: vertex
+          }));
+        }
+      }),
+      OutputDataNode = $.extend(DataNode.extend({
+        type: types.OUTPUT,
+        vertex: null, // The vertex DataNode to which this node is linked
+
+        _init: function (data) {
+          this._super();
+
+          // Initialize data members
+          this.setProperties({
+            id: this.get('vertex.vertexName') + this.get('name')
+          });
+        }
+      }), {
+        /**
+         * Initiate an OutputDataNode
+         * @param vertex {DataNode}
+         * @param data {Object}
+         */
+        instantiate: function (vertex, data) {
+          /**
+           * We will have an idea about the treeParent & depth only after creating the
+           * tree structure.
+           */
+          return OutputDataNode.create($.extend(data, {
+            vertex: vertex
+          }));
+        }
+      });
+
+  var _data = null; // Raw dag plan data
+
+  /**
+   * Step 1: Recursive
+   * Creates primary skeletal structure with vertices and inputs as nodes,
+   * All child vertices & inputs will be added to an array property named children
+   * As we are trying to treefy graph data, nodes might reoccur. Reject if its in
+   * the ancestral chain, and if the new depth is lower (Higher value) than the old
+   * reposition the node.
+   *
+   * @param vertex {VertexDataNode} Root vertex of current sub tree
+   * @param depth {Number} Depth of the passed vertex
+   * @param vertex {VertexDataNode}
+   */
+  function _treefyData(vertex, depth) {
+    var children = [],
+        inputDepth;
+
+    depth++;
+
+    vertex.ifForEach('inEdgeIds', function (edgeId) {
+      var child = _data.vertices.get(_data.edges.get(edgeId).get('inputVertexName'));
+      if(!child.isSelfOrAncestor(vertex)) {
+        if(child.depth) {
+          if(child.depth < depth) {
+            child.get('treeParent.children').removeObject(child);
+          }
+          else {
+            return;
+          }
+        }
+        child.setParent(vertex);
+        children.push(_treefyData(child, depth));
+      }
+    });
+
+    vertex.setDepth(depth);
+    children.push.apply(children, vertex.get('inputs'));
+
+    vertex._setChildren(children);
+    return vertex;
+  }
+
+  /**
+   * Step 2: Recursive awesomeness
+   * Attaches outputs into the primary structure created in step 1. As outputs must be represented
+   * in the same level of the vertex's parent. They are added as children of its parent's parent.
+   *
+   * The algorithm is designed to get a symmetric display of output nodes.
+   * A call to the function will iterate through all its children, and inserts output nodes at the
+   * position that best fits the expected symmetry.
+   *
+   * @param vertex {VertexDataNode}
+   * @return {Object} Nodes that would come to the left and right of the vertex.
+   */
+  function _addOutputs(vertex) {
+    var childVertices = vertex.get('children'),
+        childrenWithOutputs = [],
+
+        midIndex,
+        tree,
+
+        left = [],
+        right = [];
+
+    // For a symmetric display of output nodes
+    if(childVertices && childVertices.length) {
+      midIndex = Math.ceil(childVertices.length / 2);
+      if(childVertices.length % 2 == 0) {
+        midIndex--;
+      }
+
+      childVertices.forEach(function (child, index) {
+        var additionals = _addOutputs(child),
+            outputs,
+            mid;
+
+        childrenWithOutputs.push.apply(childrenWithOutputs, additionals.left);
+        childrenWithOutputs.push(child);
+        childrenWithOutputs.push.apply(childrenWithOutputs, additionals.right);
+
+        outputs = child.get('outputs');
+        if(outputs && outputs.length) {
+          mid = outputs.length / 2;
+
+          outputs.forEach(function (output) {
+            output.depth = vertex.depth;
+          });
+
+          if(index < midIndex) {
+            left.push.apply(left, outputs);
+          }
+          else if(index > midIndex) {
+            right.push.apply(right, outputs);
+          }
+          else {
+            left.push.apply(left, outputs.slice(mid));
+            right.push.apply(right, outputs.slice(0, mid));
+          }
+        }
+      });
+
+      vertex._setChildren(childrenWithOutputs);
+    }
+
+    return {
+      left: left,
+      right: right
+    };
+  }
+
+  /**
+   * Step 3: Recursive
+   * Create a copy of all possible children in allChildren for later use
+   * @param node {DataNode}
+   */
+  function _cacheChildren(node) {
+    if(node.get('children')) {
+      node.set('allChildren', node.get('children') || []);
+      node.get('children').forEach(_cacheChildren);
+    }
+  }
+
+  /**
+   * Return an array of the incoming edges/links and input-output source-sink edges of the node.
+   * @param node {DataNode}
+   * @return links {Array} Array of all incoming and input-output edges of the node
+   */
+  function _getLinks(node) {
+    var links = [];
+
+    node.ifForEach('inEdgeIds', function (inEdge) {
+      var edge = _data.edges.get(inEdge);
+      edge.setProperties({
+        sourceId: edge.get('inputVertexName'),
+        targetId: edge.get('outputVertexName')
+      });
+      links.push(edge);
+    });
+
+    if(node.type == types.INPUT) {
+      links.push(Em.Object.create({
+        sourceId: node.get('id'),
+        targetId: node.get('vertex.id')
+      }));
+    }
+    else if(node.type == types.OUTPUT) {
+      links.push(Em.Object.create({
+        sourceId: node.get('vertex.id'),
+        targetId: node.get('id')
+      }));
+    }
+
+    return links;
+  }
+
+  /**
+   * Step 4: Recursive
+   * Create a graph based on the given tree structure and edges in _data object.
+   * @param tree {DataNode}
+   * @param details {Object} Object with values tree, links, maxDepth & maxHeight
+   */
+  function _getGraphDetails(tree) {
+    var maxDepth = 0,
+        inputCount = 0,
+
+        links = _getLinks(tree);
+
+    tree.ifForEach('children', function (child) {
+      var details = _getGraphDetails(child);
+      maxDepth = Math.max(maxDepth, details.maxDepth);
+
+      if(child.type == types.INPUT) {
+        inputCount++;
+      }
+      inputCount += details.inputCount;
+
+      links.push.apply(links, details.links);
+    });
+
+    return {
+      tree: tree,
+      links: links,
+      maxDepth: maxDepth + 1,
+      inputCount: inputCount
+    };
+  }
+
+  /**
+   * Converts vertices & edges into hashes for easy access.
+   * Makes vertexGroup a property of the respective vertices.
+   * @param data {Object}
+   * @return {Object} An object with vertices hash, edges hash and array of root vertices.
+   */
+  function _normalizeRawData(data) {
+    var EmObj = Em.Object,
+        vertices,          // Hash of vertices
+        edges,             // Hash of edges
+        rootVertices = []; // Vertices without out-edges are considered root vertices
+
+    vertices = data.vertices.reduce(function (obj, vertex) {
+      vertex = VertexDataNode.create(vertex);
+      if(!vertex.outEdgeIds) {
+        rootVertices.push(vertex);
+      }
+      obj[vertex.vertexName] = vertex;
+      return obj;
+    }, {});
+
+    edges = !data.edges ? [] : data.edges.reduce(function (obj, edge) {
+      obj[edge.edgeId] = EmObj.create(edge);
+      return obj;
+    }, {});
+
+    if(data.vertexGroups) {
+      data.vertexGroups.forEach(function (group) {
+        group.groupMembers.forEach(function (vertex) {
+          vertices[vertex].vertexGroup = EmObj.create(group);
+        });
+      });
+    }
+
+    return {
+      vertices: EmObj.create(vertices),
+      edges: EmObj.create(edges),
+      rootVertices: rootVertices
+    };
+  }
+
+  return {
+    // Types enum
+    types: types,
+
+    /**
+     * Converts raw DAG-plan into an internal data representation that graph-view,
+     * and in turn d3.layout.tree can digest.
+     * @param data {Object} Dag-plan data
+     * @return {Object/String}
+     *    - Object with values tree, links, maxDepth & maxHeight
+     *    - Error message if the data was not as expected.
+     */
+    graphifyData: function (data) {
+      var dummy = DataNode.create({
+            type: types.DUMMY,
+            vertexName: 'dummy',
+            depth: 1
+          }),
+          root = RootDataNode.create({
+            dummy: dummy
+          });
+
+      if(!data.vertices) {
+        return "Vertices not found!";
+      }
+
+      _data = _normalizeRawData(data);
+
+      if(!_data.rootVertices.length) {
+        return "Sink vertex not found!";
+      }
+
+      dummy._setChildren(_data.rootVertices.map(function (vertex) {
+        return _treefyData(vertex, 2);
+      }));
+
+      _addOutputs(root);
+
+      _cacheChildren(root);
+
+      return _getGraphDetails(root);
+    }
+  };
+
+})();

http://git-wip-us.apache.org/repos/asf/tez/blob/1784f015/tez-ui/src/main/webapp/app/scripts/components/dag-view/graph-view.js
----------------------------------------------------------------------
diff --git a/tez-ui/src/main/webapp/app/scripts/components/dag-view/graph-view.js b/tez-ui/src/main/webapp/app/scripts/components/dag-view/graph-view.js
new file mode 100644
index 0000000..02c04e6
--- /dev/null
+++ b/tez-ui/src/main/webapp/app/scripts/components/dag-view/graph-view.js
@@ -0,0 +1,870 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * 'License'); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * The view part of Dag View.
+ *
+ * Displays TEZ DAG graph in a tree layout. (Uses d3.layout.tree)
+ * Graph view exposes just 4 functions to the outside world, everything else
+ * happens inside the main closure:
+ *   1. create
+ *   2. fitGraph
+ *   3. additionalDisplay
+ *   4. toggleLayouts
+ *
+ * Links, Paths:
+ * --------------
+ * d3 layout uses the term links, and SVG uses path. Hence you would see both getting used
+ * in this file. You can consider link to be a JavaScript data object, and path to be a visible
+ * SVG DOM element on the screen.
+ *
+ * Extra Nodes:
+ * ------------
+ * Root Node (Invisible):
+ *  Dag view support very complex DAGs, even DAGs without interconnections and backward links.
+ *  Hence to fit it into a tree layout I have inserted an invisible root node.
+ *
+ * Dummy Node (Invisible):
+ *  Sinks of a vertex are added at the same level of its parent node, Hence to ensure that all
+ *  nodes come under the root, a dummy node was added as the child of the root. The visible tree
+ *  would be added as child of dummy node.
+ *  Dummy also ensures the view symmetry when multiple outputs are present at the dummy level.
+ *
+ * Sample Structure, inverted tree representation:
+ *
+ *            As in the view
+ *
+ *               Source_m1
+ *                  |
+ *   Source_m2      M1----------+
+ *      |           |           |
+ *      +-----------M2      Sink_M1
+ *                  |
+ *      +-----------R1----------+
+ *      |                       |
+ *   Sink1_R1               Sink2_R1
+ *
+ *
+ *        Internal representation
+ *
+ *               Source_m1
+ *                  |
+ *   Source_m2      M1
+ *      |           |
+ *      +-----------M2      Sink_M1
+ *                  |           |
+ *                  R1----------+
+ *                  |
+ *   Sink1_R1     Dummy     Sink2_R1
+ *      |           |           |
+ *      +-----------+-----------+
+ *                  |
+ *                 Root
+ *
+ */
+App.DagViewComponent.graphView = (function (){
+
+  var PADDING = 30, // Adding to be applied on the svg view
+
+      LAYOUTS = { // The view supports two layouts - left to right and top to bottom.
+        leftToRight: {
+          hSpacing: 180,     // Horizontal spacing between nodes
+          vSpacing: 70,      // Vertical spacing between nodes
+          depthSpacing: 180, // In leftToRight depthSpacing = hSpacing
+          projector: function (x, y) { // Converts coordinate based on current orientation
+            return {x: y, y: x};
+          },
+          // Defines how path between nodes are drawn
+          pathDataFormat: "M %@ %@ Q %@ %@ %@ %@ Q %@ %@ %@ %@"
+        },
+        topToBottom: {
+          hSpacing: 120,
+          vSpacing: 100,
+          depthSpacing: 100, // In topToBottom depthSpacing = vSpacing
+          projector: function (x, y) {
+            return {x: x, y: y};
+          },
+          pathDataFormat: "M %@2 %@1 Q %@4 %@3 %@6 %@5 Q %@8 %@7 %@10 %@9"
+        }
+      },
+
+      DURATION = 750, // Animation duration
+
+      HREF_TYPE_HASH = { // Used to assess the entity type from an event target
+        "#task-bubble": "task",
+        "#vertex-bg": "vertex",
+        "#input-bg": "input",
+        "#output-bg": "output",
+        "#io-bubble": "io",
+        "#group-bubble": "group"
+      };
+
+  var _width = 0,
+      _height = 0,
+
+      _component = null,  // The parent ember component
+      _data = null,       // Data object created by data processor
+      _treeData = null,   // Points to root data node of the tree structure
+      _treeLayout = null, // Instance of d3 tree layout helper
+      _layout = null,     // Current layout, one of the values defined in LAYOUTS object
+
+      _svg = null, // jQuery instance of svg DOM element
+      _g = null,   // For pan and zoom: Svg DOM group element that encloses all the displayed items
+
+      _idCounter = 0,        // To create a fresh id for all displayed nodes
+      _scheduledClickId = 0, // Id of scheduled click, used for double click.
+
+      _tip,     // Instance of tip.js
+      _panZoom; // A closure returned by _attachPanZoom to reset/modify pan and zoom values
+
+  function _getName(d) {
+    switch(d.get('type')) {
+      case 'vertex':
+        return d.get('vertexName');
+      break;
+      default:
+        return d.get('name');
+    }
+  }
+  function _getSub(d) {
+    switch(d.get('type')) {
+      case 'vertex':
+        return d.get('data.status');
+      break;
+      default:
+        return d.get('class');
+    }
+  }
+  /**
+   * Texts grater than maxLength will be trimmed.
+   * @param text {String} Text to trim
+   * @param maxLength {Number}
+   * @return Trimmed text
+   */
+  function _trimText(text, maxLength) {
+    if(text) {
+      text = text.toString();
+      if(text.length > maxLength) {
+        text = text.substr(0, maxLength - 1) + '..';
+      }
+    }
+    return text;
+  }
+
+  /**
+   * Add task bubble to a vertex node
+   * @param node {SVG DOM element} Vertex node
+   * @param d {VertexDataNode}
+   */
+  function _addTaskBubble(node, d) {
+    var group = node.append('g');
+    group.attr('class', 'task-bubble');
+    group.append('use').attr('xlink:href', '#task-bubble');
+    group.append('text')
+        .text(_trimText(d.get('data.numTasks'), 3));
+  }
+  /**
+   * Add IO(source/sink) bubble to a vertex node
+   * @param node {SVG DOM element} Vertex node
+   * @param d {VertexDataNode}
+   */
+  function _addIOBubble(node, d) {
+    var group,
+        inputs = d.get('inputs.length'),
+        outputs = d.get('outputs.length');
+
+    if(inputs || outputs) {
+      group = node.append('g');
+      group.attr('class', 'io-bubble');
+      group.append('use').attr('xlink:href', '#io-bubble');
+      group.append('text')
+          .text(_trimText('%@/%@'.fmt(inputs, outputs), 3));
+    }
+  }
+  /**
+   * Add vertex group bubble to a vertex node
+   * @param node {SVG DOM element} Vertex node
+   * @param d {VertexDataNode}
+   */
+  function _addVertexGroupBubble(node, d) {
+    var group;
+
+    if(d.vertexGroup) {
+      group = node.append('g');
+      group.attr('class', 'group-bubble');
+      group.append('use').attr('xlink:href', '#group-bubble');
+      group.append('text')
+          .text(_trimText(d.get('vertexGroup.groupMembers.length'), 2));
+    }
+  }
+  /**
+   * Add status bar to a vertex node
+   * @param node {SVG DOM element} Vertex node
+   * @param d {VertexDataNode}
+   */
+  function _addStatusBar(node, d) {
+    var group = node.append('g');
+        statusIcon = App.Helpers.misc.getStatusClassForEntity(d.get('data'));
+    group.attr('class', 'status-bar');
+
+    group.append('foreignObject')
+        .attr("class", "status")
+        .attr("width", 70)
+        .attr("height", 15)
+        .html('<span class="msg-container"><i class="task-status ' +
+            statusIcon +
+            '"></i> ' +
+            d.get('data.status') +
+            '</span>'
+        );
+  }
+  /**
+   * Creates a base SVG DOM node, with bg and title based on the type of DataNode
+   * @param node {SVG DOM element} Vertex node
+   * @param d {DataNode}
+   * @param titleProperty {String} d's property who's value is the title to be displayed.
+   *    By default 'name'.
+   * @param maxTitleLength {Number} Title would be trimmed beyond maxTitleLength. By default 3 chars
+   */
+  function _addBasicContents(node, d, titleProperty, maxTitleLength) {
+    var className = d.type;
+
+    node.attr('class', 'node %@'.fmt(className));
+    node.append('use').attr('xlink:href', '#%@-bg'.fmt(className));
+    node.append('text')
+        .attr('class', 'title')
+        .text(_trimText(d.get(titleProperty || 'name'), maxTitleLength || 3));
+  }
+  /**
+   * Populates the calling node with the required content.
+   * @param s {DataNode}
+   */
+  function _addContent(d) {
+    var node = d3.select(this);
+
+    switch(d.type) {
+      case 'vertex':
+        _addBasicContents(node, d, 'vertexName', 15);
+        _addStatusBar(node, d);
+        _addTaskBubble(node, d);
+        _addIOBubble(node, d);
+        _addVertexGroupBubble(node, d);
+      break;
+      case 'input':
+      case 'output':
+        _addBasicContents(node, d);
+      break;
+    }
+  }
+
+  /**
+   * Create a list of all links connecting nodes in the given array.
+   * @param nodes {Array} A list of d3 nodes created by tree layout
+   * @return links {Array} All links between nodes in the current DAG
+   */
+  function _getLinks(nodes) {
+    var links = [],
+        nodeHash;
+
+    nodeHash = nodes.reduce(function (obj, node) {
+      obj[node.id] = node;
+      return obj;
+    }, {});
+
+    _data.links.forEach(function (link) {
+      var source = nodeHash[link.sourceId],
+          target = nodeHash[link.targetId];
+      if(source && target) {
+        link.setProperties({
+          source: source,
+          target: target,
+          isBackwardLink: source.isSelfOrAncestor(target)
+        });
+        links.push(link);
+      }
+    });
+
+    return links;
+  }
+
+  /**
+   * Apply proper depth spacing and remove the space occupied by dummy node
+   * if the number of other nodes are odd.
+   * @param nodes {Array} A list of d3 nodes created by tree layout
+   */
+  function _normalize(nodes) {
+    // Set layout
+    var farthestY = 0;
+    nodes.forEach(function (d) {
+      d.y = d.depth * -_layout.depthSpacing;
+      if(d.y < farthestY) farthestY = d.y;
+    });
+    farthestY -= PADDING;
+    nodes.forEach(function (d) {
+      d.y -= farthestY;
+    });
+
+    //Remove space occupied by dummy
+    var rootChildren = _treeData.get('children'),
+        rootChildCount = rootChildren.length,
+        dummyIndex;
+
+    if(rootChildCount % 2 == 0) {
+      dummyIndex = rootChildren.indexOf(_treeData.get('dummy'));
+      if(dummyIndex >= rootChildCount / 2) {
+        for(var i = dummyIndex - 1; i >= 0; i--) {
+          rootChildren[i].x = rootChildren[i + 1].x,
+          rootChildren[i].y = rootChildren[i + 1].y;
+        }
+      }
+      else {
+        for(var i = dummyIndex + 1; i < rootChildCount; i++) {
+          rootChildren[i].x = rootChildren[i - 1].x,
+          rootChildren[i].y = rootChildren[i - 1].y;
+        }
+      }
+    }
+  }
+
+  function _getType(node) {
+    if(node.tagName == 'path') {
+      return 'path';
+    }
+    return HREF_TYPE_HASH[$(node).attr('href')];
+  }
+
+  /**
+   * Mouse over handler for all displayed SVG DOM elements.
+   * Later the implementation will be refactored and moved into the respective DataNode.
+   * d {DataNode} Contains data to be displayed
+   */
+  function _onMouseOver(d) {
+    var event = d3.event,
+        node = event.target,
+        tooltipData = {}; // Will be populated with {title/text/kvList}.
+
+    node = node.correspondingUseElement || node;
+
+    switch(_getType(node)) {
+      case "vertex":
+        var list  = {};
+
+        _component.get('vertexProperties').forEach(function (property) {
+          var value = {};
+
+          if(property.contentPath) {
+            value = d.get('data.' + property.contentPath);
+          }
+          else if(property.getCellContent && !property.tableCellViewClass) {
+            value = property.getCellContent(d.get('data'));
+          }
+
+          if(typeof value != 'object') {
+            list[property.get('headerCellName')] = value;
+          }
+        });
+
+        tooltipData = {
+          title: d.get("vertexName"),
+          kvList: list
+        };
+      break;
+      case "input":
+        var list = {
+          "Class": App.Helpers.misc.getClassName(d.get("class")),
+          "Initializer": App.Helpers.misc.getClassName(d.get("initializer")),
+          "Configurations": d.get("configs.length")
+        };
+        tooltipData = {
+          title: d.get("name"),
+          kvList: list
+        };
+      break;
+      case "output":
+        var list = {
+          "Class": App.Helpers.misc.getClassName(d.get("class")),
+          "Configurations": d.get("configs.length")
+        };
+        tooltipData = {
+          title: d.get("name"),
+          kvList: list
+        };
+      break;
+      case "task":
+        var numTasks = d.get('data.numTasks');
+        tooltipData.title = (numTasks > 1 ? '%@ Tasks' : '%@ Task').fmt(numTasks);
+        node = d3.event.target;
+      break;
+      case "io":
+        var inputs = d.get('inputs.length'),
+            outputs = d.get('outputs.length'),
+            title = "";
+        title += (inputs > 1 ? '%@ Sources' : '%@ Source').fmt(inputs);
+        title += " & ";
+        title += (outputs > 1 ? '%@ Sinks' : '%@ Sink').fmt(outputs);
+        tooltipData.title = title;
+
+        node = d3.event.target;
+      break;
+      case "group":
+        tooltipData = {
+          title: d.get("vertexGroup.groupName"),
+          text: d.get("vertexGroup.groupMembers").join(", ")
+        };
+      break;
+      case "path":
+        tooltipData = {
+          position: {
+            x: event.pageX,
+            y: event.pageY
+          },
+          title: '%@ - %@'.fmt(
+            d.get('source.name') || d.get('source.vertexName'),
+            d.get('target.name') || d.get('target.vertexName')
+          )
+        };
+        if(d.get("edgeId")) {
+          tooltipData.kvList = {
+            "Edge Id": d.get("edgeId"),
+            "Data Movement Type": d.get("dataMovementType"),
+            "Data Source Type": d.get("dataSourceType"),
+            "Scheduling Type": d.get("schedulingType"),
+            "Edge Destination Class": App.Helpers.misc.getClassName(d.get("edgeDestinationClass")),
+            "Edge Source Class": App.Helpers.misc.getClassName(d.get("edgeSourceClass"))
+          };
+        }
+        else {
+          tooltipData.text = d.get('source.type') == "input" ? "Source link" : "Sink link";
+        }
+      break;
+    }
+
+    _tip.show(node, tooltipData, event);
+  }
+
+  /**
+   * onclick handler scheduled using setTimeout
+   * @params d {DataNode} data of the clicked element
+   * @param node {D3 element} Element that was clicked
+   */
+  function _scheduledClick(d, node) {
+    node = node.correspondingUseElement || node;
+
+    _component.sendAction('entityClicked', {
+      type: _getType(node),
+      d: d
+    });
+
+    _tip.hide();
+    _scheduledClickId = 0;
+  }
+
+  /**
+   * Schedules an onclick handler. If double click event is not triggered the handler
+   * will be called in 200ms.
+   * @param d {DataNode} Data of the clicked element
+   */
+  function _onClick(d) {
+    if(!_scheduledClickId) {
+      _scheduledClickId = setTimeout(_scheduledClick.bind(this, d, d3.event.target), 200);
+    }
+  }
+
+  /**
+   * Double click event handler.
+   * @param d {DataNode} Data of the clicked element
+   */
+  function _onDblclick(d) {
+    var event = d3.event,
+        node = event.target,
+        dataProcessor = App.DagViewComponent.dataProcessor;
+
+    node = node.correspondingUseElement || node;
+
+    if(_scheduledClickId) {
+      clearTimeout(_scheduledClickId);
+      _scheduledClickId = 0;
+    }
+
+    switch(_getType(node)) {
+      case "io":
+        d.toggleAdditionalInclusion();
+        _update();
+      break;
+    }
+  }
+
+  /**
+   * Creates a path data string for the given link. Google SVG path data to learn what it is.
+   * @param d {Object} Must contain source and target properties with the start and end positions.
+   * @return pathData {String} Path data string based on the current layout
+   */
+  function _createPathData(d) {
+    var sX = d.source.y,
+        sY = d.source.x,
+        tX = d.target.y,
+        tY = d.target.x,
+        mX = (sX + tX)/2,
+        mY = (sY + tY)/2,
+
+        sH = Math.abs(sX - tX) * 0.35,
+        sV = 0; // strength
+
+    if(d.isBackwardLink) {
+      if(sY == tY) {
+        sV = 45,
+        mY -= 50;
+        if(sX == tX) {
+          sX += 30,
+          tX -= 30;
+        }
+      }
+      sH = Math.abs(sX - tX) * 1.1;
+    }
+
+    return "".fmt.apply(_layout.pathDataFormat, [
+      sX, sY,
+
+      sX + sH, sY - sV,
+      mX, mY,
+
+      tX - sH, tY - sV,
+      tX, tY
+    ]);
+  }
+
+  /**
+   * Get the node from/to which the node must transition on enter/exit
+   * @param d {DataNode}
+   * @param property {String} Property to be checked for
+   * @return vertex node
+   */
+  function _getVertexNode(d, property) {
+    if(d.get('vertex.' + property)) {
+      return d.get('vertex');
+    }
+  }
+  /**
+   * Update position of all nodes in the list and preform required transitions.
+   * @param nodes {Array} Nodes to be updated
+   * @param source {d3 element} Node that trigged the update, in first update source will be root.
+   */
+  function _updateNodes(nodes, source) {
+    // Enter any new nodes at the parent's previous position.
+    nodes.enter().append('g')
+      .attr('transform', function(d) {
+        var node = _getVertexNode(d, "x0") || source;
+        node = _layout.projector(node.x0, node.y0);
+        return 'translate(' + node.x + ',' + node.y + ')';
+      })
+      .on({
+        mouseover: _onMouseOver,
+        mouseout: _tip.hide,
+        click: _onClick,
+        dblclick: _onDblclick
+      })
+      .style('opacity', 1e-6)
+      .each(_addContent);
+
+    // Transition nodes to their new position.
+    nodes.transition()
+      .duration(DURATION)
+      .attr('transform', function(d) {
+        d = _layout.projector(d.x, d.y);
+        return 'translate(' + d.x + ',' + d.y + ')';
+      })
+      .style('opacity', 1);
+
+    // Transition exiting nodes to the parent's new position.
+    nodes.exit().transition()
+      .duration(DURATION)
+      .attr('transform', function(d) {
+        var node = _getVertexNode(d, "x") || source;
+        node = _layout.projector(node.x, node.y);
+        return 'translate(' + node.x + ',' + node.y + ')';
+      })
+      .style('opacity', 1e-6)
+      .remove();
+  }
+
+  /**
+   * Get the node from/to which the link must transition on enter/exit
+   * @param d {DataNode}
+   * @param property {String} Property to be checked for
+   * @return node
+   */
+  function _getTargetNode(d, property) {
+    if(d.get('target.type') == App.DagViewComponent.dataProcessor.types.OUTPUT
+        && d.get('source.' + property)) {
+      return d.source;
+    }
+    if(d.get('target.' + property)) {
+      return d.target;
+    }
+  }
+  /**
+   * Update position of all links in the list and preform required transitions.
+   * @param links {Array} Links to be updated
+   * @param source {d3 element} Node that trigged the update, in first update source will be root.
+   */
+  function _updateLinks(links, source) {
+    // Enter any new links at the parent's previous position.
+    links.enter().insert('path', 'g')
+      .attr('class', function (d) {
+        var type = d.get('dataMovementType') || "";
+        return 'link ' + type.toLowerCase();
+      })
+      .attr("style", "marker-mid: url(#arrow-marker);")
+      .attr('d', function(d) {
+        var node = _getTargetNode(d, "x0") || source;
+        var o = {x: node.x0, y: node.y0};
+        return _createPathData({source: o, target: o});
+      })
+      .on({
+        mouseover: _onMouseOver,
+        mouseout: _tip.hide
+      });
+
+    // Transition links to their new position.
+    links.transition()
+      .duration(DURATION)
+      .attr('d', _createPathData);
+
+    // Transition exiting nodes to the parent's new position.
+    links.exit().transition()
+      .duration(DURATION)
+      .attr('d', function(d) {
+        var node = _getTargetNode(d, "x") || source;
+        var o = {x: node.x, y: node.y};
+        return _createPathData({source: o, target: o});
+      })
+      .remove();
+  }
+
+  function _getNodeId(d) {
+    return d.id || (d.id = ++_idCounter);
+  }
+  function _getLinkId(d) {
+    return d.source.id.toString() + d.target.id;
+  }
+  function _stashOldPositions(d) {
+    d.x0 = d.x,
+    d.y0 = d.y;
+  }
+
+  /**
+   * Updates position of nodes and links based on changes in _treeData.
+   */
+  function _update() {
+    var nodesData = _treeLayout.nodes(_treeData),
+        linksData = _getLinks(nodesData);
+
+    _normalize(nodesData);
+
+    var nodes = _g.selectAll('g.node')
+      .data(nodesData, _getNodeId);
+    _updateNodes(nodes, _treeData);
+
+    var links = _g.selectAll('path.link')
+        .data(linksData, _getLinkId);
+    _updateLinks(links, _treeData);
+
+    nodesData.forEach(_stashOldPositions);
+  }
+
+  /**
+   * Attach pan and zoom events on to the container.
+   * @param container {DOM element} Element onto which events are attached.
+   * @param g {d3 DOM element} SVG(d3) element that will be moved or scaled
+   */
+  function _attachPanZoom(container, g) {
+    var SCALE_TUNER = 1 / 700,
+        MIN_SCALE = .5,
+        MAX_SCALE = 2;
+
+    var prevX = 0,
+        prevY = 0,
+
+        panX = PADDING,
+        panY = PADDING,
+        scale = 1;
+
+    /**
+     * Transform g to current panX, panY and scale.
+     * @param animate {Boolean} Animate the transformation in DURATION time.
+     */
+    function transform(animate) {
+      var base = animate ? g.transition().duration(DURATION) : g;
+      base.attr('transform', 'translate(%@, %@) scale(%@)'.fmt(panX, panY, scale));
+    }
+
+    /**
+     * Set pan values
+     */
+    function onMouseMove(event) {
+      panX += event.pageX - prevX,
+      panY += event.pageY - prevY;
+
+      transform();
+
+      prevX = event.pageX,
+      prevY = event.pageY;
+    }
+    /**
+     * Set zoom values, pan also would change as we are zooming with mouse position as pivote.
+     */
+    function onWheel(event) {
+      var prevScale = scale,
+
+          offset = container.offset(),
+          mouseX = event.pageX - offset.left,
+          mouseY = event.pageY - offset.top,
+          factor = 0;
+
+      scale += event.deltaY * SCALE_TUNER;
+      if(scale < MIN_SCALE) {
+        scale = MIN_SCALE;
+      }
+      else if(scale > MAX_SCALE) {
+        scale = MAX_SCALE;
+      }
+
+      factor = 1 - scale / prevScale,
+      panX += (mouseX - panX) * factor,
+      panY += (mouseY - panY) * factor;
+
+      transform();
+
+      _tip.reposition();
+      event.preventDefault();
+    }
+
+    container
+    .on('mousewheel', onWheel)
+    .mousedown(function (event){
+      prevX = event.pageX,
+      prevY = event.pageY;
+
+      container.on('mousemove', onMouseMove);
+    })
+    .mouseup(function (event){
+      container.off('mousemove', onMouseMove);
+    })
+
+    /**
+     * A closure to reset/modify panZoom based on an external event
+     * @param newPanX {Number}
+     * @param newPanY {Number}
+     * @param newScale {Number}
+     */
+    return function (newPanX, newPanY, newScale) {
+      panX = newPanX,
+      panY = newPanY,
+      scale = newScale;
+
+      transform(true);
+    }
+  }
+
+  /**
+   * Sets the layout and update the display.
+   * @param layout {Object} One of the values defined in LAYOUTS object
+   */
+  function _setLayout(layout) {
+    var dimention = layout.projector(_data.inputCount, _data.maxDepth - 1);
+
+    _layout = layout;
+
+    _width = dimention.x *= _layout.hSpacing,
+    _height = dimention.y *= _layout.vSpacing;
+
+    dimention = _layout.projector(dimention.x, dimention.y), // Because tree is always top to bottom
+    _treeLayout = d3.layout.tree().size([dimention.x, dimention.y]);
+
+    _update();
+  }
+
+  return {
+    /**
+     * Creates a DAG view in the given element based on the data
+     * @param component {DagViewComponent} Parent ember component, to sendAction
+     * @param element {HTML DOM Element} HTML element in which the view will be created
+     * @param data {Object} Created by data processor
+     */
+    create: function (component, element, data) {
+      var svg = d3.select(element).select('svg');
+
+      _component = component,
+      _data = data,
+      _g = svg.append('g').attr('transform', 'translate(%@,%@)'.fmt(PADDING, PADDING));
+      _svg = $(svg.node());
+      _tip = App.DagViewComponent.tip;
+
+      _tip.init($(element).find('.tool-tip'), _svg);
+
+      _treeData = data.tree,
+      _treeData.x0 = _height / 2,
+      _treeData.y0 = 0;
+
+      _panZoom = _attachPanZoom(_svg, _g);
+
+      _setLayout(LAYOUTS.topToBottom);
+    },
+
+    /**
+     * Calling this function would fit the graph to the available space.
+     */
+    fitGraph: function (){
+      var scale = Math.min(
+        (_svg.width() - PADDING * 2) / _width,
+        (_svg.height() - PADDING * 2) / _height
+      );
+      _panZoom(PADDING, PADDING, scale);
+    },
+
+    /**
+     * Control display of additionals or sources and sinks.
+     * @param hide {Boolean} If true the additionals will be excluded, else included in the display
+     */
+    additionalDisplay: function (hide) {
+      var dataProcessor = App.DagViewComponent.dataProcessor,
+          filterTypes = null;
+
+      if(hide) {
+        _g.attr('class', 'hide-io');
+        _treeData.recursivelyCall('excludeAdditionals');
+      }
+      else {
+        _treeData.recursivelyCall('includeAdditionals');
+        _g.attr('class', null);
+      }
+      _update();
+    },
+
+    /**
+     * Toggle graph layouts between the available options
+     */
+    toggleLayouts: function () {
+      _setLayout(_layout == LAYOUTS.topToBottom ?
+          LAYOUTS.leftToRight :
+          LAYOUTS.topToBottom);
+      return _layout == LAYOUTS.topToBottom;
+    }
+  };
+
+})();
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/tez/blob/1784f015/tez-ui/src/main/webapp/app/scripts/components/dag-view/tip.js
----------------------------------------------------------------------
diff --git a/tez-ui/src/main/webapp/app/scripts/components/dag-view/tip.js b/tez-ui/src/main/webapp/app/scripts/components/dag-view/tip.js
new file mode 100644
index 0000000..fa95a66
--- /dev/null
+++ b/tez-ui/src/main/webapp/app/scripts/components/dag-view/tip.js
@@ -0,0 +1,190 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * 'License'); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Displays a tooltip over an svg element.
+ */
+App.DagViewComponent.tip = (function () {
+
+  var _element = null,  // jQuery tooltip DOM element
+      _bubble = null,   // Tooltip bubble in _element
+      _svg = null,      // HTML svg tag that contains the element
+      _svgPoint = null, // A SVGPoint object
+      _window = $(window),
+
+      _data = null, // Last displayed data, for re-render
+      _node = null; // Last node over which tooltip was displayed
+
+  /**
+   * Converts the provided list object into a tabular form.
+   * @param list {Object} : An object with properties to be displayed as key value pairs
+   *   {
+   *     propertyName1: "property value 1",
+   *     ..
+   *     propertyNameN: "property value N",
+   *   }
+   */
+  function _createList(list) {
+    var listContent = [],
+        properties;
+
+    if(list) {
+      listContent.push("<table>");
+
+      $.each(list, function (property, value) {
+        listContent.push(
+          "<tr><td>",
+          property,
+          "</td><td>",
+          App.Helpers.number.formatNumThousands(value),
+          "</td></tr>"
+        );
+      });
+      listContent.push("</table>");
+
+      return listContent.join("");
+    }
+  }
+
+  /**
+   * Tip supports 3 visual entities in the tooltip. Title, description text and a list.
+   * _setData sets all these based on the passed data object
+   * @param data {Object} An object of the format
+   * {
+   *   title: "tip title",
+   *   text: "tip description text",
+   *   kvList: {
+   *     propertyName1: "property value 1",
+   *     ..
+   *     propertyNameN: "property value N",
+   *   }
+   * }
+   */
+  function _setData(data) {
+    _element.find('.tip-title').html(data.title || "");
+    _element.find('.tip-text').html(data.text || "");
+    _element.find('.tip-list').html(_createList(data.kvList) || "");
+  }
+
+  return {
+    /**
+     * Set the tip defaults
+     * @param tipElement {$} jQuery reference to the tooltip DOM element.
+     *    The element must contain 3 children with class tip-title, tip-text & tip-list.
+     * @param svg {$} jQuery reference to svg html element
+     */
+    init: function (tipElement, svg) {
+      _element = tipElement,
+      _bubble = _element.find('.bubble'),
+      _svg = svg,
+      _svgPoint = svg[0].createSVGPoint();
+    },
+    /**
+     * Display a tooltip over an svg element.
+     * @param node {SVG Element} Svg element over which tooltip must be displayed.
+     * @param data {Object} An object of the format
+     * {
+     *   title: "tip title",
+     *   text: "tip description text",
+     *   kvList: {
+     *     propertyName1: "property value 1",
+     *     ..
+     *     propertyNameN: "property value N",
+     *   }
+     * }
+     * @param event {MouseEvent} Event that triggered the tooltip.
+     */
+    show: function (node, data, event) {
+      var point = data.position || (node.getScreenCTM ? _svgPoint.matrixTransform(
+            node.getScreenCTM()
+          ) : {
+            x: event.x,
+            y: event.y
+          }),
+
+          windMid = _window.height() >> 1,
+          winWidth = _window.width(),
+
+          showAbove = point.y < windMid,
+          offsetX = 0,
+          width = 0,
+
+          svgLeft = _svg.offset().left;
+
+      if(_data !== data) {
+        _data = data,
+        _node = node;
+
+        _setData(data);
+      }
+
+      if(point.x > svgLeft && point.x < svgLeft + _svg.width()) {
+        if(showAbove) {
+          _element.removeClass('below');
+          _element.addClass('above');
+        }
+        else {
+          _element.removeClass('above');
+          _element.addClass('below');
+
+          point.y -= _element.height();
+        }
+
+        width = _element.width();
+        offsetX = (width - 11) >> 1;
+
+        if(point.x - offsetX < 0) {
+          offsetX = point.x - 20;
+        }
+        else if(point.x + offsetX > winWidth) {
+          offsetX = point.x - (winWidth - 10 - width);
+        }
+
+        _bubble.css({
+          left: -offsetX
+        });
+
+        _element.addClass('show');
+
+        _element.css({
+          left: point.x,
+          top: point.y
+        });
+      }
+      else {
+        _element.removeClass('show');
+      }
+    },
+    /**
+     * Reposition the tooltip based on last passed data & node.
+     */
+    reposition: function () {
+      if(_data) {
+        this.show(_node, _data);
+      }
+    },
+    /**
+     * Hide the tooltip.
+     */
+    hide: function () {
+      _data = _node = null;
+      _element.removeClass('show');
+    }
+  };
+
+})();

http://git-wip-us.apache.org/repos/asf/tez/blob/1784f015/tez-ui/src/main/webapp/app/scripts/controllers/dag-view-controller.js
----------------------------------------------------------------------
diff --git a/tez-ui/src/main/webapp/app/scripts/controllers/dag-view-controller.js b/tez-ui/src/main/webapp/app/scripts/controllers/dag-view-controller.js
new file mode 100644
index 0000000..9e73718
--- /dev/null
+++ b/tez-ui/src/main/webapp/app/scripts/controllers/dag-view-controller.js
@@ -0,0 +1,94 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+App.DagViewController = Em.ObjectController.extend(App.PaginatedContentMixin, App.ColumnSelectorMixin, {
+  controllerName: 'DagViewController',
+
+  childEntityType: 'vertex',
+
+  needs: ["dag", "dagVertices"],
+  pageTitle: 'Dag View',
+
+  columnSelectorTitle: 'Customize vertex tooltip',
+
+  count: Number.MAX_SAFE_INTEGER - 1,
+
+  loadData: function() {
+    var filters = {
+      primary: {
+        TEZ_DAG_ID: this.get('controllers.dag.id')
+      }
+    }
+    this.setFiltersAndLoadEntities(filters);
+  },
+
+  actions: {
+    entityClicked: function (details) {
+      switch(details.type) {
+        case 'vertex':
+          this.transitionToRoute('vertex', details.d.get('data.id'));
+        break;
+        case 'task':
+          this.transitionToRoute('vertex.tasks', details.d.get('data.id'));
+        break;
+        case 'io':
+          this.transitionToRoute('vertex.additionals', details.d.get('data.id'));
+        break;
+        case 'input':
+          this.transitionToRoute('input.configs', details.d.get('parent.data.id'), details.d.entity);
+        break;
+        case 'output':
+          this.transitionToRoute('output.configs', details.d.get('vertex.data.id'), details.d.entity);
+        break;
+      }
+    }
+  },
+
+  defaultColumnConfigs: function() {
+    return this.get('controllers.dagVertices.defaultColumnConfigs');
+  }.property(),
+
+  columnConfigs: function() {
+    var configs = this.get('controllers.dagVertices.columnConfigs');
+    return configs.filter(function (config) {
+      return (config.contentPath) ||
+          (config.getCellContent && !config.tableCellViewClass);
+    });
+  }.property(),
+
+  viewData: function () {
+    var vertices = this.get('controllers.dag.vertices'),
+        entities = this.get('entities'),
+        finalVertex;
+
+    entities = entities.reduce(function (obj, vertexData) {
+      obj[vertexData.get('name')] = vertexData;
+      return obj;
+    }, {});
+
+    vertices.forEach(function (vertex) {
+      vertex.data = entities[vertex.vertexName];
+    });
+
+    return {
+      vertices: vertices,
+      edges: this.get('controllers.dag.edges'),
+      vertexGroups: this.get('controllers.dag.vertexGroups')
+    };
+  }.property('entities')
+});

http://git-wip-us.apache.org/repos/asf/tez/blob/1784f015/tez-ui/src/main/webapp/app/scripts/controllers/dag_controller.js
----------------------------------------------------------------------
diff --git a/tez-ui/src/main/webapp/app/scripts/controllers/dag_controller.js b/tez-ui/src/main/webapp/app/scripts/controllers/dag_controller.js
index 6113cac..3375e33 100644
--- a/tez-ui/src/main/webapp/app/scripts/controllers/dag_controller.js
+++ b/tez-ui/src/main/webapp/app/scripts/controllers/dag_controller.js
@@ -29,6 +29,7 @@ App.DagController = Em.ObjectController.extend(App.Helpers.DisplayHelper, {
 
 	childDisplayViews: [
 		Ember.Object.create({title: 'Details', linkTo: 'dag.index'}),
+		Ember.Object.create({title: 'View', linkTo: 'dag.view'}),
 		Ember.Object.create({title: 'Vertices', linkTo: 'dag.vertices'}),
 		Ember.Object.create({title: 'Tasks', linkTo: 'dag.tasks'}),
 		Ember.Object.create({title: 'Task Attempts', linkTo: 'dag.taskAttempts'}),

http://git-wip-us.apache.org/repos/asf/tez/blob/1784f015/tez-ui/src/main/webapp/app/scripts/controllers/dags_controller.js
----------------------------------------------------------------------
diff --git a/tez-ui/src/main/webapp/app/scripts/controllers/dags_controller.js b/tez-ui/src/main/webapp/app/scripts/controllers/dags_controller.js
index e26ada5..55f9d26 100644
--- a/tez-ui/src/main/webapp/app/scripts/controllers/dags_controller.js
+++ b/tez-ui/src/main/webapp/app/scripts/controllers/dags_controller.js
@@ -124,7 +124,7 @@ App.DagsController = Em.ObjectController.extend(App.PaginatedContentMixin, App.C
         filterID: 'dagName_filter',
         tableCellViewClass: Em.Table.TableCell.extend({
           template: Em.Handlebars.compile(
-            "{{#link-to 'dag.vertices' view.cellContent.id class='ember-table-content'}}{{view.cellContent.name}}{{/link-to}}")
+            "{{#link-to 'dag.view' view.cellContent.id class='ember-table-content'}}{{view.cellContent.name}}{{/link-to}}")
         }),
         getCellContent: function(row) {
           return {

http://git-wip-us.apache.org/repos/asf/tez/blob/1784f015/tez-ui/src/main/webapp/app/scripts/helpers/dialogs.js
----------------------------------------------------------------------
diff --git a/tez-ui/src/main/webapp/app/scripts/helpers/dialogs.js b/tez-ui/src/main/webapp/app/scripts/helpers/dialogs.js
index 4acd082..abfc62b 100644
--- a/tez-ui/src/main/webapp/app/scripts/helpers/dialogs.js
+++ b/tez-ui/src/main/webapp/app/scripts/helpers/dialogs.js
@@ -49,10 +49,9 @@ App.Dialogs = Em.Namespace.create({
     });
 
     container.append('<ol class="selectable"> %@ </ol>'.fmt(listHTML));
-    $('#dialogContainer').append(container);
 
     return new Em.RSVP.Promise(function (resolve, reject) {
-      container.dialog({
+      var dialogOptions = {
         modal: true,
         title: title,
         width: 350,
@@ -78,7 +77,13 @@ App.Dialogs = Em.Namespace.create({
             container.remove();
           }
         }
-      });
+      };
+
+      if($('#dialog-container').length) {
+        dialogOptions.appendTo = '#dialog-container';
+      }
+
+      container.dialog(dialogOptions);
     });
   }
 });

http://git-wip-us.apache.org/repos/asf/tez/blob/1784f015/tez-ui/src/main/webapp/app/scripts/helpers/fullscreen.js
----------------------------------------------------------------------
diff --git a/tez-ui/src/main/webapp/app/scripts/helpers/fullscreen.js b/tez-ui/src/main/webapp/app/scripts/helpers/fullscreen.js
new file mode 100644
index 0000000..f0e31e5
--- /dev/null
+++ b/tez-ui/src/main/webapp/app/scripts/helpers/fullscreen.js
@@ -0,0 +1,55 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+App.Helpers.fullscreen = (function(){
+  function inFullscreenMode() {
+    return document.fullscreenElement ||
+        document.mozFullScreenElement ||
+        document.webkitFullscreenElement ||
+        document.msFullscreenElement;
+  }
+
+  return {
+    inFullscreenMode: inFullscreenMode,
+    toggle: function (element) {
+      if (inFullscreenMode()) {
+        if (document.exitFullscreen) {
+          document.exitFullscreen();
+        } else if (document.msExitFullscreen) {
+          document.msExitFullscreen();
+        } else if (document.mozCancelFullScreen) {
+          document.mozCancelFullScreen();
+        } else if (document.webkitExitFullscreen) {
+          document.webkitExitFullscreen();
+        }
+      } else {
+        if (element.requestFullscreen) {
+          element.requestFullscreen();
+        } else if (element.msRequestFullscreen) {
+          element.msRequestFullscreen();
+        } else if (element.mozRequestFullScreen) {
+          element.mozRequestFullScreen();
+        } else if (element.webkitRequestFullscreen) {
+          element.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
+        }
+      }
+
+      return inFullscreenMode();
+    }
+  };
+})();
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/tez/blob/1784f015/tez-ui/src/main/webapp/app/scripts/helpers/misc.js
----------------------------------------------------------------------
diff --git a/tez-ui/src/main/webapp/app/scripts/helpers/misc.js b/tez-ui/src/main/webapp/app/scripts/helpers/misc.js
index 69f0037..f115379 100644
--- a/tez-ui/src/main/webapp/app/scripts/helpers/misc.js
+++ b/tez-ui/src/main/webapp/app/scripts/helpers/misc.js
@@ -74,6 +74,13 @@ App.Helpers.misc = {
     return $.inArray(status, ['RUNNING', 'SUCCEEDED', 'FAILED', 'KILLED']) != -1;
   },
 
+  /**
+   * To trim a complete class path with namespace to the class name.
+   */
+  getClassName: function (classPath) {
+    return classPath.substr(classPath.lastIndexOf('.') + 1);
+  },
+
   /*
    * Normalizes counter style configurations
    * @param counterConfigs Array

http://git-wip-us.apache.org/repos/asf/tez/blob/1784f015/tez-ui/src/main/webapp/app/scripts/mixins/column-selector-mixin.js
----------------------------------------------------------------------
diff --git a/tez-ui/src/main/webapp/app/scripts/mixins/column-selector-mixin.js b/tez-ui/src/main/webapp/app/scripts/mixins/column-selector-mixin.js
index b90d75e..12b86c9 100644
--- a/tez-ui/src/main/webapp/app/scripts/mixins/column-selector-mixin.js
+++ b/tez-ui/src/main/webapp/app/scripts/mixins/column-selector-mixin.js
@@ -38,6 +38,8 @@ App.ColumnSelectorMixin = Em.Mixin.create({
   _storeKey: '',
   visibleColumnIds: {},
 
+  columnSelectorTitle: 'Column Selector',
+
   init: function(){
     var visibleColumnIds;
 
@@ -70,7 +72,7 @@ App.ColumnSelectorMixin = Em.Mixin.create({
     selectColumns: function () {
       var that = this;
 
-      App.Dialogs.displayMultiSelect('Column Selector', this.get('columnConfigs'), this.visibleColumnIds, {
+      App.Dialogs.displayMultiSelect(this.get('columnSelectorTitle'), this.get('columnConfigs'), this.visibleColumnIds, {
         displayText: 'headerCellName'
       }).then(function (data) {
         if(isObjectsDifferent(data, that.visibleColumnIds)) {

http://git-wip-us.apache.org/repos/asf/tez/blob/1784f015/tez-ui/src/main/webapp/app/scripts/models/TimelineRestAdapter.js
----------------------------------------------------------------------
diff --git a/tez-ui/src/main/webapp/app/scripts/models/TimelineRestAdapter.js b/tez-ui/src/main/webapp/app/scripts/models/TimelineRestAdapter.js
index 6794e7d..d2854df 100644
--- a/tez-ui/src/main/webapp/app/scripts/models/TimelineRestAdapter.js
+++ b/tez-ui/src/main/webapp/app/scripts/models/TimelineRestAdapter.js
@@ -110,6 +110,7 @@ var timelineJsonToDagMap = {
   planVersion: 'otherinfo.dagPlan.version',
   vertices: 'otherinfo.dagPlan.vertices',
   edges: 'otherinfo.dagPlan.edges',
+  vertexGroups: 'otherinfo.dagPlan.vertexGroups',
 
   counterGroups: 'counterGroups'
 };

http://git-wip-us.apache.org/repos/asf/tez/blob/1784f015/tez-ui/src/main/webapp/app/scripts/models/dag.js
----------------------------------------------------------------------
diff --git a/tez-ui/src/main/webapp/app/scripts/models/dag.js b/tez-ui/src/main/webapp/app/scripts/models/dag.js
index c729712..cc87f5d 100644
--- a/tez-ui/src/main/webapp/app/scripts/models/dag.js
+++ b/tez-ui/src/main/webapp/app/scripts/models/dag.js
@@ -51,6 +51,7 @@ App.Dag = App.AbstractEntity.extend({
   planVersion: DS.attr('number'),
   vertices: DS.attr('array'), // Serialize when required
   edges: DS.attr('array'), // Serialize when required
+  vertexGroups: DS.attr('array'),
 
   counterGroups: DS.hasMany('counterGroup', { inverse: 'parent' })
 });

http://git-wip-us.apache.org/repos/asf/tez/blob/1784f015/tez-ui/src/main/webapp/app/scripts/router.js
----------------------------------------------------------------------
diff --git a/tez-ui/src/main/webapp/app/scripts/router.js b/tez-ui/src/main/webapp/app/scripts/router.js
index ddc8365..34eb26e 100644
--- a/tez-ui/src/main/webapp/app/scripts/router.js
+++ b/tez-ui/src/main/webapp/app/scripts/router.js
@@ -20,6 +20,7 @@ App.Router.map(function() {
   this.resource('dags', { path: '/' });
   this.resource('dag', { path: '/dag/:dag_id'}, function() {
     this.route('vertices');
+    this.route('view');
     this.route('tasks');
     this.route('taskAttempts');
     this.route('counters');

http://git-wip-us.apache.org/repos/asf/tez/blob/1784f015/tez-ui/src/main/webapp/app/styles/dag-view.less
----------------------------------------------------------------------
diff --git a/tez-ui/src/main/webapp/app/styles/dag-view.less b/tez-ui/src/main/webapp/app/styles/dag-view.less
new file mode 100644
index 0000000..3deb074
--- /dev/null
+++ b/tez-ui/src/main/webapp/app/styles/dag-view.less
@@ -0,0 +1,347 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import "../bower_components/bootstrap/less/bootstrap";
+@import "../bower_components/font-awesome/less/font-awesome";
+
+@import "app/styles/colors.less";
+@import "app/styles/shared.less";
+
+// -- HTML styles ---
+.dag-view-container {
+  .no-select;
+  .well;
+
+  background-color: @bg-liter;
+
+  position: relative;
+
+  padding: 0px;
+  width:100%;
+  height: 100%;
+
+  .svg-container {
+    width:100%;
+    overflow:hidden;
+    height: 450px;
+
+    svg {
+      height: 450px;
+      width: 100%;
+    }
+  }
+
+  .button-panel {
+    .no-select;
+
+    position: absolute;
+    top: 10px;
+    right: 10px;
+
+    // Toggle buttons
+    .tgl-orientation, .tgl-additionals, .config, .timeline, .fit-graph, .tgl-fullscreen {
+      .fa;
+      .fa-border;
+      font-size: 20px;
+      border-radius: 5px;
+
+      cursor: pointer;
+
+      border-color: @border-lite;
+      background-color: @bg-lite;
+
+      &:hover {
+        color: @bg-lite;
+        background-color: @text-color;
+      }
+    }
+    .tgl-orientation {
+      .fa-icon(share-alt);
+    }
+    .tgl-additionals {
+      .fa-icon(circle);
+
+      &.hide-additionals {
+        .fa-icon(circle-o);
+      }
+    }
+    .config {
+      .fa-icon(cog);
+    }
+    .timeline {
+      .fa-icon(clock-o);
+    }
+    .fit-graph {
+      .fa-icon(arrows-alt);
+    }
+    .tgl-fullscreen {
+      .fa-icon(expand);
+    }
+
+    .seperator {
+      display: inline-block;
+      border-left: 1px dotted @text-color;
+      height: 13px;
+    }
+  }
+
+  .fullscreen {
+    height: 100%;
+    margin: 0px;
+    .svg-container, svg {
+      height: 100%;
+    }
+
+    .tgl-fullscreen {
+      .fa-icon(compress);
+    }
+  }
+
+  &:-webkit-full-screen {
+    .fullscreen;
+  }
+  &:fullscreen {
+    .fullscreen;
+  }
+  &:-moz-full-screen {
+    .fullscreen;
+  }
+}
+
+// -- SVG styles ---
+
+.grey-glow {
+  stroke: grey;
+}
+
+.vertex-node-bg {
+  .grey-glow;
+}
+
+.input-node-bg {
+  .grey-glow;
+}
+
+.output-node-bg {
+  .grey-glow;
+}
+
+.task-bubble-bg {
+  .grey-glow;
+}
+
+.group-bubble-bg {
+  .grey-glow;
+}
+
+.node {
+  cursor: pointer;
+
+  text {
+    .no-select;
+
+    pointer-events: none;
+    font: 12px sans-serif;
+    text-anchor: middle;
+
+    -webkit-transform: translate(0px, 4px); // For safari
+    -moz-transform: translate(0px, 4px);
+    transform: translate(0px, 4px);
+  }
+}
+
+.vertex {
+  text.title {
+    -webkit-transform: translate(0px, 3px); // For safari
+    transform: translate(0px, -1px);
+  }
+
+  .task-bubble {
+    -webkit-transform: translate(38px, -15px);
+    transform: translate(38px, -15px);
+    text {
+      letter-spacing: -1px;
+      text-anchor: middle;
+    }
+  }
+
+  .io-bubble {
+    -webkit-transform: translate(-38px, -15px);
+    transform: translate(-38px, -15px);
+    opacity: 0;
+    pointer-events: none;
+
+    -moz-transition: opacity .5s ease-in-out;
+    -webkit-transition: opacity .5s ease-in-out;
+    transition: opacity .5s ease-in-out;
+
+    text {
+      text-anchor: middle;
+    }
+  }
+
+  .group-bubble {
+    -webkit-transform: translate(38px, 15px);
+    transform: translate(38px, 15px);
+  }
+
+  .status-bar {
+    pointer-events: none;
+
+    .status {
+      -webkit-transform: translate(-35px, 2px);
+      transform: translate(-35px, 2px);
+      font: 8px Helvetica;
+      text-align: center;
+
+      .msg-container {
+        border-radius: 5px;
+        padding: 2px 3px 0px 1px;
+        background-color: rgba(255, 255, 255, 0.3);
+
+        .task-status {
+          -webkit-animation: none !important;
+          font-size: 12px;
+          margin-top: 3px;
+          background-color: rgba(255, 255, 255, 0.8);
+          border-radius: 6px;
+
+          width: 10px;
+          height: 8px;
+        }
+      }
+    }
+  }
+}
+
+.hide-io {
+  .vertex {
+    .io-bubble {
+      opacity: 1;
+      pointer-events: auto;
+    }
+  }
+}
+
+.link {
+  fill: none;
+  stroke: #ccc;
+  stroke-width: 3px;
+
+  &.broadcast {
+    stroke: #ccbb8f;
+  }
+}
+
+// -- Tooltip style ---
+.tool-tip {
+  .no-select;
+
+  position: fixed;
+  pointer-events: none;
+  display: none;
+
+  max-width: 420px;
+
+  .sub {
+    font-size: 10px;
+  }
+
+  .bubble {
+    margin-left: -11px; // Border radious + arrow margin-left
+
+    position: relative;
+    padding: 10px;
+
+    font-family: helvetica;
+    background: rgba(0, 0, 0, 0.8);
+    color: #fff;
+    border-radius: 5px;
+
+    .tip-title {
+      text-align: center;
+      font-size: 1.1em;
+    }
+    .tip-text {
+    }
+    .tip-list {
+      table {
+        table-layout:fixed;
+
+        border-top: 1px solid rgba(255, 255, 255, 0.4);
+
+        td {
+          overflow: hidden;
+          white-space: nowrap;
+          max-width: 200;
+        }
+        td:nth-child(1) {
+          padding-right: 10px;
+        }
+        td:nth-child(2) {
+          text-align: right;
+          padding-left: 10px;
+          border-left: 1px solid rgba(255, 255, 255, 0.4);
+        }
+      }
+    }
+  }
+
+  &.show {
+    display: inline-block;
+  }
+
+  &.below:after, &.above:before {
+    display: inline;
+    box-sizing: border-box;
+
+    font-size: 12px;
+    line-height: 9px;
+
+    color: rgba(0, 0, 0, 0.8);
+    margin-left: -6px; // Half of font size
+  }
+
+  &.above {
+    margin-top: 10px;
+    .bubble {
+      margin-top: -5px;
+    }
+
+    &:before {
+      content: "\25B2";
+    }
+  }
+
+  &.below {
+    margin-top: -12px;
+    .bubble {
+      margin-bottom: -7px;
+    }
+
+    &:after {
+      content: "\25BC";
+    }
+  }
+}
+
+.dag-view-legend {
+  margin-top: -20px;
+  font-size: .7em;
+  text-align: right;
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/tez/blob/1784f015/tez-ui/src/main/webapp/app/styles/main.less
----------------------------------------------------------------------
diff --git a/tez-ui/src/main/webapp/app/styles/main.less b/tez-ui/src/main/webapp/app/styles/main.less
index 57fa437..34b44d9 100644
--- a/tez-ui/src/main/webapp/app/styles/main.less
+++ b/tez-ui/src/main/webapp/app/styles/main.less
@@ -19,6 +19,7 @@
 // Imports
 @import "../bower_components/font-awesome/less/font-awesome";
 @import "../bower_components/bootstrap/less/bootstrap"; //UI theme
+@import "app/styles/dag-view";
 
 // Colors
 @import "app/styles/colors";

http://git-wip-us.apache.org/repos/asf/tez/blob/1784f015/tez-ui/src/main/webapp/app/templates/components/dag-view.hbs
----------------------------------------------------------------------
diff --git a/tez-ui/src/main/webapp/app/templates/components/dag-view.hbs b/tez-ui/src/main/webapp/app/templates/components/dag-view.hbs
new file mode 100644
index 0000000..a41f772
--- /dev/null
+++ b/tez-ui/src/main/webapp/app/templates/components/dag-view.hbs
@@ -0,0 +1,108 @@
+{{!
+* Licensed to the Apache Software Foundation (ASF) under one
+* or more contributor license agreements.  See the NOTICE file
+* distributed with this work for additional information
+* regarding copyright ownership.  The ASF licenses this file
+* to you under the Apache License, Version 2.0 (the
+* "License"); you may not use this file except in compliance
+* with the License.  You may obtain a copy of the License at
+*
+*     http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+}}
+
+{{#if errMessage}}
+  <div class="text-align-center">
+    <h1>Rendering failed! </h1><h5>{{errMessage}}</h5>
+  </div>
+{{else}}
+  <div class="svg-container">
+    <svg>
+      <defs>
+        <rect id="vertex-bg"
+            class="vertex-node-bg"
+            style="fill: url(#vertex-grad); filter: url(#grey-glow)"
+            rx="5" ry="5" width="80" height="30" x="-40" y="-15"/>
+        <circle id="input-bg" class="input-node-bg"
+            style="fill: url(#input-grad); filter: url(#grey-glow)"
+            cx="0" cy="0" r="15"/>
+        <circle id="output-bg" class="output-node-bg"
+            style="fill: url(#output-grad); filter: url(#grey-glow)"
+            cx="0" cy="0" r="15"/>
+        <circle id="task-bubble" class="task-bubble-bg"
+            style="fill: url(#task-grad); filter: url(#grey-glow)"
+            r="10"/>
+        <circle id="io-bubble" class="input-node-bg"
+            style="fill: url(#input-grad); filter: url(#grey-glow)"
+            r="10"/>
+        <circle id="group-bubble" class="group-bubble-bg"
+            style="fill: url(#group-grad); filter: url(#grey-glow)"
+            r="8"/>
+
+        <marker id="arrow-marker" viewBox="0 -5 10 10" markerWidth="2" markerHeight="2" orient="auto">
+          <path style="fill: #AAA;" d="M0,-5L10,0L0,5"/>
+        </marker>
+
+        <radialGradient id="vertex-grad" cx="50%" cy="50%" r="100%" fx="50%" fy="50%">
+          <stop offset="0%" style="stop-color:#b0c4de;" />
+          <stop offset="100%" style="stop-color:#769ccd;" />
+        </radialGradient result="gradient">
+
+        <radialGradient id="input-grad" cx="50%" cy="50%" r="100%" fx="50%" fy="50%">
+          <stop offset="0%" style="stop-color:#adff2e;" />
+          <stop offset="100%" style="stop-color:#91d723;" />
+        </radialGradient result="gradient">
+
+        <radialGradient id="output-grad" cx="50%" cy="50%" r="100%" fx="50%" fy="50%">
+          <stop offset="0%" style="stop-color:#fa8072;" />
+          <stop offset="100%" style="stop-color:#d26457;" />
+        </radialGradient result="gradient">
+
+        <radialGradient id="task-grad" cx="50%" cy="50%" r="100%" fx="50%" fy="50%">
+          <stop offset="0%" style="stop-color:#D0BFD1;" />
+          <stop offset="100%" style="stop-color:#af8fb1;" />
+        </radialGradient result="gradient">
+
+        <radialGradient id="group-grad" cx="50%" cy="50%" r="100%" fx="50%" fy="50%">
+          <stop offset="0%" style="stop-color:#BBBBBB;" />
+          <stop offset="100%" style="stop-color:#999999;" />
+        </radialGradient result="gradient">
+
+        <filter id="grey-glow">
+          <feColorMatrix type="matrix" values=
+              "0 0 0 0   0
+               0 0 0 0   0
+               0 0 0 0   0
+               0 0 0 0.3 0"/>
+          <feGaussianBlur stdDeviation="2.5" result="coloredBlur"/>
+          <feMerge>
+            <feMergeNode in="coloredBlur"/>
+            <feMergeNode in="SourceGraphic"/>
+          </feMerge>
+        </filter>
+      </defs>
+    </svg>
+  </div>
+  <div class="button-panel">
+    <i {{bind-attr class=':tgl-additionals hideAdditionals'}} {{action 'tglAdditionals'}} title="Toggle source/sink visibility"></i>
+    <i {{bind-attr class=':config :jq-tooltip'}} {{action 'configure'}} title="Customize vertex tooltip"></i>
+    <i {{bind-attr class=':timeline :no-display'}} {{action 'configure'}} title="Toggle Timeline"></i>
+    <i class="seperator"></i>
+    <i {{bind-attr class=':tgl-orientation isHorizontal:fa-rotate-270:fa-rotate-180'}} {{action 'tglOrientation'}} title="Toggle orientation"></i>
+    <i {{bind-attr class=':fit-graph'}} {{action 'fitGraph'}} title="Fit DAG to viewport"></i>
+    <i {{bind-attr class=':tgl-fullscreen'}} {{action 'fullscreen'}} title="Toggle fullscreen"></i>
+  </div>
+  <div class="tool-tip">
+    <div class="bubble">
+      <div class="tip-title">Title</div>
+      <div class="tip-text"></div>
+      <div class="tip-list"></div>
+    </div>
+  </div>
+  <div id="dialog-container"></div>
+{{/if}}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/tez/blob/1784f015/tez-ui/src/main/webapp/app/templates/dag/view.hbs
----------------------------------------------------------------------
diff --git a/tez-ui/src/main/webapp/app/templates/dag/view.hbs b/tez-ui/src/main/webapp/app/templates/dag/view.hbs
new file mode 100644
index 0000000..1057a20
--- /dev/null
+++ b/tez-ui/src/main/webapp/app/templates/dag/view.hbs
@@ -0,0 +1,29 @@
+{{!
+* Licensed to the Apache Software Foundation (ASF) under one
+* or more contributor license agreements.  See the NOTICE file
+* distributed with this work for additional information
+* regarding copyright ownership.  The ASF licenses this file
+* to you under the Apache License, Version 2.0 (the
+* "License"); you may not use this file except in compliance
+* with the License.  You may obtain a copy of the License at
+*
+*     http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+}}
+
+{{#unless loading}}
+  {{dag-view-component
+    data=viewData
+    vertexProperties=columns
+    entityClicked='entityClicked'
+    configure='selectColumns'
+  }}
+  <div class="dag-view-legend">Double click source/sink bubble to toggle visibility locally.</div>
+{{else}}
+  {{partial 'partials/loading-spinner'}}
+{{/unless}}
\ No newline at end of file


Mime
View raw message