You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by on...@apache.org on 2014/07/29 14:00:44 UTC

[1/2] AMBARI-6651. Create job-details page. (onechiporenko)

Repository: ambari
Updated Branches:
  refs/heads/trunk 569acf801 -> 85362043f


http://git-wip-us.apache.org/repos/asf/ambari/blob/85362043/contrib/views/jobs/src/main/resources/ui/app/scripts/views/job/hive_job_details_view.js
----------------------------------------------------------------------
diff --git a/contrib/views/jobs/src/main/resources/ui/app/scripts/views/job/hive_job_details_view.js b/contrib/views/jobs/src/main/resources/ui/app/scripts/views/job/hive_job_details_view.js
new file mode 100644
index 0000000..122f236
--- /dev/null
+++ b/contrib/views/jobs/src/main/resources/ui/app/scripts/views/job/hive_job_details_view.js
@@ -0,0 +1,327 @@
+/**
+ * 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.JobView = Em.View.extend({
+
+  templateName: 'job/job',
+
+  selectedVertex: null,
+
+  content: null,
+
+  zoomScaleFrom: 1,
+
+  zoomScaleTo: 2,
+
+  zoomScale: 1,
+
+  showQuery: false,
+
+  willInsertElement: function () {
+    this.get('controller').loadJobDetails();
+  },
+
+  zoomStep: function () {
+    var zoomStep = 0.01;
+    var zoomFrom = this.get('zoomScaleFrom');
+    var zoomTo = this.get('zoomScaleTo');
+    if (zoomFrom < zoomTo) {
+      zoomStep = (zoomTo - zoomFrom) / 5;
+    }
+    return zoomStep;
+  }.property('zoomScaleFrom', 'zoomScaleTo'),
+
+  isGraphMaximized: false,
+
+  actions: {
+
+    doSelectSummaryMetricType: function (summaryType) {
+      switch (summaryType) {
+        case Em.I18n.t('jobs.hive.tez.metric.input'):
+          summaryType = 'input';
+          break;
+        case Em.I18n.t('jobs.hive.tez.metric.output'):
+          summaryType = 'output';
+          break;
+        case Em.I18n.t('jobs.hive.tez.metric.recordsRead'):
+          summaryType = 'recordsRead';
+          break;
+        case Em.I18n.t('jobs.hive.tez.metric.recordsWrite'):
+          summaryType = 'recordsWrite';
+          break;
+        case Em.I18n.t('jobs.hive.tez.metric.tezTasks'):
+          summaryType = 'tezTasks';
+          break;
+        case Em.I18n.t('jobs.hive.tez.metric.spilledRecords'):
+          summaryType = 'spilledRecords';
+          break;
+        default:
+          break;
+      }
+      this.set('summaryMetricType', summaryType);
+    },
+
+    toggleShowQuery: function () {
+      this.toggleProperty('showQuery');
+      var queryBlock = $('.query-info');
+      if (this.get('showQuery')) {
+        queryBlock.slideDown();
+      }
+      else {
+        queryBlock.slideUp();
+      }
+    },
+
+    actionDoSelectVertex: function (event, notTableClick) {
+      this.doSelectVertex(event, notTableClick);
+    },
+
+    doGraphZoomIn: function () {
+      var zoomTo = this.get('zoomScaleTo'),
+        zoomScale = this.get('zoomScale'),
+        zoomStep = this.get('zoomStep');
+      if (zoomScale < zoomTo) {
+        var step = Math.min(zoomStep, (zoomTo - zoomScale));
+        zoomScale += step;
+        this.set('zoomScale', zoomScale);
+      }
+    },
+
+    doGraphZoomOut: function () {
+      var zoomFrom = this.get('zoomScaleFrom'),
+        zoomScale = this.get('zoomScale'),
+        zoomStep = this.get('zoomStep');
+      if (zoomScale > zoomFrom) {
+        var step = Math.min(zoomStep, (zoomScale - zoomFrom));
+        zoomScale -= step;
+        this.set('zoomScale', zoomScale);
+      }
+    },
+
+    doGraphMaximize: function () {
+      this.set('isGraphMaximized', true);
+    },
+
+    doGraphMinimize: function () {
+      this.set('isGraphMaximized', false);
+    }
+
+  },
+
+  toggleShowQueryText: function () {
+    return this.get('showQuery') ? Em.I18n.t('jobs.hive.less') : Em.I18n.t('jobs.hive.more');
+  }.property('showQuery'),
+
+  summaryMetricType: 'input',
+
+  summaryMetricTypesDisplay: [
+    Em.I18n.t('jobs.hive.tez.metric.input'),
+    Em.I18n.t('jobs.hive.tez.metric.output'),
+    /* Em.I18n.t('jobs.hive.tez.metric.recordsRead'),
+     Em.I18n.t('jobs.hive.tez.metric.recordsWrite'), */
+    Em.I18n.t('jobs.hive.tez.metric.tezTasks'),
+    Em.I18n.t('jobs.hive.tez.metric.spilledRecords')
+  ],
+
+  summaryMetricTypeDisplay: function () {
+    return Em.I18n.t('jobs.hive.tez.metric.' + this.get('summaryMetricType'));
+  }.property('summaryMetricType'),
+
+  sortedVertices: function () {
+    var sortColumn = this.get('controller.sortingColumn');
+    if (sortColumn && sortColumn.get('status')) {
+      var sortColumnStatus = sortColumn.get('status');
+      var sorted = sortColumn.get('parentView').sort(sortColumn, sortColumnStatus === "sorting_desc", true);
+      sortColumn.set('status', sortColumnStatus);
+      return sorted;
+    }
+    var vertices = this.get('content.tezDag.vertices');
+    if (vertices != null) {
+      vertices = vertices.toArray();
+    }
+    return vertices;
+  }.property('content.tezDag.vertices', 'controller.sortingColumn'),
+
+  initialDataLoaded: function () {
+    if (this.get('controller.loaded')) {
+      this.set('content', this.get('controller.content'));
+    }
+  }.observes('controller.loaded'),
+
+  jobObserver: function () {
+    var content = this.get('content'),
+      selectedVertex = this.get('selectedVertex');
+    if (selectedVertex == null && content != null) {
+      var vertices = content.get('tezDag.vertices');
+      if (vertices) {
+        vertices.setEach('isSelected', false);
+        this.doSelectVertex(vertices.objectAt(0), false);
+      }
+    }
+  }.observes('selectedVertex', 'content.tezDag.vertices.@each.id'),
+
+  doSelectVertex: function (newVertex, notTableClick) {
+    var currentVertex = this.get('selectedVertex');
+    if (currentVertex != null) {
+      currentVertex.set('isSelected', false);
+    }
+    newVertex.set('notTableClick', !!notTableClick);
+    newVertex.set('isSelected', true);
+    this.set('selectedVertex', newVertex);
+  },
+
+  /**
+   * Provides display information for vertex I/O.
+   *
+   * {
+   *  'file': {
+   *    'read': {
+   *      'ops': '100 reads',
+   *      'bytes': '10 MB'
+   *    }
+   *    'write: {
+   *      'ops': '200 writes',
+   *      'bytes': '20 MB'
+   *    }
+   *  },
+   *  'hdfs': {
+   *    'read': {
+   *      'ops': '100 reads',
+   *      'bytes': '10 MB'
+   *    }
+   *    'write: {
+   *      'ops': '200 writes',
+   *      'bytes': '20 MB'
+   *    }
+   *  },
+   *  'records': {
+   *    'read': '100 records',
+   *    'write': '123 records'
+   *  },
+   *  'started': 'Feb 12, 2014 10:30am',
+   *  'ended': 'Feb 12, 2014 10:35am',
+   *  'status': 'Running'
+   * }
+   */
+  selectedVertexIODisplay: {},
+
+  selectedVertexIODisplayObsOnce: function() {
+    Em.run.once(this, 'selectedVertexIODisplayObs');
+  }.observes('selectedVertex.fileReadOps', 'selectedVertex.fileWriteOps', 'selectedVertex.hdfsReadOps', 'selectedVertex.hdfdWriteOps',
+      'selectedVertex.fileReadBytes', 'selectedVertex.fileWriteBytes', 'selectedVertex.hdfsReadBytes', 'selectedVertex.hdfdWriteBytes',
+      'selectedVertex.recordReadCount', 'selectedVertex.recordWriteCount', 'selectedVertex.status'),
+
+  selectedVertexIODisplayObs: function () {
+    var v = this.get('selectedVertex'),
+      naString = Em.I18n.t('common.na'),
+      status = App.Helpers.string.getCamelCase(v.getWithDefault('state', '')),
+      fileReadOps = v.getWithDefault('fileReadOps', naString),
+      fileWriteOps = v.getWithDefault('fileWriteOps', naString),
+      hdfsReadOps = v.getWithDefault('hdfsReadOps', naString),
+      hdfsWriteOps = v.getWithDefault('hdfsWriteOps', naString),
+      r = {
+      file: {
+        read: {
+          ops: Em.I18n.t('jobs.hive.tez.reads').fmt(fileReadOps),
+          bytes: App.Helpers.number.bytesToSize(v.get('fileReadBytes'))
+        },
+        write: {
+          ops: Em.I18n.t('jobs.hive.tez.writes').fmt(fileWriteOps),
+          bytes: App.Helpers.number.bytesToSize(v.get('fileWriteBytes'))
+        }
+      },
+      hdfs: {
+        read: {
+          ops: Em.I18n.t('jobs.hive.tez.reads').fmt(hdfsReadOps),
+          bytes: App.Helpers.number.bytesToSize(v.get('hdfsReadBytes'))
+        },
+        write: {
+          ops: Em.I18n.t('jobs.hive.tez.writes').fmt(hdfsWriteOps),
+          bytes: App.Helpers.number.bytesToSize(v.get('hdfsWriteBytes'))
+        }
+      },
+      records: {
+        read: v.get('recordReadCount') == null ? null : Em.I18n.t('jobs.hive.tez.records.count').fmt(v.get('recordReadCount')),
+        write: v.get('recordWriteCount') == null ? null : Em.I18n.t('jobs.hive.tez.records.count').fmt(v.get('recordWriteCount'))
+      },
+      started: v.get('startTime') ? App.Helpers.date.dateFormat(v.get('startTime'), true, true) : '',
+      ended: v.get('endTime') ? App.Helpers.date.dateFormat(v.get('endTime'), true, true) : '',
+      status: status
+    };
+    this.set('selectedVertexIODisplay', r);
+  },
+
+  canGraphZoomIn: function () {
+    return this.get('zoomScale') < this.get('zoomScaleTo');
+  }.property('zoomScale', 'zoomScaleTo'),
+
+  canGraphZoomOut: function () {
+    return this.get('zoomScale') > this.get('zoomScaleFrom');
+  }.property('zoomScale', 'zoomScaleFrom')
+
+});
+
+App.MainHiveJobDetailsVerticesTableView = App.TableView.extend({
+
+  sortView: App.Sorts.wrapperView,
+
+  didInsertElement: function () {
+    if (!this.get('controller.sortingColumn')) {
+      var columns = this.get('childViews')[0].get('childViews');
+      if (columns && columns.findProperty('name', 'name')) {
+        columns.findProperty('name', 'name').set('status', 'sorting_asc');
+        this.get('controller').set('sortingColumn', columns.findProperty('name', 'name'))
+      }
+    }
+  },
+
+  nameSort: App.Sorts.fieldView.extend({
+    column: 0,
+    name: 'name',
+    displayName: Em.I18n.t('common.name'),
+    type: 'string'
+  }),
+
+  tasksSort: App.Sorts.fieldView.extend({
+    column: 1,
+    name: 'tasksNumber',
+    displayName: Em.I18n.t('common.tasks'),
+    type: 'number'
+  }),
+
+  inputSort: App.Sorts.fieldView.extend({
+    column: 2,
+    name: 'totalReadBytes',
+    displayName: Em.I18n.t('apps.item.dag.input'),
+    type: 'number'
+  }),
+
+  outputSort: App.Sorts.fieldView.extend({
+    column: 3,
+    name: 'totalWriteBytes',
+    displayName: Em.I18n.t('apps.item.dag.output'),
+    type: 'number'
+  }),
+
+  durationSort: App.Sorts.fieldView.extend({
+    column: 4,
+    name: 'duration',
+    displayName: Em.I18n.t('apps.item.dag.duration'),
+    type: 'number'
+  })
+
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/85362043/contrib/views/jobs/src/main/resources/ui/app/scripts/views/job_view.js
----------------------------------------------------------------------
diff --git a/contrib/views/jobs/src/main/resources/ui/app/scripts/views/job_view.js b/contrib/views/jobs/src/main/resources/ui/app/scripts/views/job_view.js
deleted file mode 100644
index eae86c2..0000000
--- a/contrib/views/jobs/src/main/resources/ui/app/scripts/views/job_view.js
+++ /dev/null
@@ -1,19 +0,0 @@
-/**
- * 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.JobView = Ember.View.extend({});

http://git-wip-us.apache.org/repos/asf/ambari/blob/85362043/contrib/views/jobs/src/main/resources/ui/app/scripts/views/jobs_view.js
----------------------------------------------------------------------
diff --git a/contrib/views/jobs/src/main/resources/ui/app/scripts/views/jobs_view.js b/contrib/views/jobs/src/main/resources/ui/app/scripts/views/jobs_view.js
index 9d814d6..279298b 100644
--- a/contrib/views/jobs/src/main/resources/ui/app/scripts/views/jobs_view.js
+++ b/contrib/views/jobs/src/main/resources/ui/app/scripts/views/jobs_view.js
@@ -219,9 +219,9 @@ App.JobsView = App.TableView.extend({
     templateName: 'jobs/jobs_name',
 
     click: function(event) {
-      /*if (this.get('job.hasTezDag')) {
-        App.router.transitionTo('main.jobs.jobDetails', this.get('job'));
-      }*/
+      if (this.get('job.hasTezDag')) {
+        this.get('controller').transitionToRoute('job', this.get('job'));
+      }
       return false;
     },
 

http://git-wip-us.apache.org/repos/asf/ambari/blob/85362043/contrib/views/jobs/src/main/resources/ui/app/styles/main.less
----------------------------------------------------------------------
diff --git a/contrib/views/jobs/src/main/resources/ui/app/styles/main.less b/contrib/views/jobs/src/main/resources/ui/app/styles/main.less
index 16cfba7..dafb275 100644
--- a/contrib/views/jobs/src/main/resources/ui/app/styles/main.less
+++ b/contrib/views/jobs/src/main/resources/ui/app/styles/main.less
@@ -16,8 +16,74 @@
  * limitations under the License.
  */
 
-@import '../../app/bower_components/jquery-ui/themes/base/all.css';
+@import '../../app/bower_components/jquery-ui/themes/base/core.css';
+@import '../../app/bower_components/jquery-ui/themes/base/datepicker.css';
 @import '../../app/bower_components/bootstrap/less/bootstrap';
+@import '../../app/bower_components/font-awesome/less/font-awesome';
+
+html {
+  overflow-y: scroll;
+}
+
+a {
+  cursor: pointer;
+}
+
+/*.tooltip {
+  z-index: 10000;
+}*/
+
+.icon-remove-sign {
+  color: #FF4B4B;
+}
+
+
+/*.tooltip-inner {
+  text-align: left;
+}*/
+
+/*.jobs-tooltip {
+  .tooltip-inner {
+    max-width: 400px;
+  }
+}*/
+
+.box {
+  border: 1px solid #D4D4D4;
+  margin-bottom: 20px;
+
+  .box-header {
+    border-bottom: 1px solid #D4D4D4;
+  }
+  .box-header,
+  .box-footer {
+    padding: 4px 4px 4px 10px;
+    background: #f5f5f5;
+  }
+  .box-header:after,
+  .box-footer:after {
+    content: "";
+    display: table;
+    clear: both;
+  }
+  .box-header {
+    .btn-group {
+      float: right;
+    }
+    h4 {
+      float: left;
+      margin: 5px;
+      font-size: 15px;
+      color: #777;
+    }
+  }
+  .box-footer {
+    hr {
+      margin: 0px;
+    }
+  }
+}
+
 
 #jobs {
 
@@ -32,12 +98,12 @@
     margin-top: -20px;
   }
 
-  #filtered-jobs{
+  #filtered-jobs {
     float: left;
     margin-top: 8px;
   }
 
-  .jobs_head{
+  .jobs_head {
     height: 30px;
   }
 
@@ -51,16 +117,16 @@
     }
     div {
       display: inline-block;
-      margin:0 10px;
+      margin: 0 10px;
     }
     .items-on-page {
       label {
-        display:inline;
+        display: inline;
       }
       select {
         margin-bottom: 4px;
         margin-top: 4px;
-        width:70px;
+        width: 70px;
         font-size: 12px;
         height: 27px;
       }
@@ -68,11 +134,11 @@
 
     .paging_two_button {
       a {
-        padding:0 5px;
+        padding: 0 5px;
       }
       a.paginate_disabled_next, a.paginate_disabled_previous {
         color: gray;
-        &:hover i{
+        &:hover i {
           color: gray;
           text-decoration: none;
           cursor: default;
@@ -90,7 +156,7 @@
 
   #jobs-table {
 
-    .is-not-link{
+    .is-not-link {
       cursor: default;
       color: #000000;
       text-decoration: none;
@@ -104,7 +170,7 @@
       line-height: 22px;
     }
 
-    .input-120{
+    .input-120 {
       width: 120px;
     }
 
@@ -167,7 +233,7 @@
     th, td {
       border-left-width: 0;
     }
-    .no-data{
+    .no-data {
       text-align: center;
     }
     a.job-link {
@@ -190,17 +256,17 @@
       width: 36%;
     }
     td:first-child + td + td,
-    th:first-child + th + th{
+    th:first-child + th + th {
       width: 20%;
     }
     td:first-child + td + td + td,
     th:first-child + th + th + th,
     td:first-child + td + td + td + td,
-    th:first-child + th + th + th + th{
+    th:first-child + th + th + th + th {
       width: 16%;
     }
     td:first-child + td + td + td + td + td,
-    th:first-child + th + th + th + th + th{
+    th:first-child + th + th + th + th + th {
       width: 12%;
     }
   }
@@ -276,14 +342,13 @@
     }
 
     .sorting_asc {
-      background:
-        url(data:image/png;base64,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) no-repeat right 50%;
+      background: url(data:image/png;base64,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) no-repeat right 50
 %;
     }
     .sorting_desc {
       background: url(data:image/png;base64,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) no-repeat right 50%;
     }
     .sorting {
-      background: url(	data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAKQWlDQ1BJQ0MgUHJvZmlsZQAASA2dlndUU9kWh8+9N73QEiIgJfQaegkg0jtIFQRRiUmAUAKGhCZ2RAVGFBEpVmRUwAFHhyJjRRQLg4Ji1wnyEFDGwVFEReXdjGsJ7601896a/cdZ39nnt9fZZ+9917oAUPyCBMJ0WAGANKFYFO7rwVwSE8vE9wIYEAEOWAHA4WZmBEf4RALU/L09mZmoSMaz9u4ugGS72yy/UCZz1v9/kSI3QyQGAApF1TY8fiYX5QKUU7PFGTL/BMr0lSkyhjEyFqEJoqwi48SvbPan5iu7yZiXJuShGlnOGbw0noy7UN6aJeGjjAShXJgl4GejfAdlvVRJmgDl9yjT0/icTAAwFJlfzOcmoWyJMkUUGe6J8gIACJTEObxyDov5OWieAHimZ+SKBIlJYqYR15hp5ejIZvrxs1P5YjErlMNN4Yh4TM/0tAyOMBeAr2+WRQElWW2ZaJHtrRzt7VnW5mj5v9nfHn5T/T3IevtV8Sbsz55BjJ5Z32zsrC+9FgD2JFqbHbO+lVUAtG0GQOXhrE/vIADyBQC03pzzHoZsXpLE4gwnC4vs7GxzAZ9rLivoN/ufgm/Kv4Y595nL7vtWO6YXP4EjSRUzZUXlpqemS0TMzAwOl89k/fcQ/+PAOWnNycMsnJ/AF/GF6FVR6JQJhIlou4U8gViQLmQKhH/V4X8YNicHGX6daxRodV8AfYU5ULhJB8hvPQBDIwMkbj96An3rWxAxCsi+vGitka9zjzJ6/uf6Hwtcim7hTEEiU+b2DI9kciWiLBmj34RswQISkAd0oAo0gS4wAixgDRyAM3AD3iAAhIBIEAOWAy5IAmlABLJBPtgACkEx2AF2g2pwANSBetAEToI2cAZcBFfADXALDIBHQAqGwUsw
 Ad6BaQiC8BAVokGqkBakD5lC1hAbWgh5Q0FQOBQDxUOJkBCSQPnQJqgYKoOqoUNQPfQjdBq6CF2D+qAH0CA0Bv0BfYQRmALTYQ3YALaA2bA7HAhHwsvgRHgVnAcXwNvhSrgWPg63whfhG/AALIVfwpMIQMgIA9FGWAgb8URCkFgkAREha5EipAKpRZqQDqQbuY1IkXHkAwaHoWGYGBbGGeOHWYzhYlZh1mJKMNWYY5hWTBfmNmYQM4H5gqVi1bGmWCesP3YJNhGbjS3EVmCPYFuwl7ED2GHsOxwOx8AZ4hxwfrgYXDJuNa4Etw/XjLuA68MN4SbxeLwq3hTvgg/Bc/BifCG+Cn8cfx7fjx/GvyeQCVoEa4IPIZYgJGwkVBAaCOcI/YQRwjRRgahPdCKGEHnEXGIpsY7YQbxJHCZOkxRJhiQXUiQpmbSBVElqIl0mPSa9IZPJOmRHchhZQF5PriSfIF8lD5I/UJQoJhRPShxFQtlOOUq5QHlAeUOlUg2obtRYqpi6nVpPvUR9Sn0vR5Mzl/OX48mtk6uRa5Xrl3slT5TXl3eXXy6fJ18hf0r+pvy4AlHBQMFTgaOwVqFG4bTCPYVJRZqilWKIYppiiWKD4jXFUSW8koGStxJPqUDpsNIlpSEaQtOledK4tE20Otpl2jAdRzek+9OT6cX0H+i99AllJWVb5SjlHOUa5bPKUgbCMGD4M1IZpYyTjLuMj/M05rnP48/bNq9pXv+8KZX5Km4qfJUilWaVAZWPqkxVb9UU1Z2qbapP1DBqJmphatlq+9Uuq43Pp893ns+dXzT/5PyH6rC6iXq4+mr1w+o96pMamhq+GhkaVRqXNMY1GZpumsma5ZrnNMe0aFoLtQRa5VrntV4wlZnuzFRmJbOLOaGtru2nLdE+pN2rPa1jqLNYZ6NOs84TXZIuWzdBt1y3U3dCT0svWC9fr1HvoT5Rn62fpL9Hv1t/ysDQINpgi0GbwaihiqG/YZ5ho
 +FjI6qRq9Eqo1qjO8Y4Y7ZxivE+41smsImdSZJJjclNU9jU3lRgus+0zwxr5mgmNKs1u8eisNxZWaxG1qA5wzzIfKN5m/krCz2LWIudFt0WXyztLFMt6ywfWSlZBVhttOqw+sPaxJprXWN9x4Zq42Ozzqbd5rWtqS3fdr/tfTuaXbDdFrtOu8/2DvYi+yb7MQc9h3iHvQ732HR2KLuEfdUR6+jhuM7xjOMHJ3snsdNJp9+dWc4pzg3OowsMF/AX1C0YctFx4bgccpEuZC6MX3hwodRV25XjWuv6zE3Xjed2xG3E3dg92f24+ysPSw+RR4vHlKeT5xrPC16Il69XkVevt5L3Yu9q76c+Oj6JPo0+E752vqt9L/hh/QL9dvrd89fw5/rX+08EOASsCegKpARGBFYHPgsyCRIFdQTDwQHBu4IfL9JfJFzUFgJC/EN2hTwJNQxdFfpzGC4sNKwm7Hm4VXh+eHcELWJFREPEu0iPyNLIR4uNFksWd0bJR8VF1UdNRXtFl0VLl1gsWbPkRoxajCCmPRYfGxV7JHZyqffS3UuH4+ziCuPuLjNclrPs2nK15anLz66QX8FZcSoeGx8d3xD/iRPCqeVMrvRfuXflBNeTu4f7kufGK+eN8V34ZfyRBJeEsoTRRJfEXYljSa5JFUnjAk9BteB1sl/ygeSplJCUoykzqdGpzWmEtPi000IlYYqwK10zPSe9L8M0ozBDuspp1e5VE6JA0ZFMKHNZZruYjv5M9UiMJJslg1kLs2qy3mdHZZ/KUcwR5vTkmuRuyx3J88n7fjVmNXd1Z752/ob8wTXuaw6thdauXNu5Tnddwbrh9b7rj20gbUjZ8MtGy41lG99uit7UUaBRsL5gaLPv5sZCuUJR4b0tzlsObMVsFWzt3WazrWrblyJe0fViy+KK4k8l3JLr31l9V/ndzPaE7b2l9qX7d+B2CHfc3em681iZYlle2dCu4F2t5czyovK3u1fsvlZhW3
 FgD2mPZI+0MqiyvUqvakfVp+qk6oEaj5rmvep7t+2d2sfb17/fbX/TAY0DxQc+HhQcvH/I91BrrUFtxWHc4azDz+ui6rq/Z39ff0TtSPGRz0eFR6XHwo911TvU1zeoN5Q2wo2SxrHjccdv/eD1Q3sTq+lQM6O5+AQ4ITnx4sf4H++eDDzZeYp9qukn/Z/2ttBailqh1tzWibakNml7THvf6YDTnR3OHS0/m/989Iz2mZqzymdLz5HOFZybOZ93fvJCxoXxi4kXhzpXdD66tOTSna6wrt7LgZevXvG5cqnbvfv8VZerZ645XTt9nX297Yb9jdYeu56WX+x+aem172296XCz/ZbjrY6+BX3n+l37L972un3ljv+dGwOLBvruLr57/17cPel93v3RB6kPXj/Mejj9aP1j7OOiJwpPKp6qP6391fjXZqm99Oyg12DPs4hnj4a4Qy//lfmvT8MFz6nPK0a0RupHrUfPjPmM3Xqx9MXwy4yX0+OFvyn+tveV0auffnf7vWdiycTwa9HrmT9K3qi+OfrW9m3nZOjk03dp76anit6rvj/2gf2h+2P0x5Hp7E/4T5WfjT93fAn88ngmbWbm3/eE8/syOll+AAAACXBIWXMAAAsTAAALEwEAmpwYAAABmElEQVQ4EdWSv0vDQBTH7y4ZUkKhTdtYHArOUvwPdHAVpeBY3PwH/BfEycF/wclR6NzBxUFxKrgokRLaSkmhTZr+ADWJ32s5DeXaSkHBW97du/c+73vvHiF/vaIooj+pyZYFAaTbtn0DuzR2YQBX1G63K57n7TQajfNlhRfCfN8/6na7u4AS13VPOp3O/iLgXBgAa0i+/Hh7J5RSEoYh6fV6FfjX5wGlMCQwgKpQNs0Lo4kdjUYEz77FvSIDSmGA7DmOU+SKxGJkukeRDfTwWPjjVo0fxH48Hic1TbtmjBX5c2F1WA/3rSAI7obDoSVif81+vyNWAmNQHgwGB6qqbqHxOUVRklD
 kQ2ELCu+h+qJQKDzGUiZb6TPT6TTt9/uHABLeK947QFKE0RSyNg3DkM6c9AN0Xb9CwguUCNDXeKDQQyaTeZpVxc9SZVASQMk2frWFzyCTwUBDElqCmKZZxv10VmaIUmU8Bgmv+Xy+JNRxXzabraJfz3y/0mo2m2e1Wi2q1+sQG+VWgogkAKhlWaeY/pLw/T/7CTBQv9a27vsbAAAAAElFTkSuQmCC) no-repeat right 50%;
+      background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAKQWlDQ1BJQ0MgUHJvZmlsZQAASA2dlndUU9kWh8+9N73QEiIgJfQaegkg0jtIFQRRiUmAUAKGhCZ2RAVGFBEpVmRUwAFHhyJjRRQLg4Ji1wnyEFDGwVFEReXdjGsJ7601896a/cdZ39nnt9fZZ+9917oAUPyCBMJ0WAGANKFYFO7rwVwSE8vE9wIYEAEOWAHA4WZmBEf4RALU/L09mZmoSMaz9u4ugGS72yy/UCZz1v9/kSI3QyQGAApF1TY8fiYX5QKUU7PFGTL/BMr0lSkyhjEyFqEJoqwi48SvbPan5iu7yZiXJuShGlnOGbw0noy7UN6aJeGjjAShXJgl4GejfAdlvVRJmgDl9yjT0/icTAAwFJlfzOcmoWyJMkUUGe6J8gIACJTEObxyDov5OWieAHimZ+SKBIlJYqYR15hp5ejIZvrxs1P5YjErlMNN4Yh4TM/0tAyOMBeAr2+WRQElWW2ZaJHtrRzt7VnW5mj5v9nfHn5T/T3IevtV8Sbsz55BjJ5Z32zsrC+9FgD2JFqbHbO+lVUAtG0GQOXhrE/vIADyBQC03pzzHoZsXpLE4gwnC4vs7GxzAZ9rLivoN/ufgm/Kv4Y595nL7vtWO6YXP4EjSRUzZUXlpqemS0TMzAwOl89k/fcQ/+PAOWnNycMsnJ/AF/GF6FVR6JQJhIlou4U8gViQLmQKhH/V4X8YNicHGX6daxRodV8AfYU5ULhJB8hvPQBDIwMkbj96An3rWxAxCsi+vGitka9zjzJ6/uf6Hwtcim7hTEEiU+b2DI9kciWiLBmj34RswQISkAd0oAo0gS4wAixgDRyAM3AD3iAAhIBIEAOWAy5IAmlABLJBPtgACkEx2AF2g2pwANSBetAEToI2cAZcBFfADXALDIBHQAqGwUswA
 d6BaQiC8BAVokGqkBakD5lC1hAbWgh5Q0FQOBQDxUOJkBCSQPnQJqgYKoOqoUNQPfQjdBq6CF2D+qAH0CA0Bv0BfYQRmALTYQ3YALaA2bA7HAhHwsvgRHgVnAcXwNvhSrgWPg63whfhG/AALIVfwpMIQMgIA9FGWAgb8URCkFgkAREha5EipAKpRZqQDqQbuY1IkXHkAwaHoWGYGBbGGeOHWYzhYlZh1mJKMNWYY5hWTBfmNmYQM4H5gqVi1bGmWCesP3YJNhGbjS3EVmCPYFuwl7ED2GHsOxwOx8AZ4hxwfrgYXDJuNa4Etw/XjLuA68MN4SbxeLwq3hTvgg/Bc/BifCG+Cn8cfx7fjx/GvyeQCVoEa4IPIZYgJGwkVBAaCOcI/YQRwjRRgahPdCKGEHnEXGIpsY7YQbxJHCZOkxRJhiQXUiQpmbSBVElqIl0mPSa9IZPJOmRHchhZQF5PriSfIF8lD5I/UJQoJhRPShxFQtlOOUq5QHlAeUOlUg2obtRYqpi6nVpPvUR9Sn0vR5Mzl/OX48mtk6uRa5Xrl3slT5TXl3eXXy6fJ18hf0r+pvy4AlHBQMFTgaOwVqFG4bTCPYVJRZqilWKIYppiiWKD4jXFUSW8koGStxJPqUDpsNIlpSEaQtOledK4tE20Otpl2jAdRzek+9OT6cX0H+i99AllJWVb5SjlHOUa5bPKUgbCMGD4M1IZpYyTjLuMj/M05rnP48/bNq9pXv+8KZX5Km4qfJUilWaVAZWPqkxVb9UU1Z2qbapP1DBqJmphatlq+9Uuq43Pp893ns+dXzT/5PyH6rC6iXq4+mr1w+o96pMamhq+GhkaVRqXNMY1GZpumsma5ZrnNMe0aFoLtQRa5VrntV4wlZnuzFRmJbOLOaGtru2nLdE+pN2rPa1jqLNYZ6NOs84TXZIuWzdBt1y3U3dCT0svWC9fr1HvoT5Rn62fpL9Hv1t/ysDQINpgi0GbwaihiqG/YZ5ho+
 FjI6qRq9Eqo1qjO8Y4Y7ZxivE+41smsImdSZJJjclNU9jU3lRgus+0zwxr5mgmNKs1u8eisNxZWaxG1qA5wzzIfKN5m/krCz2LWIudFt0WXyztLFMt6ywfWSlZBVhttOqw+sPaxJprXWN9x4Zq42Ozzqbd5rWtqS3fdr/tfTuaXbDdFrtOu8/2DvYi+yb7MQc9h3iHvQ732HR2KLuEfdUR6+jhuM7xjOMHJ3snsdNJp9+dWc4pzg3OowsMF/AX1C0YctFx4bgccpEuZC6MX3hwodRV25XjWuv6zE3Xjed2xG3E3dg92f24+ysPSw+RR4vHlKeT5xrPC16Il69XkVevt5L3Yu9q76c+Oj6JPo0+E752vqt9L/hh/QL9dvrd89fw5/rX+08EOASsCegKpARGBFYHPgsyCRIFdQTDwQHBu4IfL9JfJFzUFgJC/EN2hTwJNQxdFfpzGC4sNKwm7Hm4VXh+eHcELWJFREPEu0iPyNLIR4uNFksWd0bJR8VF1UdNRXtFl0VLl1gsWbPkRoxajCCmPRYfGxV7JHZyqffS3UuH4+ziCuPuLjNclrPs2nK15anLz66QX8FZcSoeGx8d3xD/iRPCqeVMrvRfuXflBNeTu4f7kufGK+eN8V34ZfyRBJeEsoTRRJfEXYljSa5JFUnjAk9BteB1sl/ygeSplJCUoykzqdGpzWmEtPi000IlYYqwK10zPSe9L8M0ozBDuspp1e5VE6JA0ZFMKHNZZruYjv5M9UiMJJslg1kLs2qy3mdHZZ/KUcwR5vTkmuRuyx3J88n7fjVmNXd1Z752/ob8wTXuaw6thdauXNu5Tnddwbrh9b7rj20gbUjZ8MtGy41lG99uit7UUaBRsL5gaLPv5sZCuUJR4b0tzlsObMVsFWzt3WazrWrblyJe0fViy+KK4k8l3JLr31l9V/ndzPaE7b2l9qX7d+B2CHfc3em681iZYlle2dCu4F2t5czyovK3u1fsvlZhW3F
 gD2mPZI+0MqiyvUqvakfVp+qk6oEaj5rmvep7t+2d2sfb17/fbX/TAY0DxQc+HhQcvH/I91BrrUFtxWHc4azDz+ui6rq/Z39ff0TtSPGRz0eFR6XHwo911TvU1zeoN5Q2wo2SxrHjccdv/eD1Q3sTq+lQM6O5+AQ4ITnx4sf4H++eDDzZeYp9qukn/Z/2ttBailqh1tzWibakNml7THvf6YDTnR3OHS0/m/989Iz2mZqzymdLz5HOFZybOZ93fvJCxoXxi4kXhzpXdD66tOTSna6wrt7LgZevXvG5cqnbvfv8VZerZ645XTt9nX297Yb9jdYeu56WX+x+aem172296XCz/ZbjrY6+BX3n+l37L972un3ljv+dGwOLBvruLr57/17cPel93v3RB6kPXj/Mejj9aP1j7OOiJwpPKp6qP6391fjXZqm99Oyg12DPs4hnj4a4Qy//lfmvT8MFz6nPK0a0RupHrUfPjPmM3Xqx9MXwy4yX0+OFvyn+tveV0auffnf7vWdiycTwa9HrmT9K3qi+OfrW9m3nZOjk03dp76anit6rvj/2gf2h+2P0x5Hp7E/4T5WfjT93fAn88ngmbWbm3/eE8/syOll+AAAACXBIWXMAAAsTAAALEwEAmpwYAAABmElEQVQ4EdWSv0vDQBTH7y4ZUkKhTdtYHArOUvwPdHAVpeBY3PwH/BfEycF/wclR6NzBxUFxKrgokRLaSkmhTZr+ADWJ32s5DeXaSkHBW97du/c+73vvHiF/vaIooj+pyZYFAaTbtn0DuzR2YQBX1G63K57n7TQajfNlhRfCfN8/6na7u4AS13VPOp3O/iLgXBgAa0i+/Hh7J5RSEoYh6fV6FfjX5wGlMCQwgKpQNs0Lo4kdjUYEz77FvSIDSmGA7DmOU+SKxGJkukeRDfTwWPjjVo0fxH48Hic1TbtmjBX5c2F1WA/3rSAI7obDoSVif81+vyNWAmNQHgwGB6qqbqHxOUVRklDk
 Q2ELCu+h+qJQKDzGUiZb6TPT6TTt9/uHABLeK947QFKE0RSyNg3DkM6c9AN0Xb9CwguUCNDXeKDQQyaTeZpVxc9SZVASQMk2frWFzyCTwUBDElqCmKZZxv10VmaIUmU8Bgmv+Xy+JNRxXzabraJfz3y/0mo2m2e1Wi2q1+sQG+VWgogkAKhlWaeY/pLw/T/7CTBQv9a27vsbAAAAAElFTkSuQmCC) no-repeat right 50%;
     }
     div.view-wrapper {
       input[type="checkbox"], .btn-group {
@@ -318,6 +383,258 @@
   }
 }
 
-.help-inline{
+.help-inline {
   color: #b94a48;
+}
+
+#hive-job-details {
+  #toggle-query {
+    margin-left: 20px;
+  }
+  .query-info {
+    display: none;
+    margin-top: 10px;
+  }
+  #job-more-details-table {
+    td {
+      vertical-align: top;
+    }
+    td:first-child {
+      font-weight: bold;
+      padding-right: 7px;
+    }
+    a {
+      cursor: pointer;
+    }
+  }
+  .sections {
+    margin-top: 10px;
+  }
+  #tez-vertices-table-section {
+    display: block;
+  }
+  #tez-vertices-table-container {
+    max-height: 300px;
+    overflow-y: auto;
+    margin-bottom: 7px;
+    border-bottom: 1px solid #dddddd;
+    border-top: 1px solid #dddddd;
+    border-radius: 4px;
+    table {
+      margin-bottom: 0px;
+      .sorting_asc {
+        background: url(data:image/png;base64,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) no-repeat right 
 50%;
+      }
+      .sorting_desc {
+        background: url(data:image/png;base64,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) no-repeat right 50%;
+      }
+      .sorting {
+        background: url(data:image/png;base64,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) no-repeat right 50%;
+      }
+    }
+  }
+  #tez-vertex-details-section-body {
+    td {
+      border-top: none;
+    }
+    tr td:first-child {
+      font-weight: bold;
+      width: 20%;
+      overflow: auto;
+    }
+    textarea {
+      width: 95%;
+      margin-left: 10px;
+    }
+  }
+}
+
+/***************
+ * Tez DAG section
+ ***************/
+#tez-dag-section {
+  padding-left: 7px;
+  padding-top: 5px;
+  overflow: hidden;
+  #tez-dag-section-top-bar-actions {
+    margin-right: 7px;
+  }
+}
+
+#tez-dag-section-body {
+  display: block;
+}
+
+#tez-dag-section-body-dag {
+  overflow-y: hidden;
+
+  .heat-0-20 {
+    fill: #ffffff;
+  }
+  .heat-20-40 {
+    fill: #aaaaaa;
+  }
+  .heat-40-60 {
+    fill: #ffff00;
+  }
+  .heat-60-80 {
+    fill: #FFC000;
+  }
+  .heat-80-100 {
+    fill: #ff0000;
+  }
+  .heat-none {
+    fill: #999999;
+  }
+  #tez-dag-svg {
+    .link line {
+      stroke: #696969;
+    }
+    .link line.separator {
+      stroke: #fff;
+      stroke-width: 2px;
+    }
+    text {
+      font-weight: lighter;
+    }
+    .tez-root-rect {
+      fill: white;
+      stroke: white;
+      opacity: 0;
+      stroke-opacity: 0;
+    }
+    .debug {
+      fill: green;
+      opacity: 0.2;
+    }
+    .node {
+      .background.map {
+        fill: #fde0dd;
+        stroke: #bbbbbb;
+      }
+      .background.reduce {
+        fill: #DBD4EB;
+        stroke: #bbbbbb;
+      }
+      .background.union {
+        fill: #BCE6DD;
+        stroke: #bbbbbb;
+      }
+      .background.unknown-vertex-type {
+        fill: #EBE8CC;
+        stroke: #bbbbbb;
+      }
+      .background.selected {
+        stroke: #3a87ad;
+      }
+      .background {
+        stroke-width: 6px;
+        fill: #c0c0c0;
+        stroke: #999999;
+        cursor: pointer;
+      }
+      .metric {
+        rect {
+          opacity: 0.6;
+        }
+        text {
+          font-size: 90%;
+        }
+      }
+      .operation {
+        fill: #3F66E8;
+        opacity: 0.5;
+        cursor: pointer;
+        text {
+          font-size: 80%;
+        }
+      }
+      .vertex-icon-text {
+        stroke-width: 0;
+        opacity: 0.9;
+      }
+      .vertex-icon-rect {
+        opacity: 0;
+      }
+      .vertex-icon-text.running,
+      .vertex-icon-text.initializing,
+      .vertex-icon-text.succeeded,
+      .vertex-icon-text.inited {
+        fill: green;
+        stroke: green;
+      }
+      .vertex-icon-text.new {
+        fill: #fde0dd;
+        stroke: cornflowerblue;
+        stroke-width: 1;
+      }
+      .vertex-icon-text.failed,
+      .vertex-icon-text.killed,
+      .vertex-icon-text.error,
+      .vertex-icon-text.terminating {
+        fill: red;
+        stroke: red;
+      }
+    }
+    .node text {
+      pointer-events: none;
+    }
+    path.link {
+      fill: none;
+      stroke: #666;
+      stroke-width: 5px;
+      opacity: 0.6;
+    }
+    path.link.type-broadcast {
+      stroke: teal;
+    }
+    path.link.type-scatter_gather {
+      stroke: orange;
+      stroke-dasharray: 10, 3;
+    }
+    path.link.type-contains {
+      stroke: green;
+      stroke-dasharray: 10, 2, 5, 2;
+    }
+    path.link.type-unknown {
+      stroke: gray;
+    }
+  }
+}
+
+#hover-op-table {
+  margin-bottom: 2px;
+  td {
+    padding: 2px;
+    word-wrap: break-word;
+  }
+}
+
+#hive-job-details {
+  #operator-table {
+    td {
+      border-top: 1px solid #dddddd;
+    }
+    tr td:first-child {
+      width: 20%;
+      overflow: auto;
+    }
+    td:first-child + td,
+    th:first-child + th {
+      width: 80%;
+    }
+  }
+}
+
+.alert-info {
+  background-color: #E6F1F6;
+  border-color: #D2D9DD;
+  color: #4E575B;
+  text-shadow: none;
+  .link {
+    padding: 0 15px;
+  }
+  .link-left-pad {
+    padding-left:15px;
+  }
 }
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ambari/blob/85362043/contrib/views/jobs/src/main/resources/ui/app/templates/job/error_popup.hbs
----------------------------------------------------------------------
diff --git a/contrib/views/jobs/src/main/resources/ui/app/templates/job/error_popup.hbs b/contrib/views/jobs/src/main/resources/ui/app/templates/job/error_popup.hbs
new file mode 100644
index 0000000..8947b6f
--- /dev/null
+++ b/contrib/views/jobs/src/main/resources/ui/app/templates/job/error_popup.hbs
@@ -0,0 +1,19 @@
+{{!
+* 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.
+}}
+
+{{controller.error_message}}

http://git-wip-us.apache.org/repos/asf/ambari/blob/85362043/contrib/views/jobs/src/main/resources/ui/app/templates/job/hive_job_details_tez_dag.hbs
----------------------------------------------------------------------
diff --git a/contrib/views/jobs/src/main/resources/ui/app/templates/job/hive_job_details_tez_dag.hbs b/contrib/views/jobs/src/main/resources/ui/app/templates/job/hive_job_details_tez_dag.hbs
new file mode 100644
index 0000000..4d8d9e7
--- /dev/null
+++ b/contrib/views/jobs/src/main/resources/ui/app/templates/job/hive_job_details_tez_dag.hbs
@@ -0,0 +1,44 @@
+{{!
+* 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.
+}}
+
+<div id="tez-dag-section-body-dag">
+  <svg id="tez-dag-svg" stroke="black" strokeWidth="2">
+    <defs>
+      <marker id="arrow" viewBox="0 -5 10 10" refX="4" markerWidth="3" markerHeight="3" orient="auto">
+        <path d="M0,-5L10,0L0,5"></path>
+      </marker>
+      <filter id="shadow" width="200%" height="200%">
+        <feGaussianBlur in="SourceAlpha" stdDeviation="3"/>
+        <feOffset dx="2" dy="2" result="offsetblur"/>
+        <feMerge> 
+          <feMergeNode/>
+          <feMergeNode in="SourceGraphic"/>
+        </feMerge>
+      </filter>
+      <clipPath id="operatorClipPath">
+        <rect x="0" y="0" width="50" height="16">
+        </rect>
+      </clipPath>
+      <font-face xmlns="http://www.w3.org/2000/svg" font-family="FontAwesome" unicode-range="U+0-7F">
+        <font-face-src>
+          <font-face-uri xlink:href="font/fontawesome-webfont.svg" xmlns:xlink="http://www.w3.org/1999/xlink"/>
+        </font-face-src>
+      </font-face>
+    </defs>
+  </svg>
+</div>

http://git-wip-us.apache.org/repos/asf/ambari/blob/85362043/contrib/views/jobs/src/main/resources/ui/app/templates/job/hover_op_table.hbs
----------------------------------------------------------------------
diff --git a/contrib/views/jobs/src/main/resources/ui/app/templates/job/hover_op_table.hbs b/contrib/views/jobs/src/main/resources/ui/app/templates/job/hover_op_table.hbs
new file mode 100644
index 0000000..a2cf77d
--- /dev/null
+++ b/contrib/views/jobs/src/main/resources/ui/app/templates/job/hover_op_table.hbs
@@ -0,0 +1,39 @@
+{{!
+* 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.
+}}
+
+<p>{{content.operationName}}</p>
+<table id="hover-op-table" class="table table-bordered">
+  <thead>
+  <tr>
+    <th>{{t common.name}}</th>
+    <th>{{t common.value}}</th>
+  </tr>
+  </thead>
+  <tbody>
+    {{#each keys in content.operatorPlanObj}}
+    <tr>
+      <td>
+        {{keys.name}}
+      </td>
+      <td>
+        {{keys.value}}
+      </td>
+    </tr>
+    {{/each}}
+  </tbody>
+</table>

http://git-wip-us.apache.org/repos/asf/ambari/blob/85362043/contrib/views/jobs/src/main/resources/ui/app/templates/job/job.hbs
----------------------------------------------------------------------
diff --git a/contrib/views/jobs/src/main/resources/ui/app/templates/job/job.hbs b/contrib/views/jobs/src/main/resources/ui/app/templates/job/job.hbs
new file mode 100644
index 0000000..9e9b6ef
--- /dev/null
+++ b/contrib/views/jobs/src/main/resources/ui/app/templates/job/job.hbs
@@ -0,0 +1,228 @@
+{{!
+* 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.
+}}
+
+<div id="hive-job-details">
+
+  {{#if controller.loaded}}
+    <!-- Top Bar -->
+    <div class="top-bar">
+      <a {{action "actionRouteToJobs" target="controller"}} href="#">{{t menu.item.jobs}}</a> > {{view.content.name}}
+      <a {{action "toggleShowQuery" target="view"}} href="#" id="toggle-query">{{view.toggleShowQueryText}}</a>
+
+      <div class="pull-right">Job Type: <span class="label label-info">{{view.content.jobType}}</span></div>
+      <div class="alert alert-info query-info">
+        <table id="job-more-details-table">
+          <tr>
+            <td>
+              {{t jobs.hive.query}}
+            </td>
+            <td>
+              {{view.content.queryText}}
+            </td>
+          </tr>
+          <tr>
+            <td>
+              {{t jobs.hive.yarnApplication}}
+            </td>
+            <td>
+              {{view.content.tezDag.yarnApplicationId}}
+            </td>
+          </tr>
+          <tr>
+            <td>
+              {{t jobs.hive.stages}}
+            </td>
+            <td>
+              <ol>
+                {{#each stage in view.content.stages}}
+                  <li>{{stage.id}}{{stage.description}}.</li>
+                {{/each}}
+              </ol>
+            </td>
+          </tr>
+        </table>
+      </div>
+    </div>
+
+    <!-- Sections -->
+    <div class="row-fluid">
+      <div class="span12 sections">
+        <!-- Section LHS -->
+        <div id="tez-dag-lhs" {{bind-attr class="view.isGraphMaximized:span12:span6 :sections-lhs"}}>
+          <div id="tez-dag-section" class="box">
+            <div id="tez-dag-section-top-bar"> &nbsp;
+              {{t jobs.hive.tez.dag.summary.metric}}
+              <div class="btn-group display-inline-block">
+                <button class="btn dropdown-toggle"  data-toggle="dropdown">
+                  {{view.summaryMetricTypeDisplay}}
+                    <span class="caret"></span>
+                </button>
+                <ul class="dropdown-menu">
+                  <!-- dropdown menu links -->
+                  {{#each type in view.summaryMetricTypesDisplay}}
+                    <li>
+                      <a {{bind-attr title="type"}}
+                         href="#" {{action "doSelectSummaryMetricType" type target="view"}}>{{type}}</a>
+                    </li>
+                  {{/each}}
+                </ul>
+              </div>
+              <div id="tez-dag-section-top-bar-actions" class="pull-right">
+                <div class="btn-group">
+                  <a id="tez-dag-zoom-in-button" {{bind-attr class="view.canGraphZoomIn::disabled :btn"}} {{action "doGraphZoomIn" target="view"}}>
+                    <i class="icon-zoom-in"></i>
+                  </a>
+                  <a id="tez-dag-zoom-out-button" {{bind-attr class="view.canGraphZoomOut::disabled :btn"}} {{action "doGraphZoomOut" target="view"}}>
+                    <i class="icon-zoom-out"></i>
+                  </a>
+                  {{#if view.isGraphMaximized}}
+                    <a id="tez-dag-resize-small-button" class="btn" {{action "doGraphMinimize" target="view"}}>
+                      <i class="icon-resize-small"></i>
+                    </a>
+                  {{else}}
+                    <a id="tez-dag-resize-full-button" class="btn" {{action "doGraphMaximize" target="view"}}>
+                      <i class="icon-resize-full"></i>
+                    </a>
+                  {{/if}}
+                </div>
+              </div>
+            </div>
+            <div id="tez-dag-section-body">
+                {{#if controller.loaded}}
+                  {{view App.MainHiveJobDetailsTezDagView
+                    contentBinding="view.content"
+                    tezDagBinding="view.content.tezDag"
+                    selectedVertexBinding="view.selectedVertex"
+                    summaryMetricTypeBinding="view.summaryMetricType"
+                    zoomScaleBinding="view.zoomScale"
+                    zoomScaleFromBinding="view.zoomScaleFrom"
+                    zoomScaleToBinding="view.zoomScaleTo"
+                  }}
+                {{/if}}
+            </div>
+          </div>
+        </div>
+
+        <!-- Section RHS -->
+        <div id="tez-vertices-rhs" {{bind-attr class="view.isGraphMaximized:hidden:span6 :sections-rhs"}}>
+
+          <!-- Section RHS Vertices -->
+          <div id="tez-vertices-table-section">
+            <div id="tez-vertices-table-container" class="section">
+              <table class="table table-hover table-bordered table-striped">
+                {{#view App.MainHiveJobDetailsVerticesTableView
+                  contentBinding="view.sortedVertices"
+                  doSelectVertexBinding="view.doSelectVertex"
+                  actionDoSelectVertexBinding="view.actionDoSelectVertex"
+                  selectedVertexBinding="view.selectedVertex"
+                }}
+                  <thead>
+                    {{#view view.sortView contentBinding="view.content"}}
+                      {{view view.parentView.nameSort}}
+                      {{view view.parentView.tasksSort}}
+                      {{view view.parentView.inputSort}}
+                      {{view view.parentView.outputSort}}
+                      {{view view.parentView.durationSort}}
+                    {{/view}}
+                  </thead>
+                  <tbody>
+                    {{#each vertex in view.content}}
+                    <tr {{bind-attr class="vertex.isSelected:info"}}>
+                      <td>
+                        <a {{bind-attr title="vertex.name"}} {{action "actionDoSelectVertex" vertex target="view.parentView"}}>{{vertex.name}}</a>
+                      </td>
+                      <td>{{vertex.tasksNumber}}</td>
+                      <td>{{vertex.totalReadBytesDisplay}}</td>
+                      <td>{{vertex.totalWriteBytesDisplay}}</td>
+                      <td>{{vertex.durationDisplay}}</td>
+                    </tr>
+                    {{/each}}
+                  </tbody>
+                {{/view}}
+              </table>
+            </div>
+          </div>
+
+          <!-- Section RHS Vertex -->
+          {{#if view.selectedVertex}}
+            <div id="section tez-vertex-details-section">
+              <div class="box">
+                <div class="box-header">
+                  <h4>{{view.selectedVertex.name}}</h4>
+                </div>
+                <div id="tez-vertex-details-section-body">
+                  <table class="table no-borders table-condensed">
+                    <tr>
+                      <td>{{t common.status}}</td>
+                      <td>{{view.selectedVertexIODisplay.status}}</td>
+                      <td></td>
+                    </tr>
+                    <tr>
+                      <td>{{t common.time.start}}</td>
+                      <td>{{view.selectedVertexIODisplay.started}}</td>
+                      <td></td>
+                    </tr>
+                    <tr>
+                      <td>{{t common.time.end}}</td>
+                      <td>{{view.selectedVertexIODisplay.ended}}</td>
+                      <td></td>
+                    </tr>
+                    <tr>
+                      <td>{{t jobs.hive.tez.tasks}}</td>
+                      <td>{{view.selectedVertex.tasksCount}}</td>
+                      <td></td>
+                    </tr>
+                    <tr>
+                      <td>{{t jobs.hive.tez.hdfs}}</td>
+                      <td>{{view.selectedVertexIODisplay.hdfs.read.ops}}
+                        / {{view.selectedVertexIODisplay.hdfs.read.bytes}}</td>
+                      <td>{{view.selectedVertexIODisplay.hdfs.write.ops}}
+                        / {{view.selectedVertexIODisplay.hdfs.write.bytes}}</td>
+                    </tr>
+                    <tr>
+                      <td>{{t jobs.hive.tez.localFiles}}</td>
+                      <td>{{view.selectedVertexIODisplay.file.read.ops}}
+                        / {{view.selectedVertexIODisplay.file.read.bytes}}</td>
+                      <td>{{view.selectedVertexIODisplay.file.write.ops}}
+                        / {{view.selectedVertexIODisplay.file.write.bytes}}</td>
+                    </tr>
+                    <tr>
+                      <td>{{t jobs.hive.tez.spilledRecords}}</td>
+                      <td>{{view.selectedVertex.spilledRecords}}</td>
+                    </tr>
+                    {{#if view.selectedVertexIODisplay.records.read}}
+                      <tr>
+                        <td>{{t jobs.hive.tez.records}}</td>
+                        <td>{{view.selectedVertexIODisplay.records.read}}</td>
+                        <td>{{view.selectedVertexIODisplay.records.write}}</td>
+                      </tr>
+                    {{/if}}
+                  </table>
+                </div>
+              </div>
+            </div>
+          {{/if}}
+        </div>
+      </div>
+    </div>
+  {{else}}
+    <div class="alert alert-info">
+      <h4>{{t app.loadingPlaceholder}}</h4>
+    </div>
+  {{/if}}
+</div>

http://git-wip-us.apache.org/repos/asf/ambari/blob/85362043/contrib/views/jobs/src/main/resources/ui/bower.json
----------------------------------------------------------------------
diff --git a/contrib/views/jobs/src/main/resources/ui/bower.json b/contrib/views/jobs/src/main/resources/ui/bower.json
index 6eda9d2..78377f9 100644
--- a/contrib/views/jobs/src/main/resources/ui/bower.json
+++ b/contrib/views/jobs/src/main/resources/ui/bower.json
@@ -10,7 +10,9 @@
     "bootstrap": "2.3.*",
     "ember-addons.bs_for_ember": ">=0.7",
     "ember-json-mapper": "master",
-    "jquery-ui": ">=1.11"
+    "jquery-ui": ">=1.11",
+    "d3": "2.10.2",
+    "font-awesome": "3.2.1"
   },
   "devDependencies": {
     "ember-mocha-adapter": "0.1.2"


[2/2] git commit: AMBARI-6651. Create job-details page. (onechiporenko)

Posted by on...@apache.org.
AMBARI-6651. Create job-details page. (onechiporenko)


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

Branch: refs/heads/trunk
Commit: 85362043f7365fe59c2900284ef12057f00cf0b1
Parents: 569acf8
Author: Oleg Nechiporenko <on...@apache.org>
Authored: Tue Jul 29 14:58:06 2014 +0300
Committer: Oleg Nechiporenko <on...@apache.org>
Committed: Tue Jul 29 14:58:06 2014 +0300

----------------------------------------------------------------------
 .../jobs/src/main/resources/ui/Gruntfile.js     |   6 +
 .../jobs/src/main/resources/ui/app/index.html   |   4 +
 .../src/main/resources/ui/app/scripts/app.js    |  14 +-
 .../app/scripts/controllers/job_controller.js   |  87 +-
 .../resources/ui/app/scripts/helpers/ajax.js    |  24 +
 .../resources/ui/app/scripts/helpers/jobs.js    | 255 +++++
 .../resources/ui/app/scripts/helpers/number.js  |  36 +-
 .../resources/ui/app/scripts/helpers/string.js  |  59 ++
 .../app/scripts/mappers/jobs/hive_job_mapper.js |  58 +-
 .../scripts/mappers/jobs/hive_jobs_mapper.js    |   4 +
 .../ui/app/scripts/models/jobs/hive_job.js      |   6 +-
 .../ui/app/scripts/models/jobs/tez_dag.js       | 105 ++-
 .../src/main/resources/ui/app/scripts/router.js |   2 +-
 .../ui/app/scripts/routes/application_route.js  |  13 +-
 .../resources/ui/app/scripts/translations.js    |  37 +-
 .../views/job/hive_job_details_tez_dag_view.js  | 925 +++++++++++++++++++
 .../scripts/views/job/hive_job_details_view.js  | 327 +++++++
 .../resources/ui/app/scripts/views/job_view.js  |  19 -
 .../resources/ui/app/scripts/views/jobs_view.js |   6 +-
 .../src/main/resources/ui/app/styles/main.less  | 353 ++++++-
 .../ui/app/templates/job/error_popup.hbs        |  19 +
 .../templates/job/hive_job_details_tez_dag.hbs  |  44 +
 .../ui/app/templates/job/hover_op_table.hbs     |  39 +
 .../main/resources/ui/app/templates/job/job.hbs | 228 +++++
 .../views/jobs/src/main/resources/ui/bower.json |   4 +-
 25 files changed, 2537 insertions(+), 137 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/85362043/contrib/views/jobs/src/main/resources/ui/Gruntfile.js
----------------------------------------------------------------------
diff --git a/contrib/views/jobs/src/main/resources/ui/Gruntfile.js b/contrib/views/jobs/src/main/resources/ui/Gruntfile.js
index 444acb1..d223202 100644
--- a/contrib/views/jobs/src/main/resources/ui/Gruntfile.js
+++ b/contrib/views/jobs/src/main/resources/ui/Gruntfile.js
@@ -267,6 +267,12 @@ module.exports = function (grunt) {
             flatten: true,
             src: '<%= yeoman.app %>/bower_components/jquery-ui/themes/base/images/*',
             dest: '<%= yeoman.dist %>/styles/images/'
+          },
+          {
+            expand: true,
+            flatten: true,
+            src: '<%= yeoman.app %>/bower_components/font-awesome/font/*',
+            dest: '<%= yeoman.dist %>/font/'
           }
         ]
       }

http://git-wip-us.apache.org/repos/asf/ambari/blob/85362043/contrib/views/jobs/src/main/resources/ui/app/index.html
----------------------------------------------------------------------
diff --git a/contrib/views/jobs/src/main/resources/ui/app/index.html b/contrib/views/jobs/src/main/resources/ui/app/index.html
index f5ae0de..feb8bcb 100644
--- a/contrib/views/jobs/src/main/resources/ui/app/index.html
+++ b/contrib/views/jobs/src/main/resources/ui/app/index.html
@@ -27,7 +27,10 @@
   </head>
   <body>
     <!-- build:js(app) scripts/components.js -->
+    <script src="bower_components/cldr/plurals.js"></script>
     <script src="bower_components/jquery/jquery.js"></script>
+    <script src="bower_components/bootstrap/js/bootstrap-dropdown.js"></script>
+    <script src="bower_components/bootstrap/js/bootstrap-button.js"></script>
     <script src="bower_components/bootstrap/js/bootstrap-tooltip.js"></script>
     <script src="bower_components/jquery-ui/ui/datepicker.js"></script>
     <script src="bower_components/moment/moment.js"></script>
@@ -39,6 +42,7 @@
     <script src="bower_components/ember-addons.bs_for_ember/dist/js/bs-core.min.js"></script>
     <script src="bower_components/ember-addons.bs_for_ember/dist/js/bs-button.min.js"></script>
     <script src="bower_components/ember-addons.bs_for_ember/dist/js/bs-modal.min.js"></script>
+    <script src="bower_components/d3/d3.v2.js"></script>
     <!-- endbuild -->
 
     <!-- build:js(.tmp) scripts/templates.js -->

http://git-wip-us.apache.org/repos/asf/ambari/blob/85362043/contrib/views/jobs/src/main/resources/ui/app/scripts/app.js
----------------------------------------------------------------------
diff --git a/contrib/views/jobs/src/main/resources/ui/app/scripts/app.js b/contrib/views/jobs/src/main/resources/ui/app/scripts/app.js
index b7c6d2b..1067704 100644
--- a/contrib/views/jobs/src/main/resources/ui/app/scripts/app.js
+++ b/contrib/views/jobs/src/main/resources/ui/app/scripts/app.js
@@ -16,7 +16,14 @@
  * limitations under the License.
  */
 
-var App = window.App = Ember.Application.create();
+var App = window.App = Ember.Application.createWithMixins(Bootstrap, {
+  LOG_TRANSITIONS: false,
+  LOG_TRANSITIONS_INTERNAL: false
+});
+
+require('scripts/router');
+require('scripts/routes/*');
+require('scripts/store');
 
 App.Helpers = Ember.Namespace.create();
 
@@ -56,15 +63,12 @@ App.initializer({
 
 /* Order and include as you please. */
 require('scripts/translations');
-require('scripts/router');
-require('scripts/store');
 require('scripts/mixins/*');
 require('scripts/helpers/*');
 require('scripts/models/**/*');
 require('scripts/mappers/server_data_mapper.js');
 require('scripts/mappers/**/*');
-require('scripts/controllers/*');
-require('scripts/routes/*');
+require('scripts/controllers/**/*');
 require('scripts/components/*');
 require('scripts/views/sort_view');
 require('scripts/views/filter_view');

http://git-wip-us.apache.org/repos/asf/ambari/blob/85362043/contrib/views/jobs/src/main/resources/ui/app/scripts/controllers/job_controller.js
----------------------------------------------------------------------
diff --git a/contrib/views/jobs/src/main/resources/ui/app/scripts/controllers/job_controller.js b/contrib/views/jobs/src/main/resources/ui/app/scripts/controllers/job_controller.js
index e300323..bce4526 100644
--- a/contrib/views/jobs/src/main/resources/ui/app/scripts/controllers/job_controller.js
+++ b/contrib/views/jobs/src/main/resources/ui/app/scripts/controllers/job_controller.js
@@ -16,4 +16,89 @@
  * limitations under the License.
  */
 
-App.JobController = Ember.Controller.extend({});
+App.JobController = Ember.ObjectController.extend({
+
+  name: 'jobController',
+
+  loaded: false,
+
+  loadTimeout: null,
+
+  job: null,
+
+  sortingColumn: null,
+
+  showPopupButtons: [
+    Ember.Object.create({title: Em.I18n.t('ok'), dismiss: 'modal'})
+  ],
+
+  showPopup: function (title) {
+    Bootstrap.ModalManager.open(
+      'errorPopup',
+      title,
+      'job/error_popup',
+      this.get('showPopupButtons'),
+      this
+    );
+  },
+
+  loadJobDetails: function () {
+    var self = this,
+      timeout = this.get('loadTimeout'),
+      yarnService = App.HiveJob.store.getById('service', 'YARN'),
+      content = this.get('content');
+    if (!Em.isNone(yarnService)) {
+      if (!Em.isNone(content)) {
+        App.Helpers.jobs.refreshJobDetails(
+          content,
+          function () {
+            self.set('content', App.HiveJob.store.getById('hiveJob', content.get('id')));
+            self.set('loaded', true);
+          },
+          function (errorId) {
+            switch (errorId) {
+              case 'job.dag.noId':
+                self.set('error_message', Em.I18n.t('jobs.hive.tez.dag.error.noDagId.message'));
+                self.showPopup(Em.I18n.t('jobs.hive.tez.dag.error.noDagId.title'));
+                break;
+              case 'job.dag.noname':
+                self.set('error_message', Em.I18n.t('jobs.hive.tez.dag.error.noDag.message'));
+                self.showPopup(Em.I18n.t('jobs.hive.tez.dag.error.noDag.title'));
+                break;
+              case 'job.dag.id.noDag':
+                self.set('error_message', Em.I18n.t('jobs.hive.tez.dag.error.noDagForId.message'));
+                self.showPopup(Em.I18n.t('jobs.hive.tez.dag.error.noDagForId.title'));
+                break;
+              case 'job.dag.id.loaderror':
+              case 'job.dag.name.loaderror':
+                break;
+              default:
+                break;
+            }
+            self.routeToJobs();
+          }
+        );
+      }
+    } else {
+      clearTimeout(timeout);
+      timeout = setTimeout(function () {
+        self.loadJobDetails();
+      }, 300);
+    }
+  },
+
+  /**
+   * open jobs page
+   * @method routeToJobs
+   */
+  routeToJobs: function () {
+    this.transitionToRoute('jobs');
+  },
+
+  actions: {
+    actionRouteToJobs: function () {
+      this.routeToJobs();
+    }
+  }
+
+});
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ambari/blob/85362043/contrib/views/jobs/src/main/resources/ui/app/scripts/helpers/ajax.js
----------------------------------------------------------------------
diff --git a/contrib/views/jobs/src/main/resources/ui/app/scripts/helpers/ajax.js b/contrib/views/jobs/src/main/resources/ui/app/scripts/helpers/ajax.js
index 46eda9a..73a87fd 100644
--- a/contrib/views/jobs/src/main/resources/ui/app/scripts/helpers/ajax.js
+++ b/contrib/views/jobs/src/main/resources/ui/app/scripts/helpers/ajax.js
@@ -42,6 +42,30 @@ var urls = {
     apiPrefix: ''
   },
 
+  'job_details': {
+    real: '/proxy?url=http://{historyServerHostName}:{ahsWebPort}/ws/v1/timeline/HIVE_QUERY_ID/{job_id}?fields=events,otherinfo',
+    mock: '/scripts/assets/hive-query-2.json',
+    apiPrefix: ''
+  },
+
+  'jobs.tezDag.NametoID': {
+    'real': '/proxy?url=http://{historyServerHostName}:{ahsWebPort}/ws/v1/timeline/TEZ_DAG_ID?primaryFilter=dagName:{tezDagName}',
+    'mock': '/scripts/assets/tezDag-name-to-id.json',
+    'apiPrefix': ''
+  },
+
+  'jobs.tezDag.tezDagId': {
+    'real': '/proxy?url=http://{historyServerHostName}:{ahsWebPort}/ws/v1/timeline/TEZ_DAG_ID/{tezDagId}?fields=relatedentities,otherinfo',
+    'mock': '/scripts/assets/tezDag.json',
+    'apiPrefix': ''
+  },
+
+  'jobs.tezDag.tezDagVertexId': {
+    'real': '/proxy?url=http://{historyServerHostName}:{ahsWebPort}/ws/v1/timeline/TEZ_VERTEX_ID/{tezDagVertexId}?fields=otherinfo',
+    'mock': '/scripts/assets/tezDagVertex.json',
+    'apiPrefix': ''
+  },
+
   'cluster_name': {
     real: 'clusters',
     mock: '/scripts/assets/clusters.json'

http://git-wip-us.apache.org/repos/asf/ambari/blob/85362043/contrib/views/jobs/src/main/resources/ui/app/scripts/helpers/jobs.js
----------------------------------------------------------------------
diff --git a/contrib/views/jobs/src/main/resources/ui/app/scripts/helpers/jobs.js b/contrib/views/jobs/src/main/resources/ui/app/scripts/helpers/jobs.js
new file mode 100644
index 0000000..e67f534
--- /dev/null
+++ b/contrib/views/jobs/src/main/resources/ui/app/scripts/helpers/jobs.js
@@ -0,0 +1,255 @@
+/**
+ * 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.jobs = {
+
+  /**
+   * Refreshes the latest information for a given job
+   *
+   * @param {App.AbstractJob} job
+   * @param {Function} successCallback
+   * @param {Function} errorCallback (errorId) Called in error cases where there is no
+   *          data from server. 'errorId' can be one of
+   *          <ul>
+   *            <li>job.dag.noId</li>
+   *            <li>job.dag.noname</li>
+   *            <li>job.dag.id.noDag</li>
+   *            <li>job.dag.id.loaderror</li>
+   *            <li>job.dag.name.loaderror</li>
+   *          </ul>
+   */
+  refreshJobDetails: function (job, successCallback, errorCallback) {
+    this.refreshHiveJobDetails(job, successCallback, errorCallback);
+  },
+
+  /**
+   * Refreshes latest information of a Hive Job.
+   *
+   * @param {App.HiveJob} hiveJob
+   * @param {Function} successCallback
+   * @param {Function} errorCallback @see #refreshJobDetails()
+   * @method refreshHiveJobDetails
+   */
+  refreshHiveJobDetails: function (hiveJob, successCallback, errorCallback) {
+    var yarnService = App.HiveJob.store.getById('service', 'YARN'),
+      historyServerHostName = App.HiveJob.store.getById('component', 'APP_TIMELINE_SERVER').get('hostName'),
+      ahsWebPort = yarnService.get('ahsWebPort');
+
+    return App.ajax.send({
+      name: 'job_details',
+      sender: this,
+      data: {
+        historyServerHostName: historyServerHostName,
+        ahsWebPort: ahsWebPort,
+        job_id: hiveJob.get('id'),
+        successCallback: successCallback,
+        errorCallback: errorCallback
+      },
+      success: 'refreshHiveJobDetailsSuccessCallback'
+    });
+
+  },
+
+  refreshHiveJobDetailsSuccessCallback: function (data, opt, params) {
+    App.hiveJobMapper.map(data);
+    var hiveRecord = App.HiveJob.store.getById('hiveJob', params.job_id),
+      tezDagName = hiveRecord.get('tezDag.name'),
+      self = this;
+    if (!Em.isNone(tezDagName)) {
+      var sender = {
+        dagNameToIdSuccess: function (data) {
+          if (data && data.entities && data.entities.length > 0) {
+            var dagId = data.entities[0].entity;
+            hiveRecord.get('tezDag').set('instanceId', dagId);
+            self.refreshTezDagDetails(tezDagName, params.successCallback, params.errorCallback);
+          }
+          else {
+            params.errorCallback('job.dag.noId');
+          }
+        },
+        dagNameToIdError: function (jqXHR, url, method, showStatus) {
+          params.errorCallback('job.dag.name.loaderror');
+        }
+      };
+      App.ajax.send({
+        name: 'jobs.tezDag.NametoID',
+        sender: sender,
+        data: {
+          historyServerHostName: params.historyServerHostName,
+          tezDagName: tezDagName,
+          ahsWebPort: params.ahsWebPort
+        },
+        success: 'dagNameToIdSuccess',
+        error: 'dagNameToIdError'
+      });
+    }
+    else {
+      params.errorCallback('job.dag.noname');
+    }
+  },
+
+  /**
+   * Refreshes runtime information of a Tez DAG based on events generated.
+   * The instance ID of the Tez DAG should be set.
+   *
+   * @param {string} tezDagId ID of the Tez DAG. Example: 'HIVE-Q2:1'
+   * @param {Function} successCallback
+   * @param {Function} errorCallback @see #refreshJobDetails()
+   * @method refreshTezDagDetails
+   */
+  refreshTezDagDetails: function (tezDagId, successCallback, errorCallback) {
+    var self = this,
+      yarnService = App.HiveJob.store.getById('service', 'YARN'),
+      historyServerHostName = App.HiveJob.store.getById('component', 'APP_TIMELINE_SERVER').get('hostName'),
+      ahsWebPort = yarnService.get('ahsWebPort'),
+      tezDag = App.HiveJob.store.getById('tezDag', tezDagId);
+    if (tezDag) {
+      var tezDagInstanceId = tezDag.get('instanceId'),
+        sender = {
+          loadTezDagSuccess: function (data) {
+            if (data) {
+              if (data.otherinfo && data.otherinfo.applicationId) {
+                tezDag.set('yarnApplicationId', data.otherinfo.applicationId);
+              }
+              if (data.relatedentities && data.relatedentities.TEZ_VERTEX_ID != null) {
+                var count = data.relatedentities.TEZ_VERTEX_ID.length;
+                data.relatedentities.TEZ_VERTEX_ID.forEach(function (v) {
+                  self.refreshTezDagVertex(tezDagId, v, function () {
+                    if (--count <= 0) {
+                      // all vertices succeeded
+                      successCallback();
+                    }
+                  });
+                });
+              }
+            }
+          },
+          loadTezDagError: function (jqXHR, url, method, showStatus) {
+            errorCallback('job.dag.id.loaderror');
+          }
+        };
+      App.ajax.send({
+        name: 'jobs.tezDag.tezDagId',
+        sender: sender,
+        data: {
+          historyServerHostName: historyServerHostName,
+          tezDagId: tezDagInstanceId,
+          ahsWebPort: ahsWebPort
+        },
+        success: 'loadTezDagSuccess',
+        error: 'loadTezDagError'
+      });
+    }
+    else {
+      errorCallback('job.dag.id.noDag');
+    }
+  },
+
+  /**
+   * Refreshes runtime information of the given vertex.
+   *
+   * @param {string} tezDagId ID of the Tez DAG. Exmaple: 'HIVE-Q2:1'
+   * @param {string} tezVertexInstanceId Instance ID of the vertex to refresh. Example 'vertex_1390516007863_0001_1_00'
+   * @param {Function} successCallback
+   * @method refreshTezDagVertex
+   */
+  refreshTezDagVertex: function (tezDagId, tezVertexInstanceId, successCallback) {
+    var yarnService = App.HiveJob.store.getById('service', 'YARN'),
+      historyServerHostName = App.HiveJob.store.getById('component', 'APP_TIMELINE_SERVER').get('hostName'),
+      ahsWebPort = yarnService.get('ahsWebPort'),
+      tezDag = App.HiveJob.store.getById('tezDag', tezDagId),
+      hiveJob = App.HiveJob.store.all('hiveJob').findBy('tezDag', tezDag),
+      hiveJobFailed = hiveJob.get('failed'),
+      hiveJobEndTime = hiveJob.get('endTime'),
+      sender = {
+        loadTezDagVertexSuccess: function (data) {
+          if (data && data.otherinfo) {
+            var vertexRecord = App.HiveJob.store.getById('tezDagVertex', tezDagId + "/" + data.otherinfo.vertexName);
+            if (vertexRecord != null) {
+              vertexRecord.set('startTime', data.otherinfo.startTime);
+              if (data.otherinfo.endTime == undefined && hiveJobFailed) {
+                vertexRecord.set('endTime', hiveJobEndTime);
+              }
+              else {
+                vertexRecord.set('endTime', data.otherinfo.endTime);
+              }
+              vertexRecord.set('tasksCount', data.otherinfo.numTasks);
+              if (data.otherinfo.status == null && hiveJobFailed) {
+                vertexRecord.set('state', Em.I18n.t('jobs.hive.failed'));
+              }
+              else {
+                vertexRecord.set('state', data.otherinfo.status);
+              }
+              if (data.otherinfo.counters && data.otherinfo.counters.counterGroups) {
+                data.otherinfo.counters.counterGroups.forEach(function (cGroup) {
+                  var cNameToPropetyMap = {};
+                  switch (cGroup.counterGroupName) {
+                    case 'org.apache.tez.common.counters.FileSystemCounter':
+                      cNameToPropetyMap = {
+                        'FILE_BYTES_READ': 'fileReadBytes',
+                        'FILE_BYTES_WRITTEN': 'fileWriteBytes',
+                        'FILE_READ_OPS': 'fileReadOps',
+                        'FILE_WRITE_OPS': 'fileWriteOps',
+                        'HDFS_BYTES_READ': 'hdfsReadBytes',
+                        'HDFS_BYTES_WRITTEN': 'hdfsWriteBytes',
+                        'HDFS_READ_OPS': 'hdfsReadOps',
+                        'HDFS_WRITE_OPS': 'hdfsWriteOps'
+                      };
+                      break;
+                    case 'org.apache.tez.common.counters.TaskCounter':
+                      cNameToPropetyMap = {
+                        'SPILLED_RECORDS': 'spilledRecords'
+                      };
+                      break;
+                    case 'HIVE':
+                      cNameToPropetyMap = {
+                        'RECORDS_READ': 'recordReadCount',
+                        'RECORDS_WRITE': 'recordWriteCount'
+                      };
+                      break;
+                    default:
+                      break;
+                  }
+                  if (cGroup.counters) {
+                    cGroup.counters.forEach(function (counter) {
+                      var prop = cNameToPropetyMap[counter.counterName];
+                      if (prop != null) {
+                        vertexRecord.set(prop, counter.counterValue);
+                      }
+                    });
+                  }
+                });
+              }
+              successCallback();
+            }
+          }
+        },
+        loadTezDagVertexError: function (jqXHR, url, method, showStatus) {}
+      };
+    App.ajax.send({
+      name: 'jobs.tezDag.tezDagVertexId',
+      sender: sender,
+      data: {
+        historyServerHostName: historyServerHostName,
+        tezDagVertexId: tezVertexInstanceId,
+        ahsWebPort: ahsWebPort
+      },
+      success: 'loadTezDagVertexSuccess',
+      error: 'loadTezDagVertexError'
+    });
+  }
+};

http://git-wip-us.apache.org/repos/asf/ambari/blob/85362043/contrib/views/jobs/src/main/resources/ui/app/scripts/helpers/number.js
----------------------------------------------------------------------
diff --git a/contrib/views/jobs/src/main/resources/ui/app/scripts/helpers/number.js b/contrib/views/jobs/src/main/resources/ui/app/scripts/helpers/number.js
index 164ae96..4adf792 100644
--- a/contrib/views/jobs/src/main/resources/ui/app/scripts/helpers/number.js
+++ b/contrib/views/jobs/src/main/resources/ui/app/scripts/helpers/number.js
@@ -30,7 +30,7 @@ App.Helpers.number = {
    * @return {String} Returns converted value with abbreviation.
    */
   bytesToSize: function (bytes, precision, parseType, multiplyBy) {
-    if (bytes === null || bytes === undefined) {
+    if (Em.isNone(bytes)) {
       return 'n/a';
     } else {
       if (arguments[2] === undefined) {
@@ -52,6 +52,40 @@ App.Helpers.number = {
       var parsedValue = window[parseType](value);
       return parsedValue.toFixed(precision) + " " + sizes[posttxt];
     }
+  },
+
+  /**
+   * Validates if the given string or number is an integer between the
+   * values of min and max (inclusive). The minimum and maximum
+   * checks are ignored if their valid is NaN.
+   *
+   * @method validateInteger
+   * @param {string|number} str - input string
+   * @param {string|number} [min]
+   * @param {string|number} [max]
+   */
+  validateInteger : function(str, min, max) {
+    if (str==null || str==undefined || (str + "").trim().length < 1) {
+      return Em.I18n.t('number.validate.empty');
+    } else {
+      str = (str + "").trim();
+      var number = parseInt(str);
+      if (isNaN(number)) {
+        return Em.I18n.t('number.validate.notValidNumber');
+      } else {
+        if (str.length != (number + "").length) {
+          // parseInt("1abc") returns 1 as integer
+          return Em.I18n.t('number.validate.notValidNumber');
+        }
+        if (!isNaN(min) && number < min) {
+          return Em.I18n.t('number.validate.lessThanMinumum').fmt(min);
+        }
+        if (!isNaN(max) && number > max) {
+          return Em.I18n.t('number.validate.moreThanMaximum').fmt(max);
+        }
+      }
+    }
+    return null;
   }
 
 };

http://git-wip-us.apache.org/repos/asf/ambari/blob/85362043/contrib/views/jobs/src/main/resources/ui/app/scripts/helpers/string.js
----------------------------------------------------------------------
diff --git a/contrib/views/jobs/src/main/resources/ui/app/scripts/helpers/string.js b/contrib/views/jobs/src/main/resources/ui/app/scripts/helpers/string.js
index eb49c4a..487f4a3 100644
--- a/contrib/views/jobs/src/main/resources/ui/app/scripts/helpers/string.js
+++ b/contrib/views/jobs/src/main/resources/ui/app/scripts/helpers/string.js
@@ -40,6 +40,65 @@ App.Helpers.string = {
    */
   startsWith: function (str, prefix) {
     return str.indexOf(prefix) == 0;
+  },
+
+  getCamelCase: function(name) {
+    if (name != null) {
+      return name.toLowerCase().replace(/(\b\w)/g, function(f) {
+        return f.toUpperCase();
+      })
+    }
+    return name;
+  },
+
+  /**
+   * Finds the value in an object where this string is a key.
+   * Optionally, the index of the key can be provided where the
+   * value of the nth key in the hierarchy is returned.
+   *
+   * Example:
+   *  var tofind = 'smart';
+   *  var person = {'name': 'Bob Bob', 'smart': 'no', 'age': '28', 'personality': {'smart': 'yes', 'funny': 'yes', 'emotion': 'happy'} };
+   *  findIn(tofind, person); // 'no'
+   *  findIn(tofind, person, 0); // 'no'
+   *  findIn(tofind, person, 1); // 'yes'
+   *  findIn(tofind, person, 2); // null
+   *
+   *  @method findIn
+   *  @param s {string}
+   *  @param multi {object}
+   *  @param index {number} Occurrence count of this key
+   *  @param _foundValues {array}
+   *  @return {*} Value of key at given index
+   */
+  findIn: function(s, multi, index, _foundValues) {
+    if (!index) {
+      index = 0;
+    }
+    if (!_foundValues) {
+      _foundValues = [];
+    }
+    multi = multi || '';
+    var value = null,
+      str = this.valueOf();
+    if (typeof multi == 'object') {
+      for ( var key in multi) {
+        if (value != null) {
+          break;
+        }
+        if (key == str) {
+          _foundValues.push(multi[key]);
+        }
+        if (_foundValues.length - 1 == index) {
+          // Found the value
+          return _foundValues[index];
+        }
+        if (typeof multi[key] == 'object') {
+          value = value || this.findIn(s, multi[key], index, _foundValues);
+        }
+      }
+    }
+    return value;
   }
 
 };

http://git-wip-us.apache.org/repos/asf/ambari/blob/85362043/contrib/views/jobs/src/main/resources/ui/app/scripts/mappers/jobs/hive_job_mapper.js
----------------------------------------------------------------------
diff --git a/contrib/views/jobs/src/main/resources/ui/app/scripts/mappers/jobs/hive_job_mapper.js b/contrib/views/jobs/src/main/resources/ui/app/scripts/mappers/jobs/hive_job_mapper.js
index ad65902..059c08a 100644
--- a/contrib/views/jobs/src/main/resources/ui/app/scripts/mappers/jobs/hive_job_mapper.js
+++ b/contrib/views/jobs/src/main/resources/ui/app/scripts/mappers/jobs/hive_job_mapper.js
@@ -34,7 +34,6 @@
  */
 App.hiveJobMapper = App.QuickDataMapper.create({
 
-  model : App.HiveJob,
   map : function(json) {
     var model = this.get('model'),
       hiveJob = {};
@@ -80,8 +79,8 @@ App.hiveJobMapper = App.QuickDataMapper.create({
             var vertexObj = {
               id : dagName + "/" + vertexName,
               name : vertexName,
-              incoming_edges : [],
-              outgoing_edges : []
+              incomingEdges : [],
+              outgoingEdges : []
             };
             vertexIds.push(vertexObj.id);
             var operatorExtractor = function(obj) {
@@ -104,13 +103,13 @@ App.hiveJobMapper = App.QuickDataMapper.create({
             if (vertex["Map Operator Tree:"] != null) {
               vertexObj.type = App.TezDagVertexType.MAP;
               vertexObj.operations = operatorExtractor(vertex["Map Operator Tree:"]);
-              vertexObj.operation_plan = JSON.stringify(vertex["Map Operator Tree:"], undefined, "  ");
+              vertexObj.operationPlan = JSON.stringify(vertex["Map Operator Tree:"], undefined, "  ");
             }
             else
               if (vertex["Reduce Operator Tree:"] != null) {
                 vertexObj.type = App.TezDagVertexType.REDUCE;
                 vertexObj.operations = operatorExtractor(vertex["Reduce Operator Tree:"]);
-                vertexObj.operation_plan = JSON.stringify(vertex["Reduce Operator Tree:"], undefined, "  ");
+                vertexObj.operationPlan = JSON.stringify(vertex["Reduce Operator Tree:"], undefined, "  ");
               }
               else
                 if (vertex["Vertex:"] != null && vertexName==vertex['Vertex:']) {
@@ -141,21 +140,21 @@ App.hiveJobMapper = App.QuickDataMapper.create({
               }
               var edgeObj = {
                 id : dagName + "/" + parentVertex + "-" + childVertex,
-                from_vertex_id : dagName + "/" + parentVertex,
-                to_vertex_id : dagName + "/" + childVertex
+                fromVertex: dagName + "/" + parentVertex,
+                toVertex : dagName + "/" + childVertex
               };
-              vertexIdMap[edgeObj.from_vertex_id].outgoing_edges.push(edgeObj.id);
-              vertexIdMap[edgeObj.to_vertex_id].incoming_edges.push(edgeObj.id);
+              vertexIdMap[edgeObj.fromVertex].outgoingEdges.push(edgeObj.id);
+              vertexIdMap[edgeObj.toVertex].incomingEdges.push(edgeObj.id);
               edgeIds.push(edgeObj.id);
               switch (e.type) {
               case "BROADCAST_EDGE":
-                edgeObj.edge_type = App.TezDagEdgeType.BROADCAST;
+                edgeObj.edgeType = App.TezDagEdgeType.BROADCAST;
                 break;
               case "SIMPLE_EDGE":
-                edgeObj.edge_type = App.TezDagEdgeType.SCATTER_GATHER;
+                edgeObj.edgeType = App.TezDagEdgeType.SCATTER_GATHER;
                 break;
               case "CONTAINS":
-                edgeObj.edge_type = App.TezDagEdgeType.CONTAINS;
+                edgeObj.edgeType = App.TezDagEdgeType.CONTAINS;
                 break;
               default:
                 break;
@@ -175,40 +174,43 @@ App.hiveJobMapper = App.QuickDataMapper.create({
           // reload as the structure does not change. Reloading
           // here causes missing data (got from other calls)
           // to propagate into UI - causing flashing.
-          var newVertices = [];
-          var newEdges = [];
+          var newVertices = [],
+            newEdges = [];
           vertices.forEach(function(v) {
-            var vertexRecord = App.TezDagVertex.find(v.id);
-            if (!vertexRecord.get('isLoaded')) {
+            var vertexRecord = App.HiveJob.store.getById('tezDagVertex', v.id);
+            if (!vertexRecord || !vertexRecord.get('isLoaded')) {
               newVertices.push(v);
             }
           });
           edges.forEach(function(e) {
-            var edgeRecord = App.TezDagEdge.find(e.id);
-            if (!edgeRecord.get('isLoaded')) {
+            var edgeRecord = App.HiveJob.store.getById('tezDagEdge', e.id);
+            if (!edgeRecord || !edgeRecord.get('isLoaded')) {
               newEdges.push(e);
             }
           });
-          App.store.loadMany(App.TezDagVertex, newVertices);
-          App.store.loadMany(App.TezDagEdge, newEdges);
-          var dagRecord = App.TezDag.find(tezDag.id);
-          if (!dagRecord.get('isLoaded')) {
-            App.store.load(App.TezDag, tezDag);
+
+          App.HiveJob.store.pushMany('tezDagVertex', newVertices);
+          App.HiveJob.store.pushMany('tezDagEdge', newEdges);
+
+          var dagRecord = App.HiveJob.store.getById('tezDag', tezDag.id);
+          if (!dagRecord || !dagRecord.get('isLoaded')) {
+            App.HiveJob.store.push('tezDag', tezDag);
           }
           hiveJob.tezDag = tezDag.id;
         }
       }
-      if(App.HiveJob.find().get('content').length == 0){
-        App.store.load(model, hiveJob);
+
+      if(App.HiveJob.store.all('hiveJob').length == 0){
+        App.HiveJob.store.push('hiveJob', hiveJob);
       }
-      var hiveJobRecord = App.HiveJob.find(hiveJob.id);
+      var hiveJobRecord = App.HiveJob.store.getById('hiveJob', hiveJob.id);
       if (hiveJobRecord != null) {
-        hiveJobRecord.set('stages', hiveJob.stages.sortProperty('id'));
+        hiveJobRecord.set('stages', hiveJob.stages.sortBy('id'));
         hiveJobRecord.set('startTime', hiveJob.startTime);
         hiveJobRecord.set('endTime', hiveJob.endTime);
         if (hiveJob.tezDag != null) {
           // Some hive queries dont use Tez
-          hiveJobRecord.set('tezDag', App.TezDag.find(hiveJob.tezDag));
+          hiveJobRecord.set('tezDag', App.HiveJob.store.getById('tezDag', hiveJob.tezDag));
         }
       }
     }

http://git-wip-us.apache.org/repos/asf/ambari/blob/85362043/contrib/views/jobs/src/main/resources/ui/app/scripts/mappers/jobs/hive_jobs_mapper.js
----------------------------------------------------------------------
diff --git a/contrib/views/jobs/src/main/resources/ui/app/scripts/mappers/jobs/hive_jobs_mapper.js b/contrib/views/jobs/src/main/resources/ui/app/scripts/mappers/jobs/hive_jobs_mapper.js
index 49b373c..e87d8e9 100644
--- a/contrib/views/jobs/src/main/resources/ui/app/scripts/mappers/jobs/hive_jobs_mapper.js
+++ b/contrib/views/jobs/src/main/resources/ui/app/scripts/mappers/jobs/hive_jobs_mapper.js
@@ -83,6 +83,10 @@ App.hiveJobsMapper = App.QuickDataMapper.create({
           });
         }
         hiveJobs.push(hiveJob);
+        var tezDag = App.HiveJob.store.all('tezDag').findBy('hiveJob.id', hiveJob.id);
+        if (!Em.isNone(tezDag)) {
+          hiveJob.tezDag = tezDag.id;
+        }
         jobsToDelete = jobsToDelete.without(hiveJob.id);
       });
 

http://git-wip-us.apache.org/repos/asf/ambari/blob/85362043/contrib/views/jobs/src/main/resources/ui/app/scripts/models/jobs/hive_job.js
----------------------------------------------------------------------
diff --git a/contrib/views/jobs/src/main/resources/ui/app/scripts/models/jobs/hive_job.js b/contrib/views/jobs/src/main/resources/ui/app/scripts/models/jobs/hive_job.js
index 53d309e..0ecdaa1 100644
--- a/contrib/views/jobs/src/main/resources/ui/app/scripts/models/jobs/hive_job.js
+++ b/contrib/views/jobs/src/main/resources/ui/app/scripts/models/jobs/hive_job.js
@@ -17,13 +17,13 @@
 
 App.HiveJob = App.AbstractJob.extend({
 
-  jobType : App.JobType.HIVE,
+  jobType: App.JobType.HIVE,
 
-  queryText : DS.attr('string'),
+  queryText: DS.attr('string'),
 
   hasTezDag: DS.attr('boolean'),
 
-  tezDag : DS.belongsTo('tezDag', {async:true}),
+  tezDag: DS.belongsTo('tezDag'),
 
   failed: DS.attr('boolean')
 

http://git-wip-us.apache.org/repos/asf/ambari/blob/85362043/contrib/views/jobs/src/main/resources/ui/app/scripts/models/jobs/tez_dag.js
----------------------------------------------------------------------
diff --git a/contrib/views/jobs/src/main/resources/ui/app/scripts/models/jobs/tez_dag.js b/contrib/views/jobs/src/main/resources/ui/app/scripts/models/jobs/tez_dag.js
index 71a42b6..cd6178b 100644
--- a/contrib/views/jobs/src/main/resources/ui/app/scripts/models/jobs/tez_dag.js
+++ b/contrib/views/jobs/src/main/resources/ui/app/scripts/models/jobs/tez_dag.js
@@ -20,33 +20,38 @@ App.TezDag = DS.Model.extend({
   /**
    * When DAG is actually running on server, a unique ID is assigned.
    */
-  instanceId : DS.attr('string'),
+  instanceId: DS.attr('string'),
 
-  name : DS.attr('string'),
+  name: DS.attr('string'),
 
   yarnApplicationId: DS.attr('string'),
 
-  stage : DS.attr('string'),
+  stage: DS.attr('string'),
 
-  vertices : DS.hasMany('tezDagVertex', {async:true}),
+  vertices: DS.hasMany('tezDagVertex'),
 
-  edges : DS.hasMany('tezDagEdge', {async:true})
+  edges: DS.hasMany('tezDagEdge'),
+
+  hiveJob: DS.belongsTo('hiveJob')
 
 });
 
 App.TezDagEdge = DS.Model.extend({
 
-  instanceId : DS.attr('string'),
+  instanceId: DS.attr('string'),
 
-  fromVertex : DS.belongsTo('tezDagVertex', {async:true}),
+  fromVertex: DS.belongsTo('tezDagVertex'),
 
-  toVertex : DS.belongsTo('tezDagVertex', {async:true}),
+  toVertex: DS.belongsTo('tezDagVertex'),
 
   /**
    * Type of this edge connecting vertices. Should be one of constants defined
    * in 'App.TezDagEdgeType'.
    */
-  edgeType : DS.attr('string')
+  edgeType: DS.attr('string'),
+
+  tezDag: DS.belongsTo('tezDag')
+
 });
 
 App.TezDagVertex = DS.Model.extend({
@@ -54,35 +59,37 @@ App.TezDagVertex = DS.Model.extend({
   /**
    * When DAG vertex is actually running on server, a unique ID is assigned.
    */
-  instanceId : DS.attr('string'),
+  instanceId: DS.attr('string'),
+
+  name: DS.attr('string'),
 
-  name : DS.attr('string'),
+  tezDag: DS.belongsTo('tezDag'),
 
   /**
    * State of this vertex. Should be one of constants defined in
    * App.TezDagVertexState.
    */
-  state : DS.attr('string'),
+  state: DS.attr('string'),
 
   /**
    * Vertex type has to be one of the types defined in 'App.TezDagVertexType'
    * @return {string}
    */
-  type : DS.attr('string'),
+  type: DS.attr('string'),
 
   /**
    * A vertex can have multiple incoming edges.
    */
-  incomingEdges : DS.hasMany('tezDagEdge', {async:true}),
+  incomingEdges: DS.hasMany('tezDagEdge'),
 
   /**
    * This vertex can have multiple outgoing edges.
    */
-  outgoingEdges : DS.hasMany('tezDagEdge', {async:true}),
+  outgoingEdges: DS.hasMany('tezDagEdge'),
 
-  startTime : DS.attr('number'),
+  startTime: DS.attr('number'),
 
-  endTime : DS.attr('number'),
+  endTime: DS.attr('number'),
 
   /**
    * Provides the duration of this job. If the job has not started, duration
@@ -90,7 +97,7 @@ App.TezDagVertex = DS.Model.extend({
    *
    * @return {Number} Duration in milliseconds.
    */
-  duration : function() {
+  duration: function () {
     return App.Helpers.date.duration(this.get('startTime'), this.get('endTime'))
   }.property('startTime', 'endTime'),
 
@@ -101,89 +108,89 @@ App.TezDagVertex = DS.Model.extend({
    *
    * Array of strings. [{string}]
    */
-  operations : DS.attr('array'),
+  operations: DS.attr('array'),
 
   /**
    * Provides additional information about the 'operations' performed in this
    * vertex. This is shown directly to the user.
    */
-  operationPlan : DS.attr('string'),
+  operationPlan: DS.attr('string'),
 
   /**
    * Number of actual Map/Reduce tasks in this vertex
    */
-  tasksCount : DS.attr('number'),
+  tasksCount: DS.attr('number'),
 
   tasksNumber: function () {
-    return this.get('tasksCount') ? this.get('tasksCount') : 0;
+    return this.getWithDefault('tasksCount', 0);
   }.property('tasksCount'),
 
   /**
    * Local filesystem usage metrics for this vertex
    */
-  fileReadBytes : DS.attr('number'),
+  fileReadBytes: DS.attr('number'),
 
-  fileWriteBytes : DS.attr('number'),
+  fileWriteBytes: DS.attr('number'),
 
-  fileReadOps : DS.attr('number'),
+  fileReadOps: DS.attr('number'),
 
-  fileWriteOps : DS.attr('number'),
+  fileWriteOps: DS.attr('number'),
 
   /**
    * Spilled records
    */
-  spilledRecords : DS.attr('number'),
+  spilledRecords: DS.attr('number'),
 
   /**
    * HDFS usage metrics for this vertex
    */
-  hdfsReadBytes : DS.attr('number'),
+  hdfsReadBytes: DS.attr('number'),
 
-  hdfsWriteBytes : DS.attr('number'),
+  hdfsWriteBytes: DS.attr('number'),
 
-  hdfsReadOps : DS.attr('number'),
+  hdfsReadOps: DS.attr('number'),
 
-  hdfsWriteOps : DS.attr('number'),
+  hdfsWriteOps: DS.attr('number'),
 
   /**
    * Record metrics for this vertex
    */
-  recordReadCount : DS.attr('number'),
+  recordReadCount: DS.attr('number'),
 
-  recordWriteCount : DS.attr('number'),
+  recordWriteCount: DS.attr('number'),
 
-  totalReadBytes : function() {
+  totalReadBytes: function () {
     return this.get('fileReadBytes') + this.get('hdfsReadBytes');
   }.property('fileReadBytes', 'hdfsReadBytes'),
 
-  totalWriteBytes : function() {
+  totalWriteBytes: function () {
     return this.get('fileWriteBytes') + this.get('hdfsWriteBytes');
   }.property('fileWriteBytes', 'hdfsWriteBytes'),
 
-  totalReadBytesDisplay : function() {
+  totalReadBytesDisplay: function () {
     return  App.Helpers.number.bytesToSize(this.get('totalReadBytes'));
   }.property('totalReadBytes'),
 
-  totalWriteBytesDisplay : function() {
+  totalWriteBytesDisplay: function () {
     return  App.Helpers.number.bytesToSize(this.get('totalWriteBytes'));
   }.property('totalWriteBytes'),
 
-  durationDisplay : function() {
+  durationDisplay: function () {
     return App.Helpers.date.timingFormat(this.get('duration'), true);
   }.property('duration')
 
 });
 
 App.TezDagVertexState = {
-  NEW : "NEW",
-  INITIALIZING : "INITIALIZING",
-  INITED : "INITED",
-  RUNNING : "RUNNING",
-  SUCCEEDED : "SUCCEEDED",
-  FAILED : "FAILED",
-  KILLED : "KILLED",
-  ERROR : "ERROR",
-  TERMINATING : "TERMINATING",
+  NEW: "NEW",
+  INITIALIZING: "INITIALIZING",
+  INITED: "INITED",
+  RUNNING: "RUNNING",
+  SUCCEEDED: "SUCCEEDED",
+  FAILED: "FAILED",
+  KILLED: "KILLED",
+  ERROR: "ERROR",
+  TERMINATING: "TERMINATING",
   JOBFAILED: "JOB FAILED"
 };
 
@@ -194,8 +201,8 @@ App.TezDagVertexType = {
 };
 
 App.TezDagEdgeType = {
-  SCATTER_GATHER : "SCATTER_GATHER",
-  BROADCAST : "BROADCAST",
+  SCATTER_GATHER: "SCATTER_GATHER",
+  BROADCAST: "BROADCAST",
   CONTAINS: "CONTAINS"
 };
 

http://git-wip-us.apache.org/repos/asf/ambari/blob/85362043/contrib/views/jobs/src/main/resources/ui/app/scripts/router.js
----------------------------------------------------------------------
diff --git a/contrib/views/jobs/src/main/resources/ui/app/scripts/router.js b/contrib/views/jobs/src/main/resources/ui/app/scripts/router.js
index 83df6bf..8fea4db 100644
--- a/contrib/views/jobs/src/main/resources/ui/app/scripts/router.js
+++ b/contrib/views/jobs/src/main/resources/ui/app/scripts/router.js
@@ -18,5 +18,5 @@
 
 App.Router.map(function () {
   this.resource("jobs", { path: "/" });
-  this.resource('job', { path: 'jobs/:job_id' });
+  this.resource('job', { path: 'jobs/:hive_job_id' });
 });

http://git-wip-us.apache.org/repos/asf/ambari/blob/85362043/contrib/views/jobs/src/main/resources/ui/app/scripts/routes/application_route.js
----------------------------------------------------------------------
diff --git a/contrib/views/jobs/src/main/resources/ui/app/scripts/routes/application_route.js b/contrib/views/jobs/src/main/resources/ui/app/scripts/routes/application_route.js
index fa0e128..7532197 100644
--- a/contrib/views/jobs/src/main/resources/ui/app/scripts/routes/application_route.js
+++ b/contrib/views/jobs/src/main/resources/ui/app/scripts/routes/application_route.js
@@ -16,13 +16,13 @@
  * limitations under the License.
  */
 
-App.ApplicationRoute = Ember.Route.extend({
+App.IndexRoute = Ember.Route.extend({
 
   model: function () {
     return this.modelFor('hiveJobs');
   },
 
-  redirect: function () {
+  beforeModel: function () {
     this.transitionTo('jobs');
   }
 
@@ -31,15 +31,20 @@ App.ApplicationRoute = Ember.Route.extend({
 App.JobsRoute = Ember.Route.extend({
 
   model: function () {
-    return this.store.all('hiveJob');
+    return this.get('store').find('hiveJob');
   }
 
 });
 
 App.JobRoute = Ember.Route.extend({
 
+  setupController: function(controller, model) {
+    this._super(controller, model);
+    controller.set('loaded', false);
+  },
+
   model: function (params) {
-    return this.store.all('hiveJob', params.id);
+    return this.get('store').getById('hiveJob', params.hive_job_id);
   }
 
 });
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ambari/blob/85362043/contrib/views/jobs/src/main/resources/ui/app/scripts/translations.js
----------------------------------------------------------------------
diff --git a/contrib/views/jobs/src/main/resources/ui/app/scripts/translations.js b/contrib/views/jobs/src/main/resources/ui/app/scripts/translations.js
index 8389a08..95c7fcb 100644
--- a/contrib/views/jobs/src/main/resources/ui/app/scripts/translations.js
+++ b/contrib/views/jobs/src/main/resources/ui/app/scripts/translations.js
@@ -23,6 +23,20 @@ Ember.I18n.translations = {
   'ok': 'Ok',
   'cancel': 'Cancel',
 
+
+  'common.status':'Status',
+  'common.time.start': 'Start Time',
+  'common.time.end': 'End Time',
+  'common.name': 'Name',
+  'common.tasks':'Tasks',
+  'common.na': 'n/a',
+  'common.value': 'Value',
+
+  'number.validate.empty': 'cannot be empty',
+  'number.validate.notValidNumber': 'not a valid number',
+  'number.validate.lessThanMinumum': 'value less than %@1',
+  'number.validate.moreThanMaximum': 'value greater than %@1',
+
   'jobs.type':'Jobs Type',
   'jobs.type.hive':'Hive',
   'jobs.show.up.to':'Show up to',
@@ -58,9 +72,9 @@ Ember.I18n.translations = {
   'jobs.hive.tez.localFiles':'Local Files',
   'jobs.hive.tez.spilledRecords':'Spilled Records',
   'jobs.hive.tez.records':'Records',
-  'jobs.hive.tez.reads':'{0} reads',
-  'jobs.hive.tez.writes':'{0} writes',
-  'jobs.hive.tez.records.count':'{0} Records',
+  'jobs.hive.tez.reads':'%@1 reads',
+  'jobs.hive.tez.writes':'%@1 writes',
+  'jobs.hive.tez.records.count':'%@1 Records',
   'jobs.hive.tez.operatorPlan':'Operator Plan',
   'jobs.hive.tez.dag.summary.metric':'Summary Metric',
   'jobs.hive.tez.dag.error.noDag.title':'No Tez Information',
@@ -78,6 +92,21 @@ Ember.I18n.translations = {
   'jobs.hive.tez.edge.':'Unknown',
   'jobs.hive.tez.edge.contains':'Contains',
   'jobs.hive.tez.edge.broadcast':'Broadcast',
-  'jobs.hive.tez.edge.scatter_gather':'Shuffle'
+  'jobs.hive.tez.edge.scatter_gather':'Shuffle',
+
+  'app.loadingPlaceholder': 'Loading...',
+  'apps.item.dag.job': 'Job',
+  'apps.item.dag.jobId': 'Job Id',
+  'apps.item.dag.type': 'Job Type',
+  'apps.item.dag.status': 'Status',
+  'apps.item.dag.num_stages': 'Total Stages',
+  'apps.item.dag.stages': 'Tasks per Stage',
+  'apps.item.dag.maps': 'Maps',
+  'apps.item.dag.reduces': 'Reduces',
+  'apps.item.dag.input': 'Input',
+  'apps.item.dag.output': 'Output',
+  'apps.item.dag.duration': 'Duration',
+
+  'menu.item.jobs':'Jobs'
 
 };

http://git-wip-us.apache.org/repos/asf/ambari/blob/85362043/contrib/views/jobs/src/main/resources/ui/app/scripts/views/job/hive_job_details_tez_dag_view.js
----------------------------------------------------------------------
diff --git a/contrib/views/jobs/src/main/resources/ui/app/scripts/views/job/hive_job_details_tez_dag_view.js b/contrib/views/jobs/src/main/resources/ui/app/scripts/views/job/hive_job_details_tez_dag_view.js
new file mode 100644
index 0000000..7d73f06
--- /dev/null
+++ b/contrib/views/jobs/src/main/resources/ui/app/scripts/views/job/hive_job_details_tez_dag_view.js
@@ -0,0 +1,925 @@
+/**
+ * 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.MainHiveJobDetailsTezDagView = Em.View.extend({
+  templateName: 'job/hive_job_details_tez_dag',
+  selectedVertex: null,
+  summaryMetricType: null,
+  svgVerticesLayer: null, // The contents of the <svg> element.
+  svgTezRoot: null,
+  svgWidth: -1,
+  svgHeight: -1,
+
+  // zoomScaleFom: -1, // Bound from parent view
+  // zoomScaleTo: -1, // Bound from parent view
+  // zoomScale: -1, // Bound from parent view
+  zoomTranslate: [0, 0],
+  zoomBehavior: null,
+  svgCreated: false,
+
+  /**
+   * Populated by #drawTezDag()
+   *
+   * {
+   *   "nodes": [
+   *     {
+   *       "id": "Map2",
+   *       "name": "Map 2",
+   *       "type": App.TezDagVertexType.MAP,
+   *       "operations": [
+   *         "TableScan",
+   *         "File Output"
+   *       ],
+   *       "depth": 1,
+   *       "parents": [],
+   *       "children": [],
+   *       "x": 0,
+   *       "y": 0,
+   *       "metricDisplay": "100MB",
+   *       "metricPercent": 64,
+   *       "metricType": "Input",
+   *       "selected": true,
+   *       "fixed": true,
+   *       "metrics": {
+   *         "input": 40022,
+   *         "output": 224344,
+   *         "recordsRead": 200,
+   *         "recordsWrite": 122,
+   *         "tezTasks": 2
+   *       }
+   *     }
+   *   ],
+   *   "links": [
+   *     {
+   *       "source": {},
+   *       "target": {},
+   *       "edgeType": "BROADCAST"
+   *     }
+   *   ]
+   * }
+   */
+  dagVisualModel: {
+    nodes: [],
+    links: [],
+    maxMetrics: {},
+    minMetrics: {}
+  },
+
+  didInsertElement: function () {
+    this._super();
+    this.createSvg();
+  },
+
+  willDestroyElement: function () {
+    $('.svg-tooltip').tooltip('destroy');
+  },
+
+  createSvg: function () {
+    var self = this;
+    var dagVisualModel = this.get('dagVisualModel');
+    dagVisualModel.nodes.clear();
+    dagVisualModel.links.clear();
+    dagVisualModel.maxMetrics = {};
+    dagVisualModel.minMetrics = {};
+
+    //this.set('content', this.get('content'));
+    var svg = d3.select("#tez-dag-svg");
+    d3.selectAll(".tez-dag-canvas").remove();
+    var tezRoot = svg.append("svg:g").attr("class", "tez-root");
+    this.set('svgTezRoot', tezRoot);
+    var tezRootRect = tezRoot.append("rect").attr("class", "tez-root-rect");
+    this.set('svgVerticesLayer', tezRoot.append("svg:g").attr("class", "tez-dag-canvas"));
+    this.adjustGraphHeight();
+    var canvasSize = this.drawTezDag();
+    var minScale = Math.min(this.get('svgHeight') / canvasSize.height, this.get('svgWidth') / canvasSize.width);
+    if (minScale > 1) {
+      minScale = 1;
+    }
+    tezRootRect.attr("width", canvasSize.width).attr("height", canvasSize.height);
+    var zoom = d3.behavior.zoom().scaleExtent([ minScale, 2 ]).on("zoom", function () {
+      tezRoot.attr("transform", "translate(" + (d3.event.translate) + ")scale(" + d3.event.scale + ")");
+      self.set('zoomScale', d3.event.scale);
+      self.set('zoomTranslate', d3.event.translate);
+    });
+    svg.call(zoom);
+    this.set('zoomBehavior', zoom);
+    this.set('zoomTranslate', [0, 0]);
+    this.set('zoomScaleFrom', minScale);
+    this.set('zoomScaleTo', 2);
+    this.set('zoomScale', minScale);
+    this.set('svgCreated', true);
+  },
+
+  zoomScaleObserver: function () {
+    var tezRoot = this.get("svgTezRoot");
+    var newScale = this.get('zoomScale');
+    var newScaleFrom = this.get('zoomScaleFrom');
+    var newScaleTo = this.get('zoomScaleTo');
+    var zoomTranslate = this.get('zoomTranslate');
+    var zoomBehavior = this.get('zoomBehavior');
+    if (d3.event == null && this.get('svgCreated')) {
+      // Values were set from actions instead of UI events
+      // We need to center in on selected vertex if available.
+      var selectedNode = null;
+      var dagVisualModel = this.get('dagVisualModel');
+      if (dagVisualModel && dagVisualModel.nodes && dagVisualModel.nodes.length > 0) {
+        dagVisualModel.nodes.every(function (node) {
+          if (node.selected) {
+            selectedNode = node;
+            return false;
+          }
+          return true;
+        })
+      }
+      if (selectedNode != null) {
+        var cX = selectedNode.x + selectedNode.width / 2;
+        var cY = selectedNode.y + selectedNode.height / 2;
+        var mX = (cX * zoomBehavior.scale()) + zoomTranslate[0];
+        var mY = (cY * zoomBehavior.scale()) + zoomTranslate[1];
+        var pX = (cX * newScale) + zoomTranslate[0];
+        var pY = (cY * newScale) + zoomTranslate[1];
+        var nX = (mX - pX);
+        var nY = (mY - pY);
+        zoomTranslate[0] += nX;
+        zoomTranslate[1] += nY;
+        this.set('zoomTranslate', zoomTranslate);
+      }
+    }
+    zoomBehavior.scale(newScale);
+    zoomBehavior.translate(zoomTranslate);
+    tezRoot.attr("transform", "translate(" + zoomTranslate + ")scale(" + newScale + ")");
+  }.observes('zoomScale', 'zoomScaleFrom', 'zoomScaleTo', 'zoomTranslate'),
+
+  /**
+   * We have to make the height of the DAG section match the height of the
+   * Summary section.
+   */
+  adjustGraphHeight: function () {
+    var rhsDiv = document.getElementById('tez-vertices-rhs');
+    var lhsDiv = document.getElementById('tez-dag-section');
+    if (lhsDiv && rhsDiv) {
+      var rhsHeight = rhsDiv.clientHeight - 26; // box boundary
+      var currentWidth = lhsDiv.clientWidth;
+      var currentHeight = lhsDiv.clientHeight;
+      $(lhsDiv).attr('style', "height:" + rhsHeight + "px;");
+      var svgHeight = rhsHeight - 20;
+      d3.select("#tez-dag-svg").attr('height', svgHeight).attr('width', '100%');
+      this.set('svgWidth', currentWidth);
+      this.set('svgHeight', svgHeight);
+    }
+  },
+
+  vertexSelectionUpdated: function () {
+    var vertexId = this.get('selectedVertex.id');
+    var zoomTranslate = [];
+    var zoomBehavior = this.get('zoomBehavior');
+    var selectedNode = this.get('dagVisualModel').nodes.findProperty('id', vertexId);
+    var dagVisualModel = this.get('dagVisualModel');
+    if (dagVisualModel && dagVisualModel.nodes && dagVisualModel.nodes.length > 0) {
+      dagVisualModel.nodes.forEach(function (node) {
+        node.selected = node.id == vertexId;
+      })
+    }
+    if (!this.get('selectedVertex.notTableClick')) {
+      var cX = selectedNode.x + (selectedNode.width) / 2;
+      var cY = selectedNode.y + (selectedNode.height) / 2;
+      zoomTranslate[0] = (225 / zoomBehavior.scale() - cX);
+      zoomTranslate[1] = (250 / zoomBehavior.scale() - cY);
+      this.set('zoomTranslate', [0, 0]);
+      this.get('svgVerticesLayer').attr("transform", "translate(0,0)");
+      this.get('svgVerticesLayer').attr("transform", "translate(" + zoomTranslate[0] + "," + zoomTranslate[1] + ")");
+    }
+    this.refreshGraphUI();
+  }.observes('selectedVertex'),
+
+  summaryMetricTypeUpdated: function () {
+    var summaryMetricType = this.get('summaryMetricType');
+    var dagVisualModel = this.get('dagVisualModel');
+    var min = dagVisualModel.minMetrics[summaryMetricType];
+    var max = dagVisualModel.maxMetrics[summaryMetricType];
+    dagVisualModel.nodes.forEach(function (node) {
+      var value = node.metrics[summaryMetricType];
+      var percent = -1;
+      if (App.Helpers.number.validateInteger(value) == null && value >= 0) {
+        if (App.Helpers.number.validateInteger(min) == null && App.Helpers.number.validateInteger(max) == null) {
+          if (max > min && value >= 0) {
+            percent = Math.round((value - min) * 100 / (max - min));
+          }
+        }
+      } else {
+        value = '';
+      }
+      switch (summaryMetricType) {
+        case "input":
+        case "output":
+          value = App.Helpers.number.bytesToSize(value);
+          break;
+        default:
+          break;
+      }
+      node.metricType = Em.I18n.t('jobs.hive.tez.metric.' + summaryMetricType);
+      node.metricDisplay = value;
+      node.metricPercent = percent;
+    });
+    this.refreshGraphUI();
+  }.observes('summaryMetricType'),
+
+  /**
+   * Observes metrics of all vertices.
+   */
+  vertexMetricsUpdated: function () {
+    var dagVisualModel = this.get('dagVisualModel');
+    dagVisualModel.minMetrics = {
+      input: Number.MAX_VALUE,
+      output: Number.MAX_VALUE,
+      recordsRead: Number.MAX_VALUE,
+      recordsWrite: Number.MAX_VALUE,
+      tezTasks: Number.MAX_VALUE,
+      spilledRecords: Number.MAX_VALUE
+    };
+    dagVisualModel.maxMetrics = {
+      input: 0,
+      output: 0,
+      recordsRead: 0,
+      recordsWrite: 0,
+      tezTasks: 0,
+      spilledRecords: 0
+    };
+    if (dagVisualModel.nodes) {
+      dagVisualModel.nodes.forEach(function (node) {
+        var vertex = App.HiveJob.store.getById('tezDagVertex', node.id);
+        if (vertex) {
+          node.metrics['input'] = vertex.get('fileReadBytes') + vertex.get('hdfsReadBytes');
+          node.metrics['output'] = vertex.get('fileWriteBytes') + vertex.get('hdfsWriteBytes');
+          node.metrics['recordsRead'] = vertex.get('recordReadCount');
+          node.metrics['recordsWrite'] = vertex.get('recordWriteCount');
+          node.metrics['tezTasks'] = vertex.get('tasksCount');
+          node.metrics['spilledRecords'] = vertex.get('spilledRecords');
+          node.state = vertex.get('state');
+          // Min metrics
+          dagVisualModel.minMetrics.input = Math.min(dagVisualModel.minMetrics.input, node.metrics.input);
+          dagVisualModel.minMetrics.output = Math.min(dagVisualModel.minMetrics.output, node.metrics.output);
+          dagVisualModel.minMetrics.recordsRead = Math.min(dagVisualModel.minMetrics.recordsRead, node.metrics.recordsRead);
+          dagVisualModel.minMetrics.recordsWrite = Math.min(dagVisualModel.minMetrics.recordsWrite, node.metrics.recordsWrite);
+          dagVisualModel.minMetrics.tezTasks = Math.min(dagVisualModel.minMetrics.tezTasks, node.metrics.tezTasks);
+          dagVisualModel.minMetrics.spilledRecords = Math.min(dagVisualModel.minMetrics.spilledRecords, node.metrics.spilledRecords);
+          // Max metrics
+          dagVisualModel.maxMetrics.input = Math.max(dagVisualModel.maxMetrics.input, node.metrics.input);
+          dagVisualModel.maxMetrics.output = Math.max(dagVisualModel.maxMetrics.output, node.metrics.output);
+          dagVisualModel.maxMetrics.recordsRead = Math.max(dagVisualModel.maxMetrics.recordsRead, node.metrics.recordsRead);
+          dagVisualModel.maxMetrics.recordsWrite = Math.max(dagVisualModel.maxMetrics.recordsWrite, node.metrics.recordsWrite);
+          dagVisualModel.maxMetrics.tezTasks = Math.max(dagVisualModel.maxMetrics.tezTasks, node.metrics.tezTasks);
+          dagVisualModel.maxMetrics.spilledRecords = Math.max(dagVisualModel.maxMetrics.spilledRecords, node.metrics.spilledRecords);
+        }
+      });
+    }
+    Ember.run.once(this, 'summaryMetricTypeUpdated');
+  }.observes('content.tezDag.vertices.@each.fileReadBytes', 'content.tezDag.vertices.@each.fileWriteBytes',
+      'content.tezDag.vertices.@each.hdfsReadBytes', 'content.tezDag.vertices.@each.hdfsWriteBytes',
+      'content.tezDag.vertices.@each.recordReadCount', 'content.tezDag.vertices.@each.recordWriteCount',
+      'content.tezDag.vertices.@each.state', 'content.tezDag.vertices.@each.spilledRecords'),
+
+  createOperationPlanObj: function (vertexName, op, opIndex) {
+    var operatorPlanObj = [],
+      text = this.get('content.tezDag.vertices').findBy('name', vertexName).get('operationPlan');
+    text = text.replace(/:"/g, '"').replace(/([:,])(?=\S)/g, '$1 ');
+    var jsonText = $.parseJSON(text);
+    opIndex = opIndex ? parseInt(opIndex) - 1 : 0;
+    jsonText = App.Helpers.string.findIn(op, jsonText, opIndex);
+    if (jsonText != null) {
+      for (var key in jsonText) {
+        if (jsonText.hasOwnProperty(key) && typeof(jsonText[key]) == "string") {
+          operatorPlanObj.push({
+            name: key,
+            value: jsonText[key]
+          });
+        }
+      }
+    }
+    return operatorPlanObj;
+  },
+
+  /**
+   * Determines layout and creates Tez graph. In the process it populates the
+   * visual model into 'dagVisualModel' field.
+   *
+   * Terminology: 'vertices' and 'edges' are Tez terms. 'nodes' and 'links' are
+   * visual (d3) terms.
+   */
+  drawTezDag: function () {
+    var self = this,
+      width = this.get('svgWidth'),
+      svgLayer = this.get('svgVerticesLayer'),
+      vertices = this.get('content.tezDag.vertices'),
+      edges = this.get('content.tezDag.edges'),
+      constants = this.get('constants'),
+      vertexIdToNode = {},
+      depthToNodes = [], // Array of id arrays
+      dagVisualModel = this.get('dagVisualModel'),
+      selectedVertex = this.get('selectedVertex'),
+      minVertexDuration = Number.MAX_VALUE,
+      maxVertexDuration = Number.MIN_VALUE;
+
+    //
+    // CALCULATE DEPTH - BFS to get correct graph depth
+    //
+    var visitEdges = [];
+    var maxRowLength = 0;
+    var maxRowDepth = 0;
+    vertices.forEach(function (v) {
+      if (v.get('incomingEdges.length') < 1) {
+        visitEdges.push({
+          depth: 0,
+          parent: null,
+          toVertex: v
+        });
+      }
+    });
+    function getNodeFromEdge(edgeObj) {
+      var vertex = edgeObj.toVertex;
+      var pName = edgeObj.parent ? edgeObj.parent.name : null;
+      var cName = edgeObj.toVertex ? edgeObj.toVertex.get('name') : null;
+      if (edgeObj.parent && edgeObj.depth < edgeObj.parent.depth + 1) {
+        edgeObj.depth = edgeObj.parent.depth + 1;
+      }
+      var node = vertexIdToNode[vertex.get('id')];
+      for (var k = depthToNodes.length; k <= edgeObj.depth; k++) {
+        depthToNodes.push([]);
+      }
+      if (!node) {
+        // New node
+        node = {
+          id: vertex.get('id'),
+          name: vertex.get('name'),
+          state: vertex.get('state'),
+          type: vertex.get('type'),
+          operations: vertex.get('operations'),
+          depth: edgeObj.depth,
+          parents: [],
+          children: [],
+          x: 0,
+          y: 0,
+          metricType: null,
+          metricDisplay: null,
+          metricPercent: -1,
+          selected: selectedVertex != null ? selectedVertex.get('id') == vertex.get('id') : false,
+          fixed: true,
+          metrics: {
+            input: -1,
+            output: -1,
+            recordsRead: -1,
+            recordsWrite: -1,
+            tezTasks: -1
+          },
+          duration: vertex.get('duration')
+        };
+        if (node.duration < minVertexDuration && node.duration > 0) {
+          minVertexDuration = node.duration;
+        }
+        if (node.duration > maxVertexDuration && node.duration > 0) {
+          maxVertexDuration = node.duration;
+        }
+        vertexIdToNode[vertex.get('id')] = node;
+        depthToNodes[node.depth].push(node);
+      } else {
+        // Existing node
+        if (edgeObj.depth > node.depth) {
+          function moveNodeToDepth(node, newDepth) {
+            var oldIndex = depthToNodes[node.depth].indexOf(node);
+            depthToNodes[node.depth].splice(oldIndex, 1);
+            node.depth = newDepth;
+            if (!depthToNodes[node.depth]) {
+              depthToNodes[node.depth] = [];
+            }
+            depthToNodes[node.depth].push(node);
+            if (node.children) {
+              // Move children down depth
+              node.children.forEach(function (c) {
+                moveNodeToDepth(c, node.depth + 1);
+              })
+            }
+          }
+
+          moveNodeToDepth(node, edgeObj.depth);
+        }
+      }
+      if (depthToNodes[node.depth].length > maxRowLength) {
+        maxRowLength = depthToNodes[node.depth].length;
+        maxRowDepth = node.depth;
+      }
+      if (edgeObj.parent != null) {
+        node.parents.push(edgeObj.parent);
+        edgeObj.parent.children.push(node);
+      }
+      return node;
+    }
+
+    var edgeObj;
+    var visitedVertexMap = {};
+    while (edgeObj = visitEdges.shift()) {
+      var node = getNodeFromEdge(edgeObj);
+      if (!visitedVertexMap[edgeObj.toVertex.get('id')]) {
+        visitedVertexMap[edgeObj.toVertex.get('id')] = true;
+        var outEdges = edgeObj.toVertex.get('outgoingEdges');
+        outEdges.forEach(function (oe) {
+          var childVertex = oe.get('toVertex');
+          visitEdges.push({
+            depth: node.depth + 1,
+            parent: node,
+            toVertex: childVertex
+          });
+        });
+      }
+    }
+    edges.forEach(function (e) {
+      dagVisualModel.links.push({
+        source: vertexIdToNode[e.get('fromVertex.id')],
+        target: vertexIdToNode[e.get('toVertex.id')],
+        edgeType: e.get('edgeType')
+      });
+    });
+    // Sort nodes so that parents stay together
+    for (var depth = 0; depth < depthToNodes.length; depth++) {
+      var nodes = depthToNodes[depth];
+      nodes.sort(function (n1, n2) {
+        var ck1 = '';
+        var ck2 = '';
+        if (n1.children) {
+          n1.children.forEach(function (c) {
+            ck1 += c.name;
+          });
+        }
+        if (n2.children) {
+          n2.children.forEach(function (c) {
+            ck2 += c.name;
+          });
+        }
+        if (ck1 < ck2) {
+          return -1
+        }
+        if (ck1 > ck2) {
+          return 1
+        }
+        return 0
+      });
+      depthToNodes[depth] = nodes;
+    }
+
+    //
+    // LAYOUT - Now with correct depth, we calculate layouts
+    //
+    // When a node's effective width changes, all its parent nodes are updated.
+    var updateNodeEffectiveWidth = function (node, newEffectiveWidth) {
+      if (App.Helpers.number.validateInteger(node.effectiveWidth) != null) {
+        node.effectiveWidth = newEffectiveWidth;
+      }
+      var diff = newEffectiveWidth - node.effectiveWidth;
+      if (diff > 0) {
+        var oldEffectiveWidth = node.effectiveWidth;
+        node.effectiveWidth = newEffectiveWidth;
+        if (node.parents != null) {
+          node.parents.forEach(function (parent) {
+            updateNodeEffectiveWidth(parent, parent.effectiveWidth + diff);
+          })
+        }
+      }
+    };
+    var xGap = 20;
+    var yGap = 70;
+    var currentY = 40;
+    // First pass - calculate layout widths, and Y coordinates
+    for (var depth = 0; depth < depthToNodes.length; depth++) {
+      var nodes = depthToNodes[depth];
+      var maxNodeHeight = 0;
+      for (var nodeIndex = 0; nodeIndex < nodes.length; nodeIndex++) {
+        var node = nodes[nodeIndex];
+        var nodeDim = this.getNodeCalculatedDimensions(node, minVertexDuration, maxVertexDuration);
+        node.drawWidth = nodeDim.drawWidth;
+        node.drawHeight = nodeDim.drawHeight;
+        node.scale = nodeDim.scale;
+        node.width = nodeDim.width;
+        node.height = nodeDim.height;
+        if (maxNodeHeight < node.height) {
+          maxNodeHeight = node.height;
+        }
+        if (depth == 0) {
+          // Top nodes - position uniformly
+          updateNodeEffectiveWidth(node, xGap + node.width);
+        }
+        if (node.children && node.children.length > 0) {
+          // There can be dedicated or shared children.
+          // Dedicated children increase effective width of parent by their
+          // width.
+          // Shared children increase effective width of parent only by the
+          // fraction of parentage
+          var childrenWidth = 0;
+          node.children.forEach(function (child) {
+            var childDim = self.getNodeCalculatedDimensions(child, minVertexDuration, maxVertexDuration);
+            childrenWidth += ((childDim.width + xGap) / child.parents.length);
+          });
+          updateNodeEffectiveWidth(node, Math.max(childrenWidth, (node.width + xGap)));
+        } else {
+          updateNodeEffectiveWidth(node, xGap + node.width);
+        }
+        node.y = currentY;
+        node.incomingY = node.y;
+        node.outgoingY = node.incomingY + node.height;
+      }
+      currentY += maxNodeHeight;
+      currentY += yGap;
+    }
+    // Second pass - determine actual X coordinates
+    var maxX = 0;
+    for (var depth = 0; depth < depthToNodes.length; depth++) {
+      var nodes = depthToNodes[depth];
+      var currentX = -1;
+      var parentCurrentXMap = {};
+      for (var nodeIndex = 0; nodeIndex < nodes.length; nodeIndex++) {
+        var node = nodes[nodeIndex];
+        var parentsKey = null;
+        if (node.parents != null && node.parents.length > 0) {
+          var parentMidX = 0;
+          var parentsKey = '';
+          var childrenEffectiveWidth = -1;
+          node.parents.forEach(function (parent) {
+            parentMidX += (parent.x + (parent.width / 2));
+            parentsKey += (parent.id + '//');
+            if (childrenEffectiveWidth < 0) {
+              parent.children.forEach(function (c) {
+                childrenEffectiveWidth += (c.effectiveWidth);
+              });
+            }
+          });
+          parentMidX = parentMidX / node.parents.length;
+          var parentCurrentX = parentCurrentXMap[parentsKey];
+          if (parentCurrentX == null || parentCurrentX == undefined) {
+            parentCurrentX = parentMidX - (childrenEffectiveWidth / 2);
+            parentCurrentXMap[parentsKey] = parentCurrentX;
+          }
+          currentX = parentCurrentX;
+        } else {
+          if (currentX < 0) {
+            currentX = 0;
+          }
+        }
+        node.x = (currentX + (node.effectiveWidth - node.width) / 2);
+        node.outgoingX = (node.x + node.width / 2);
+        node.incomingX = node.outgoingX;
+        dagVisualModel.nodes.push(node);
+        if (parentsKey != null) {
+          parentCurrentXMap[parentsKey] = currentX + node.effectiveWidth;
+        } else {
+          currentX += node.effectiveWidth;
+        }
+        if ((node.x + node.width) > maxX) {
+          maxX = node.x + node.width;
+        }
+      }
+    }
+    var canvasHeight = currentY;
+    var canvasWidth = maxX + (xGap << 1);
+
+    //
+    // Draw SVG
+    //
+    var force = d3.layout.force().nodes(dagVisualModel.nodes).links(dagVisualModel.links).start();
+    var nodeDragData = {
+      nodeRelativeX: 0,
+      nodeRelativeY: 0
+    };
+    var nodeDrag = d3.behavior.drag().on('dragstart',function (node) {
+      d3.event.sourceEvent.stopPropagation();
+      var rc = d3.mouse(this);
+      nodeDragData.nodeRelativeX = (rc[0] * node.scale);
+      nodeDragData.nodeRelativeY = (rc[1] * node.scale);
+    }).on('drag',function (node) {
+        var nx = d3.event.x - nodeDragData.nodeRelativeX;
+        var ny = d3.event.y - nodeDragData.nodeRelativeY;
+        self.dragVertex(d3.select(this), node, [nx, ny], diagonal);
+      }).on('dragend', function () {
+        nodeDragData.nodeRelativeX = 0;
+        nodeDragData.nodeRelativeY = 0;
+      });
+    // Create Links
+    var diagonal = d3.svg.diagonal().source(function (d) {
+      return {
+        x: d.source.outgoingX,
+        y: d.source.outgoingY
+      };
+    }).target(function (d) {
+        return {
+          x: d.target.incomingX,
+          y: d.target.incomingY - 12
+        }
+      });
+    var link = svgLayer.selectAll(".link-g").data(dagVisualModel.links).enter().append("g").attr("class", "link-g").attr("marker-end", "url(#arrow)");
+    link.append("path").attr("class",function (l) {
+      var classes = "link svg-tooltip ";
+      if (l.edgeType) {
+        classes += ("type-" + l.edgeType.toLowerCase() + " ");
+      } else {
+        classes += "type-unknown ";
+      }
+      return classes;
+    }).attr("d", diagonal).attr("title", function (l) {
+        var lower = l.edgeType ? l.edgeType.toLowerCase() : '';
+        return Em.I18n.t("jobs.hive.tez.edge." + lower);
+      });
+    // Create Nodes
+    var node = svgLayer.selectAll(".node").data(dagVisualModel.nodes).enter().append("g").attr("class", "node").call(nodeDrag);
+    node.append("rect").attr("class", "background").attr("width",function (n) {
+      return n.drawWidth;
+    }).attr("height",function (n) {
+        return n.drawHeight;
+      }).attr("rx", "10").attr("filter", "url(#shadow)").on('mousedown', function (n) {
+        //var vertex = App.TezDagVertex.find(n.id);
+        var vertex = App.HiveJob.store.getById('tezDagVertex', n.id);
+        if (vertex != null) {
+          self.get('parentView').doSelectVertex(vertex, true);
+        }
+      });
+    node.each(function (n, nodeIndex) {
+      var ops = n.operations;
+      var opCount = {};
+      if (ops != null && ops.length > 0) {
+        var opGroups = d3.select(this).selectAll(".operation").data(ops).enter().append("g").attr("class", "operation").attr("transform",function (op, opIndex) {
+          var row = Math.floor(opIndex / 3);
+          var column = opIndex % 3;
+          return "translate(" + (10 + column * 55) + "," + (37 + row * 20) + ")";
+        }).attr("clip-path", "url(#operatorClipPath)").attr("opIndex",function (op) {
+            if (!opCount[op]) {
+              opCount[op] = 1;
+            }
+            else {
+              opCount[op] = opCount[op] + 1;
+            }
+            return opCount[op];
+          }).on('mouseover', function (op) {
+            var viewContent = {
+              operationName: op,
+              operatorPlanObj: []
+            };
+            /*viewContent.operatorPlanObj = self.createOperationPlanObj(n.name, op, this.getAttribute('opIndex'));
+            var template = App.HoverOpTable.create({content: viewContent});
+            $(this).find('.svg-tooltip').attr('title', template.renderToBuffer().string()).tooltip('fixTitle').tooltip('show');*/
+          });
+
+        opGroups.append("rect").attr("class", "operation svg-tooltip ").attr("width", "50").attr("height", "16");
+        opGroups.append("text").attr("x", "2").attr("dy", "1em").text(function (op) {
+          return op != null ? op.split(' ')[0] : '';
+        });
+      }
+    });
+    var metricNodes = node.append("g").attr("class", "metric").attr("transform", "translate(112,7)");
+    metricNodes.append("rect").attr("width",function (n) {
+      if (n.type == App.TezDagVertexType.UNION) {
+        return 0;
+      }
+      return 60;
+    }).attr("height",function (n) {
+        if (n.type == App.TezDagVertexType.UNION) {
+          return 0;
+        }
+        return 18;
+      }).attr("rx", "3").attr("class", "metric-title svg-tooltip");
+    metricNodes.append("text").attr("class", "metric-text").attr("x", "2").attr("dy", "1em");
+    node.append("text").attr("x", "1.9em").attr("dy", "1.5em").text(function (d) {
+      return d.name;
+    });
+    var iconContainer = node.append('g').attr('class', 'vertex-icon-container').attr('transform', 'translate(10,10)');
+    iconContainer.append('rect').attr('width', '1em').attr('height', '1em').attr('class', 'vertex-icon-rect  svg-tooltip ');
+    iconContainer.append('text').attr('dy', '10px').attr("font-family", "FontAwesome").attr('class', 'vertex-icon-text');
+    node.attr("transform", function (d) {
+      return "translate(" + d.x + "," + d.y + ") scale(" + d.scale + ") ";
+    });
+    this.vertexMetricsUpdated();
+    $('.svg-tooltip').each(function () {
+      var item = $(this);
+      if (item.prop('tagName') == 'path') {
+        item.hover(function (e) {
+          var offset = $(this).offset();
+          item.prop('offsetWidth', function () {
+            return 2 * (e.pageX - offset.left);
+          });
+          item.prop('offsetHeight', function () {
+            return 2 * (e.pageY - offset.top);
+          });
+        });
+      }
+      if (item.prop('offsetWidth') == undefined) {
+        item.prop('offsetWidth', function () {
+          return item.width();
+        });
+      }
+      if (item.prop('offsetHeight') == undefined) {
+        item.prop('offsetHeight', function () {
+          return item.height();
+        });
+      }
+    });
+    App.tooltip($('.svg-tooltip'), {
+      placement: 'left',
+      template: '<div class="tooltip jobs-tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
+    });
+
+    /*if (App.supports.debugJobsDag) {
+      // Draws node bounding box - for debug purposes
+      node.append("rect").attr("width",function (n) {
+        return n.effectiveWidth;
+      }).attr("height",function (n) {
+          return n.height;
+        }).attr("x",function (n) {
+          return -1 * ((n.effectiveWidth - n.width) / 2);
+        }).attr("y",function (n) {
+          return 0;
+        }).attr("style", "opacity: 0.2;fill:yellow;");
+    }*/
+
+    // Position in center
+    var translateX = Math.round((width - canvasWidth) / 2);
+    if (translateX > 0) {
+      svgLayer.attr("transform", "translate(" + translateX + ",0)");
+    }
+    return {
+      width: canvasWidth,
+      height: canvasHeight
+    }
+  },
+
+  dragVertex: function (d3Vertex, node, newPosition, diagonal) {
+    // Move vertex
+    node.x = newPosition[0];
+    node.y = newPosition[1];
+    node.incomingX = newPosition[0] + (node.width / 2);
+    node.incomingY = newPosition[1];
+    node.outgoingX = newPosition[0] + (node.width / 2);
+    node.outgoingY = newPosition[1] + node.height;
+    d3Vertex.attr('transform', 'translate(' + newPosition[0] + ',' + newPosition[1] + ') scale(' + node.scale + ') ');
+    // Move links
+    d3.selectAll('.link').filter(function (l) {
+      if (l && (l.source === node || l.target === node)) {
+        return this
+      }
+      return null;
+    }).attr('d', diagonal);
+  },
+
+  /**
+   * Refreshes UI of the Tez graph with latest values
+   */
+  refreshGraphUI: function () {
+    var svgLayer = this.get('svgVerticesLayer');
+    if (svgLayer != null) {
+      var self = this;
+      var metricNodes = svgLayer.selectAll(".metric");
+      var metricNodeTexts = svgLayer.selectAll(".metric-text");
+      var metricNodeTitles = svgLayer.selectAll(".metric-title");
+      var nodeBackgrounds = svgLayer.selectAll(".background");
+      var vertexIconTexts = svgLayer.selectAll(".vertex-icon-text");
+      var vertexIconRects = svgLayer.selectAll(".vertex-icon-rect");
+      metricNodes.attr("class", function (node) {
+        var classes = "metric ";
+        var percent = node.metricPercent;
+        if (App.Helpers.number.validateInteger(percent) == null && percent >= 0) {
+          if (percent <= 20) {
+            classes += "heat-0-20 ";
+          } else if (percent <= 40) {
+            classes += "heat-20-40 ";
+          } else if (percent <= 60) {
+            classes += "heat-40-60 ";
+          } else if (percent <= 80) {
+            classes += "heat-60-80 ";
+          } else if (percent <= 100) {
+            classes += "heat-80-100 ";
+          } else {
+            classes += "heat-none";
+          }
+        } else {
+          classes += "heat-none";
+        }
+        return classes;
+      });
+      metricNodeTexts.text(function (node) {
+        if (node.type == App.TezDagVertexType.UNION) {
+          return '';
+        }
+        return node.metricDisplay;
+      });
+      metricNodeTitles.attr("title",function (node) {
+        return node.metricType;
+      }).attr("data-original-title", function (node) {
+          return node.metricType;
+        });
+      nodeBackgrounds.attr("class", function (n) {
+        var classes = "background ";
+        if (n.type) {
+          classes += (n.type.toLowerCase() + " ");
+        } else {
+          classes += "unknown-vertex-type ";
+        }
+        if (n.selected) {
+          classes += "selected ";
+        }
+        return classes;
+      });
+      vertexIconRects.attr('title',function (node) {
+        return App.Helpers.string.getCamelCase(node.state);
+      }).attr('data-original-title', function (node) {
+          return App.Helpers.string.getCamelCase(node.state);
+        });
+      vertexIconTexts.text(function (n) {
+        return self.getVertexIcon(n)
+      }).attr('class', function (n) {
+          var classes = 'vertex-icon-text ';
+          if (n.state != null) {
+            if (n.state == App.TezDagVertexState.JOBFAILED) {
+              classes += App.TezDagVertexState.FAILED.toLowerCase();
+            }
+            else {
+              classes += n.state.toLowerCase();
+            }
+          }
+          return classes;
+        });
+    }
+  },
+
+  getVertexIcon: function (node) {
+    var icon = "";
+    switch (node.state) {
+      case App.TezDagVertexState.NEW:
+        icon = '\uF10C'; //icon-circle-blank
+      case App.TezDagVertexState.RUNNING:
+      case App.TezDagVertexState.FAILED:
+        icon = '\uF111'; //icon-circle
+        break;
+      case App.TezDagVertexState.SUCCEEDED:
+        icon = '\uF00C'; //icon-ok
+        break;
+      case App.TezDagVertexState.KILLED:
+      case App.TezDagVertexState.ERROR:
+        icon = '\uF057'; //icon-remove-sign
+        break;
+      case App.TezDagVertexState.INITED:
+      case App.TezDagVertexState.INITIALIZING:
+      case App.TezDagVertexState.TERMINATING:
+        icon = '\uF141'; //icon-ellipsis-horizontal
+        break;
+      case App.TezDagVertexState.JOBFAILED:
+        icon = '\uF05C'; //icon-remove-circle
+        break;
+    }
+    return icon;
+  },
+
+  /**
+   * Determines the size of a node by taking into account its duration and
+   * number of operations performed.
+   *
+   * @return {Object} Provides various metrics necessary in drawing a node.
+   * <code>
+   * {
+   *  width: 360, // Scaled width of the node
+   *  height: 80, // Scaled height of the node
+   *  scale: 2, // Scale used on vertex dimensions. Quickest vertex is scaled to 1 and slowest vertex is scaled to 10.
+   *  drawWidth: 180, // Width of actual drawing (that will be scaled)
+   *  drawHeight: 40 // Height of actual drawing (that will be scaled)
+   * }
+   * </code>
+   */
+  getNodeCalculatedDimensions: function (node, minVertexDuration, maxVertexDuration) {
+    var size = {
+      width: 180,
+      height: 40,
+      drawWidth: 180,
+      drawHeight: 40,
+      scale: 1
+    };
+    if (node.operations && node.operations.length > 0) {
+      var opsHeight = Math.ceil(node.operations.length / 3);
+      size.drawHeight += (opsHeight * 20);
+    }
+    size.width = size.drawWidth * size.scale;
+    size.height = size.drawHeight * size.scale;
+    return size;
+  }
+
+});
+
+App.HoverOpTable = Em.View.extend({
+  templateName: 'job/hover_op_table'
+});