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/18 16:21:31 UTC

[2/3] git commit: AMBARI-6539. Create main page with table of jobs. (onechiporenko)

AMBARI-6539. Create main page with table of jobs. (onechiporenko)


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

Branch: refs/heads/trunk
Commit: 5659f0a23b2cb8346d496246d0cce635aeefa87d
Parents: 7bdb1e4
Author: Oleg Nechiporenko <on...@apache.org>
Authored: Fri Jul 18 17:17:03 2014 +0300
Committer: Oleg Nechiporenko <on...@apache.org>
Committed: Fri Jul 18 17:17:03 2014 +0300

----------------------------------------------------------------------
 .../jobs/src/main/resources/ui/Gruntfile.js     |   2 +-
 .../ui/app/img/glyphicons-halflings.png         | Bin 0 -> 13826 bytes
 .../jobs/src/main/resources/ui/app/index.html   |   7 +-
 .../src/main/resources/ui/app/scripts/app.js    |  15 +-
 .../ui/app/scripts/assets/hive-queries.json     | 156 ++++++
 .../app/scripts/controllers/job_controller.js   |  19 +
 .../app/scripts/controllers/jobs_controller.js  | 489 +++++++++++++++++++
 .../resources/ui/app/scripts/helpers/ajax.js    |  16 +-
 .../resources/ui/app/scripts/helpers/misc.js    |  28 +-
 .../scripts/mappers/jobs/hive_jobs_mapper.js    |  92 ++--
 .../ui/app/scripts/mixins/run_periodically.js   |  78 +++
 .../ui/app/scripts/models/jobs/hive_job.js      |   2 -
 .../resources/ui/app/scripts/translations.js    |  81 +++
 .../ui/app/scripts/views/filter_view.js         | 488 ++++++++++++++++++
 .../resources/ui/app/scripts/views/job_view.js  |  19 +
 .../resources/ui/app/scripts/views/jobs_view.js | 305 ++++++++++++
 .../resources/ui/app/scripts/views/sort_view.js | 253 ++++++++++
 .../ui/app/scripts/views/table_view.js          | 362 ++++++++++++++
 .../src/main/resources/ui/app/styles/main.less  | 301 +++++++++++-
 .../main/resources/ui/app/templates/jobs.hbs    |  90 ++++
 .../ui/app/templates/jobs/jobs_name.hbs         |  19 +
 .../ui/app/templates/sort_field_template.hbs    |  19 +
 .../table/navigation/pagination_first.hbs       |  19 +
 .../table/navigation/pagination_last.hbs        |  19 +
 .../table/navigation/pagination_left.hbs        |  19 +
 .../table/navigation/pagination_right.hbs       |  19 +
 .../ui/app/templates/wrapper_layout.hbs         |  19 +
 .../ui/app/templates/wrapper_template.hbs       |  25 +
 .../views/jobs/src/main/resources/ui/bower.json |   4 +-
 29 files changed, 2910 insertions(+), 55 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/5659f0a2/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 b5ce07b..7dc777d 100644
--- a/contrib/views/jobs/src/main/resources/ui/Gruntfile.js
+++ b/contrib/views/jobs/src/main/resources/ui/Gruntfile.js
@@ -257,7 +257,7 @@ module.exports = function (grunt) {
             src: [
               '*.{ico,txt}',
               '.htaccess',
-              'images/{,*/}*.{webp,gif}',
+              'img/*',
               'styles/fonts/*',
               'scripts/assets/**/*'
             ]

http://git-wip-us.apache.org/repos/asf/ambari/blob/5659f0a2/contrib/views/jobs/src/main/resources/ui/app/img/glyphicons-halflings.png
----------------------------------------------------------------------
diff --git a/contrib/views/jobs/src/main/resources/ui/app/img/glyphicons-halflings.png b/contrib/views/jobs/src/main/resources/ui/app/img/glyphicons-halflings.png
new file mode 100644
index 0000000..79bc568
Binary files /dev/null and b/contrib/views/jobs/src/main/resources/ui/app/img/glyphicons-halflings.png differ

http://git-wip-us.apache.org/repos/asf/ambari/blob/5659f0a2/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 850126e..ea74dcf 100644
--- a/contrib/views/jobs/src/main/resources/ui/app/index.html
+++ b/contrib/views/jobs/src/main/resources/ui/app/index.html
@@ -27,11 +27,14 @@
   </head>
   <body>
     <!-- build:js(app) scripts/components.js -->
-    <script src="bower_components/jquery/dist/jquery.js"></script>
-    <script src="bower_components/handlebars/handlebars.runtime.js"></script>
+    <script src="bower_components/jquery/jquery.js"></script>
+    <script src="bower_components/bootstrap/js/bootstrap-tooltip.js"></script>
+    <script src="bower_components/moment/moment.js"></script>
+    <script src="bower_components/handlebars/handlebars.js"></script>
     <script src="@@ember"></script>
     <script src="@@ember_data"></script>
     <script src="bower_components/ember-json-mapper/ember-json-mapper.js"></script>
+    <script src="bower_components/ember-i18n/lib/i18n.js"></script>
     <!-- endbuild -->
 
     <!-- build:js(.tmp) scripts/templates.js -->

http://git-wip-us.apache.org/repos/asf/ambari/blob/5659f0a2/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 5622f4a..c5d68f2 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
@@ -27,11 +27,19 @@ App.initializer({
   initialize: function(container, application) {
 
     application.reopen({
+
       /**
        * Test mode is automatically enabled if running on localhost
        * @type {bool}
        */
-      testMode: (location.hostname == 'localhost')
+      testMode: (location.hostname == 'localhost'),
+
+      /**
+       * Prefix for API-requests
+       * @type {string}
+       */
+      urlPrefix: '/api/v1'
+
     });
 
   }
@@ -39,8 +47,10 @@ 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');
@@ -48,4 +58,7 @@ require('scripts/mappers/**/*');
 require('scripts/controllers/*');
 require('scripts/routes/*');
 require('scripts/components/*');
+require('scripts/views/sort_view');
+require('scripts/views/filter_view');
+require('scripts/views/table_view');
 require('scripts/views/*');

http://git-wip-us.apache.org/repos/asf/ambari/blob/5659f0a2/contrib/views/jobs/src/main/resources/ui/app/scripts/assets/hive-queries.json
----------------------------------------------------------------------
diff --git a/contrib/views/jobs/src/main/resources/ui/app/scripts/assets/hive-queries.json b/contrib/views/jobs/src/main/resources/ui/app/scripts/assets/hive-queries.json
index b601670..8bd8f58 100644
--- a/contrib/views/jobs/src/main/resources/ui/app/scripts/assets/hive-queries.json
+++ b/contrib/views/jobs/src/main/resources/ui/app/scripts/assets/hive-queries.json
@@ -227,6 +227,162 @@
       },
       "entity": "root_20140221171313_c9710dd6-0d1c-4d9c-9dff-031edbd20b66",
       "entitytype": "HIVE_QUERY_ID"
+    },
+    {
+      "starttime": 1393443850756,
+      "events": [
+        {
+          "timestamp": 1393443850756,
+          "eventtype": "QUERY_COMPLETED",
+          "eventinfo": {}
+        },
+        {
+          "timestamp": 1393443850756,
+          "eventtype": "QUERY_SUBMITTED",
+          "eventinfo": {}
+        }
+      ],
+      "otherinfo": {
+        "status": false,
+        "query": "{}"
+      },
+      "primaryfilters": {
+        "user": [
+          "hive"
+        ]
+      },
+      "entity": "hive_20188952544444_6301b51e-d52c-4618-995f-573e3f59006c",
+      "entitytype": "HIVE_QUERY_ID"
+    },
+    {
+      "starttime": 1393443850756,
+      "events": [
+        {
+          "timestamp": 1393443850756,
+          "eventtype": "QUERY_COMPLETED",
+          "eventinfo": {}
+        },
+        {
+          "timestamp": 1393443850756,
+          "eventtype": "QUERY_SUBMITTED",
+          "eventinfo": {}
+        }
+      ],
+      "otherinfo": {
+        "status": false,
+        "query": "{}"
+      },
+      "primaryfilters": {
+        "user": [
+          "hive"
+        ]
+      },
+      "entity": "hive_20196139444444_6301b51e-d52c-4618-995f-573e3f59006c",
+      "entitytype": "HIVE_QUERY_ID"
+    },
+    {
+      "starttime": 1393443850756,
+      "events": [
+        {
+          "timestamp": 1393443850756,
+          "eventtype": "QUERY_COMPLETED",
+          "eventinfo": {}
+        },
+        {
+          "timestamp": 1393443850756,
+          "eventtype": "QUERY_SUBMITTED",
+          "eventinfo": {}
+        }
+      ],
+      "otherinfo": {
+        "status": false,
+        "query": "{}"
+      },
+      "primaryfilters": {
+        "user": [
+          "hive"
+        ]
+      },
+      "entity": "hive_20127273144444_6301b51e-d52c-4618-995f-573e3f59006c",
+      "entitytype": "HIVE_QUERY_ID"
+    },
+    {
+      "starttime": 1393443850756,
+      "events": [
+        {
+          "timestamp": 1393443850756,
+          "eventtype": "QUERY_COMPLETED",
+          "eventinfo": {}
+        },
+        {
+          "timestamp": 1393443850756,
+          "eventtype": "QUERY_SUBMITTED",
+          "eventinfo": {}
+        }
+      ],
+      "otherinfo": {
+        "status": false,
+        "query": "{}"
+      },
+      "primaryfilters": {
+        "user": [
+          "hive"
+        ]
+      },
+      "entity": "hive_20113100844444_6301b51e-d52c-4618-995f-573e3f59006c",
+      "entitytype": "HIVE_QUERY_ID"
+    },
+    {
+      "starttime": 1393443850756,
+      "events": [
+        {
+          "timestamp": 1393443850756,
+          "eventtype": "QUERY_COMPLETED",
+          "eventinfo": {}
+        },
+        {
+          "timestamp": 1393443850756,
+          "eventtype": "QUERY_SUBMITTED",
+          "eventinfo": {}
+        }
+      ],
+      "otherinfo": {
+        "status": false,
+        "query": "{}"
+      },
+      "primaryfilters": {
+        "user": [
+          "hive"
+        ]
+      },
+      "entity": "hive_20167400444444_6301b51e-d52c-4618-995f-573e3f59006c",
+      "entitytype": "HIVE_QUERY_ID"
+    },
+    {
+      "starttime": 1393443850756,
+      "events": [
+        {
+          "timestamp": 1393443850756,
+          "eventtype": "QUERY_COMPLETED",
+          "eventinfo": {}
+        },
+        {
+          "timestamp": 1393443850756,
+          "eventtype": "QUERY_SUBMITTED",
+          "eventinfo": {}
+        }
+      ],
+      "otherinfo": {
+        "status": false,
+        "query": "{}"
+      },
+      "primaryfilters": {
+        "user": [
+          "hive"
+        ]
+      },
+      "entity": "hive_20110915544444_6301b51e-d52c-4618-995f-573e3f59006c",
+      "entitytype": "HIVE_QUERY_ID"
     }
   ]
 }

http://git-wip-us.apache.org/repos/asf/ambari/blob/5659f0a2/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
new file mode 100644
index 0000000..e300323
--- /dev/null
+++ b/contrib/views/jobs/src/main/resources/ui/app/scripts/controllers/job_controller.js
@@ -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.
+ */
+
+App.JobController = Ember.Controller.extend({});

http://git-wip-us.apache.org/repos/asf/ambari/blob/5659f0a2/contrib/views/jobs/src/main/resources/ui/app/scripts/controllers/jobs_controller.js
----------------------------------------------------------------------
diff --git a/contrib/views/jobs/src/main/resources/ui/app/scripts/controllers/jobs_controller.js b/contrib/views/jobs/src/main/resources/ui/app/scripts/controllers/jobs_controller.js
new file mode 100644
index 0000000..c2b6560
--- /dev/null
+++ b/contrib/views/jobs/src/main/resources/ui/app/scripts/controllers/jobs_controller.js
@@ -0,0 +1,489 @@
+/**
+ * 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.JobsController = Ember.ArrayController.extend(App.RunPeriodically, {
+
+  name:'mainJobsController',
+
+  /**
+   * Sorted ArrayProxy
+   */
+  sortedContent: [],
+
+  contentAndSortObserver : function() {
+    Ember.run.once(this, 'contentAndSortUpdater');
+  }.observes('content.length', 'content.@each.id', 'content.@each.startTime', 'content.@each.endTime', 'sortProperties', 'sortAscending'),
+
+  contentAndSortUpdater: function() {
+    this.set('sortingDone', false);
+    var content = this.get('content');
+    var sortedContent = content.toArray();
+    var sortProperty = this.get('sortProperty');
+    var sortAscending = this.get('sortAscending');
+    sortedContent.sort(function(r1, r2) {
+      var r1id = r1.get(sortProperty);
+      var r2id = r2.get(sortProperty);
+      if (r1id < r2id)
+        return sortAscending ? -1 : 1;
+      if (r1id > r2id)
+        return sortAscending ? 1 : -1;
+      return 0;
+    });
+    var sortedArray = this.get('sortedContent');
+    var count = 0;
+    sortedContent.forEach(function(sortedJob){
+      if(sortedArray.length <= count) {
+        sortedArray.pushObject(Ember.Object.create());
+      }
+      sortedArray[count].set('failed', sortedJob.get('failed'));
+      sortedArray[count].set('hasTezDag', sortedJob.get('hasTezDag'));
+      sortedArray[count].set('queryText', sortedJob.get('queryText'));
+      sortedArray[count].set('name', sortedJob.get('name'));
+      sortedArray[count].set('user', sortedJob.get('user'));
+      sortedArray[count].set('id', sortedJob.get('id'));
+      sortedArray[count].set('startTimeDisplay', sortedJob.get('startTimeDisplay'));
+      sortedArray[count].set('endTimeDisplay', sortedJob.get('endTimeDisplay'));
+      sortedArray[count].set('durationDisplay', sortedJob.get('durationDisplay'));
+      count ++;
+    });
+    if(sortedArray.length > count) {
+      for(var c = sortedArray.length-1; c >= count; c--){
+        sortedArray.removeObject(sortedArray[c]);
+      }
+    }
+    sortedContent.length = 0;
+    this.set('sortingDone', true);
+  },
+
+  navIDs: {
+    backIDs: [],
+    nextID: ''
+  },
+
+  lastJobID: '',
+
+  hasNewJobs: false,
+
+  loaded : false,
+
+  loading : false,
+
+  resetPagination: false,
+
+  loadJobsTimeout: null,
+
+  loadTimeout: null,
+
+  jobsUpdateInterval: 6000,
+
+  jobsUpdate: null,
+
+  sortingColumn: null,
+
+  sortProperty: 'id',
+
+  sortAscending: true,
+
+  sortingDone: true,
+
+  jobsMessage: Em.I18n.t('jobs.loadingTasks'),
+
+  sortingColumnObserver: function () {
+    if(this.get('sortingColumn')){
+      this.set('sortProperty', this.get('sortingColumn').get('name'));
+      this.set('sortAscending', this.get('sortingColumn').get('status') !== "sorting_desc");
+    }
+  }.observes('sortingColumn.name','sortingColumn.status'),
+
+  updateJobsByClick: function () {
+    this.set('navIDs.backIDs', []);
+    this.set('navIDs.nextID', '');
+    this.get('filterObject').set('nextFromId', '');
+    this.get('filterObject').set('backFromId', '');
+    this.get('filterObject').set('fromTs', '');
+    this.set('hasNewJobs', false);
+    this.set('resetPagination', true);
+    this.loadJobs();
+  },
+
+  updateJobs: function (controllerName, funcName) {
+    clearInterval(this.get('jobsUpdate'));
+    var self = this;
+    var interval = setInterval(function () {
+      App.router.get(controllerName)[funcName]();
+    }, this.jobsUpdateInterval);
+    this.set('jobsUpdate', interval);
+  },
+
+  totalOfJobs: 0,
+
+  setTotalOfJobs: function () {
+    if(this.get('totalOfJobs') < this.get('content.length')){
+      this.set('totalOfJobs', this.get('content.length'));
+    }
+  }.observes('content.length'),
+
+  filterObject: Ember.Object.create({
+    id: "",
+    isIdFilterApplied: false,
+    jobsLimit: '10',
+    user: "",
+    windowStart: "",
+    windowEnd: "",
+    nextFromId: "",
+    backFromId: "",
+    fromTs: "",
+    isAnyFilterApplied: false,
+
+    onApplyIdFilter: function () {
+      this.set('isIdFilterApplied', this.get('id') != "");
+    }.observes('id'),
+
+    /**
+     * Direct binding to startTime filter field
+     */
+    startTime: "",
+
+    onStartTimeChange:function(){
+      var time = "";
+      var curTime = new Date().getTime();
+      switch (this.get('startTime')) {
+        case 'Past 1 hour':
+          time = curTime - 3600000;
+          break;
+        case 'Past 1 Day':
+          time = curTime - 86400000;
+          break;
+        case 'Past 2 Days':
+          time = curTime - 172800000;
+          break;
+        case 'Past 7 Days':
+          time = curTime - 604800000;
+          break;
+        case 'Past 14 Days':
+          time = curTime - 1209600000;
+          break;
+        case 'Past 30 Days':
+          time = curTime - 2592000000;
+          break;
+        case 'Custom':
+          this.showCustomDatePopup();
+          break;
+        case 'Any':
+          time = "";
+          break;
+      }
+      if(this.get('startTime') != "Custom"){
+        this.set("windowStart", time);
+        this.set("windowEnd", "");
+      }
+    }.observes("startTime"),
+
+    // Fields values from Select Custom Dates form
+    customDateFormFields: Ember.Object.create({
+      startDate: null,
+      hoursForStart: null,
+      minutesForStart: null,
+      middayPeriodForStart: null,
+      endDate: null,
+      hoursForEnd: null,
+      minutesForEnd: null,
+      middayPeriodForEnd: null
+    }),
+
+    errors: Ember.Object.create({
+      isStartDateError: false,
+      isEndDateError: false
+    }),
+
+    errorMessages: Ember.Object.create({
+      startDate: '',
+      endDate: ''
+    }),
+
+    showCustomDatePopup: function () {
+      var self = this,
+        windowEnd = "",
+        windowStart = "";
+      /*App.ModalPopup.show({
+        header: Em.I18n.t('jobs.table.custom.date.header'),
+        onPrimary: function () {
+          self.validate();
+          if(self.get('errors.isStartDateError') || self.get('errors.isEndDateError')){
+            return;
+          }
+
+          var windowStart = self.createCustomStartDate();
+          var windowEnd = self.createCustomEndDate();
+
+          self.set("windowStart", windowStart.getTime());
+          self.set("windowEnd", windowEnd.getTime());
+          this.hide();
+        },
+        onSecondary: function () {
+          self.set('startTime','Any');
+          this.hide();
+        },
+        bodyClass: App.JobsCustomDatesSelectView.extend({
+          controller: self
+        })
+      });*/
+    },
+
+    createCustomStartDate : function () {
+      var startDate = this.get('customDateFormFields.startDate'),
+        hoursForStart = this.get('customDateFormFields.hoursForStart'),
+        minutesForStart = this.get('customDateFormFields.minutesForStart'),
+        middayPeriodForStart = this.get('customDateFormFields.middayPeriodForStart');
+      if (startDate && hoursForStart && minutesForStart && middayPeriodForStart) {
+        return new Date(startDate + ' ' + hoursForStart + ':' + minutesForStart + ' ' + middayPeriodForStart);
+      }
+      return null;
+    },
+
+    createCustomEndDate : function () {
+      var endDate = this.get('customDateFormFields.endDate'),
+        hoursForEnd = this.get('customDateFormFields.hoursForEnd'),
+        minutesForEnd = this.get('customDateFormFields.minutesForEnd'),
+        middayPeriodForEnd = this.get('customDateFormFields.middayPeriodForEnd');
+      if (endDate && hoursForEnd && minutesForEnd && middayPeriodForEnd) {
+        return new Date(endDate + ' ' + hoursForEnd + ':' + minutesForEnd + ' ' + middayPeriodForEnd);
+      }
+      return null;
+    },
+
+    clearErrors: function () {
+      var errorMessages = this.get('errorMessages');
+      Em.keys(errorMessages).forEach(function (key) {
+        errorMessages.set(key, '');
+      }, this);
+      var errors = this.get('errors');
+      Em.keys(errors).forEach(function (key) {
+        errors.set(key, false);
+      }, this);
+    },
+
+    // Validation for every field in customDateFormFields
+    validate: function () {
+      var formFields = this.get('customDateFormFields'),
+        errors = this.get('errors'),
+        errorMessages = this.get('errorMessages');
+      this.clearErrors();
+      // Check if feild is empty
+      Em.keys(errorMessages).forEach(function (key) {
+        if (!formFields.get(key)) {
+          errors.set('is' + key.capitalize() + 'Error', true);
+          errorMessages.set(key, Em.I18n.t('jobs.customDateFilter.error.required'));
+        }
+      }, this);
+      // Check that endDate is after startDate
+      var startDate = this.createCustomStartDate(),
+        endDate = this.createCustomEndDate();
+      if (startDate && endDate && (startDate > endDate)) {
+        errors.set('isEndDateError', true);
+        errorMessages.set('endDate', Em.I18n.t('jobs.customDateFilter.error.date.order'));
+      }
+    },
+
+    /**
+     * Create link for server request
+     * @return {String}
+     */
+    createJobsFiltersLink: function() {
+      var link = "?fields=events,primaryfilters,otherinfo&secondaryFilter=tez:true",
+        numberOfAppliedFilters = 0;
+
+      if(this.get("id") !== "") {
+        link = "/" + this.get("id") + link;
+        numberOfAppliedFilters++;
+      }
+
+      link += "&limit=" + (parseInt(this.get("jobsLimit")) + 1);
+
+      if(this.get("user") !== ""){
+        link += "&primaryFilter=user:" + this.get("user");
+        numberOfAppliedFilters++;
+      }
+      if(this.get("backFromId") != ""){
+        link += "&fromId=" + this.get("backFromId");
+      }
+      if(this.get("nextFromId") != ""){
+        link += "&fromId=" + this.get("nextFromId");
+      }
+      if(this.get("fromTs") != ""){
+        link += "&fromTs=" + this.get("fromTs");
+      }
+      if(this.get("startTime") !== "" && this.get("startTime") !== "Any"){
+        link += this.get("windowStart") !== "" ? ("&windowStart=" + this.get("windowStart")) : "";
+        link += this.get("windowEnd") !== "" ? ("&windowEnd=" + this.get("windowEnd")) : "";
+        numberOfAppliedFilters++;
+      }
+
+      this.set('isAnyFilterApplied', numberOfAppliedFilters > 0);
+
+      return link;
+    }
+  }),
+
+  /*columnsName: Ember.ArrayController.create({
+    content: [
+      { name: Em.I18n.t('jobs.column.id'), index: 0 },
+      { name: Em.I18n.t('jobs.column.user'), index: 1 },
+      { name: Em.I18n.t('jobs.column.start.time'), index: 2 },
+      { name: Em.I18n.t('jobs.column.end.time'), index: 3 },
+      { name: Em.I18n.t('jobs.column.duration'), index: 4 }
+    ],
+    columnsCount: function () {
+      return this.get('content.length') + 1;
+    }.property('content.length')
+  }),*/
+
+  lastIDSuccessCallback: function(data) {
+    if(!data.entities[0]){
+      return;
+    }
+    var lastReceivedID = data.entities[0].entity;
+    if(this.get('lastJobID') == '') {
+      this.set('lastJobID', lastReceivedID);
+      if (this.get('loaded') && App.HiveJob.find().get('length') < 1) {
+        this.set('hasNewJobs', true);
+      }
+    }
+    else
+      if (this.get('lastJobID') !== lastReceivedID) {
+        this.set('lastJobID', lastReceivedID);
+        if(!App.HiveJob.find().findProperty('id', lastReceivedID)) {
+          this.set('hasNewJobs', true);
+        }
+      }
+  },
+
+  lastIDErrorCallback: function(data, jqXHR, textStatus) {
+    console.debug(jqXHR);
+  },
+
+  checkDataLoadingError: function (jqXHR){
+    /*var atsComponent = App.HostComponent.find().findProperty('componentName','APP_TIMELINE_SERVER');
+    if(atsComponent && atsComponent.get('workStatus') != "STARTED") {
+      this.set('jobsMessage', Em.I18n.t('jobs.error.ats.down'));
+    }else if (jqXHR && jqXHR.status == 400) {
+      this.set('jobsMessage', Em.I18n.t('jobs.error.400'));
+    }else if ((!jqXHR && this.get('loaded') && !this.get('loading')) || (jqXHR && jqXHR.status == 500)) {
+      this.set('jobsMessage', Em.I18n.t('jobs.nothingToShow'));
+    }else{
+      this.set('jobsMessage', Em.I18n.t('jobs.loadingTasks'));
+    }*/
+  },
+
+  init: function() {
+    this.set('interval', 6000);
+    this.loop('loadJobs');
+  },
+
+  loadJobs : function() {
+      //var yarnService = App.YARNService.find().objectAt(0),
+      //atsComponent = App.HostComponent.find().findProperty('componentName','APP_TIMELINE_SERVER'),
+      //atsInValidState = !!atsComponent && atsComponent.get('workStatus') === "STARTED",
+      //retryLoad = this.checkDataLoadingError();
+    //if (yarnService != null && atsInValidState) {
+    this.set('loading', true);
+    /*var historyServerHostName = yarnService.get('appTimelineServer.hostName'),
+      filtersLink = this.get('filterObject').createJobsFiltersLink(),
+      hiveQueriesUrl = App.get('testMode') ? "/scripts/assets/hive-queries.json" : "/proxy?url=http://" + historyServerHostName
+        + ":" + yarnService.get('ahsWebPort') + "/ws/v1/timeline/HIVE_QUERY_ID" + filtersLink;*/
+    /*App.ajax.send({
+      name: 'jobs.lastID',
+      sender: self,
+      data: {
+        historyServerHostName: '',//historyServerHostName,
+        ahsWebPort: ''//yarnService.get('ahsWebPort')
+      },
+      success: 'lastIDSuccessCallback',
+      error : 'lastIDErrorCallback'
+    });*/
+    App.ajax.send({
+      name: 'load_jobs',
+      sender: this,
+      data: {
+        historyServerHostName: '',
+        ahsWebPort: '',
+        filtersLink: this.get('filterObject').createJobsFiltersLink()
+      },
+      success: 'loadJobsSuccessCallback',
+      error : 'loadJobsErrorCallback'
+    });
+  },
+
+  loadJobsSuccessCallback: function(data) {
+    App.hiveJobsMapper.map(data);
+    this.set('loading', false);
+    if(this.get('loaded') == false || this.get('resetPagination') == true) {
+      this.initializePagination();
+      this.set('resetPagination', false);
+    }
+    this.set('loaded', true);
+  },
+
+  loadJobsErrorCallback: function(jqXHR) {
+    App.hiveJobsMapper.map({entities : []});
+    this.checkDataLoadingError(jqXHR);
+  },
+
+  initializePagination: function() {
+    var back_link_IDs = this.get('navIDs.backIDs.[]');
+    if(!back_link_IDs.contains(this.get('lastJobID'))) {
+      back_link_IDs.push(this.get('lastJobID'));
+    }
+    this.set('filterObject.backFromId', this.get('lastJobID'));
+    this.get('filterObject').set('fromTs', new Date().getTime());
+  },
+
+  navigateNext: function() {
+    this.set("filterObject.backFromId", '');
+    var back_link_IDs = this.get('navIDs.backIDs.[]');
+    var lastBackID = this.get('navIDs.nextID');
+    if(!back_link_IDs.contains(lastBackID)) {
+      back_link_IDs.push(lastBackID);
+    }
+    this.set('navIDs.backIDs.[]', back_link_IDs);
+    this.set("filterObject.nextFromId", this.get('navIDs.nextID'));
+    this.set('navIDs.nextID', '');
+    this.loadJobs();
+  },
+
+  navigateBack: function() {
+    this.set("filterObject.nextFromId", '');
+    var back_link_IDs = this.get('navIDs.backIDs.[]');
+    back_link_IDs.pop();
+    var lastBackID = back_link_IDs[back_link_IDs.length - 1];
+    this.set('navIDs.backIDs.[]', back_link_IDs);
+    this.set("filterObject.backFromId", lastBackID);
+    this.loadJobs();
+  },
+
+  refreshLoadedJobs : function() {
+    this.loadJobs();
+  }.observes(
+      'filterObject.id',
+      'filterObject.jobsLimit',
+      'filterObject.user',
+      'filterObject.windowStart',
+      'filterObject.windowEnd'
+    )
+
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/5659f0a2/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 42bae63..2c301ad 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
@@ -28,7 +28,21 @@
  *
  * @type {Object}
  */
-var urls = {};
+var urls = {
+
+  'load_jobs': {
+    real: '/proxy?url=http://{historyServerHostName}:{ahsWebPort}/ws/v1/timeline/HIVE_QUERY_ID{filtersLink}',
+    mock: '/scripts/assets/hive-queries.json',
+    apiPrefix: ''
+  },
+
+  'jobs_lastID': {
+    real: '/proxy?url=http://{historyServerHostName}:{ahsWebPort}/ws/v1/timeline/HIVE_QUERY_ID?limit=1&secondaryFilter=tez:true',
+    mock: '/scripts/assets/hive-queries.json',
+    apiPrefix: ''
+  }
+
+};
 /**
  * Replace data-placeholders to its values
  *

http://git-wip-us.apache.org/repos/asf/ambari/blob/5659f0a2/contrib/views/jobs/src/main/resources/ui/app/scripts/helpers/misc.js
----------------------------------------------------------------------
diff --git a/contrib/views/jobs/src/main/resources/ui/app/scripts/helpers/misc.js b/contrib/views/jobs/src/main/resources/ui/app/scripts/helpers/misc.js
index 25af752..f26d658 100644
--- a/contrib/views/jobs/src/main/resources/ui/app/scripts/helpers/misc.js
+++ b/contrib/views/jobs/src/main/resources/ui/app/scripts/helpers/misc.js
@@ -28,7 +28,7 @@ App.Helpers.misc = {
       } else {
         if (value < 1048576) {
           value = (value / 1024).toFixed(1) + 'KB';
-        } else  if (value >= 1048576 && value < 1073741824){
+        } else if (value >= 1048576 && value < 1073741824) {
           value = (value / 1048576).toFixed(1) + 'MB';
         } else {
           value = (value / 1073741824).toFixed(2) + 'GB';
@@ -36,6 +36,32 @@ App.Helpers.misc = {
       }
     }
     return value;
+  },
+
+  /**
+   * Convert ip address to integer
+   * @param ip
+   * @return integer
+   */
+  ipToInt: function (ip) {
+    // *     example 1: ipToInt('192.0.34.166');
+    // *     returns 1: 3221234342
+    // *     example 2: ipToInt('255.255.255.256');
+    // *     returns 2: false
+    // Verify IP format.
+    if (!/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(ip)) {
+      return false; // Invalid format.
+    }
+    // Reuse ip variable for component counter.
+    var d = ip.split('.');
+    return ((((((+d[0]) * 256) + (+d[1])) * 256) + (+d[2])) * 256) + (+d[3]);
   }
 
 };
+
+App.tooltip = function (self, options) {
+  self.tooltip(options);
+  self.on("remove DOMNodeRemoved", function () {
+    $(this).trigger('mouseleave');
+  });
+};
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ambari/blob/5659f0a2/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 9a1fc07..49b373c 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
@@ -17,12 +17,46 @@
 
 App.hiveJobsMapper = App.QuickDataMapper.create({
 
-  model: App.HiveJob,
+  json_map: {
+    id: 'entity',
+    name: 'entity',
+    user: 'primaryfilters.user',
+    hasTezDag: {
+      custom: function(source) {
+        var query = Ember.get(source, 'otherinfo.query');
+        return Ember.isNone(query) ? false : query.match("\"Tez\".*\"DagName:\"");
+      }
+    },
+    queryText: {
+      custom: function(source) {
+        var query = Ember.get(source, 'otherinfo.query');
+        return Ember.isNone(query) ? '' : $.parseJSON(query).queryText;
+      }
+    },
+    failed: {
+      custom: function(source) {
+        return Ember.get(source ,'otherinfo.status') === false;
+      }
+    },
+    startTime: {
+      custom: function(source) {
+        return source.starttime > 0 ? source.starttime : null
+      }
+    },
+    endTime: {
+      custom: function(source) {
+        return source.endtime > 0 ? source.endtime : null
+      }
+    }
+  },
 
   map: function (json) {
 
     var model = this.get('model'),
+      jobsToDelete = App.HiveJob.store.all('hiveJob').get('content').mapProperty('id'),
+      map = this.get('json_map'),
       hiveJobs = [];
+
     if (json) {
       if (!json.entities) {
         json.entities = [];
@@ -30,67 +64,35 @@ App.hiveJobsMapper = App.QuickDataMapper.create({
           json.entities = [json];
         }
       }
-      var currentEntityMap = {};
+
       json.entities.forEach(function (entity) {
-        currentEntityMap[entity.entity] = entity.entity;
-        var hiveJob = {
-          id: entity.entity,
-          name: entity.entity,
-          user: entity.primaryfilters.user
-        };
-        hiveJob.has_tez_dag = false;
-        hiveJob.query_text = '';
-        if (entity.otherinfo && entity.otherinfo.query) {
-          // Explicit false match needed for when failure hook not set
-          hiveJob.failed = entity.otherinfo.status === false;
-          hiveJob.has_tez_dag = entity.otherinfo.query.match("\"Tez\".*\"DagName:\"");
-          var queryJson = $.parseJSON(entity.otherinfo.query);
-          if (queryJson && queryJson.queryText) {
-            hiveJob.query_text = queryJson.queryText;
-          }
-        }
+        var hiveJob = Ember.JsonMapper.map(entity, map);
+
         if (entity.events != null) {
           entity.events.forEach(function (event) {
             switch (event.eventtype) {
               case "QUERY_SUBMITTED":
-                hiveJob.start_time = event.timestamp;
+                hiveJob.startTime = event.timestamp;
                 break;
               case "QUERY_COMPLETED":
-                hiveJob.end_time = event.timestamp;
+                hiveJob.endTime = event.timestamp;
                 break;
               default:
                 break;
             }
           });
         }
-        if (!hiveJob.start_time && entity.starttime > 0) {
-          hiveJob.start_time = entity.starttime;
-        }
-        if (!hiveJob.end_time && entity.endtime > 0) {
-          hiveJob.end_time = entity.endtime;
-        }
         hiveJobs.push(hiveJob);
-        hiveJob = null;
-        entity = null;
+        jobsToDelete = jobsToDelete.without(hiveJob.id);
       });
 
-      /*if(hiveJobs.length > App.router.get('mainJobsController.filterObject.jobsLimit')) {
-       var lastJob = hiveJobs.pop();
-       if(App.router.get('mainJobsController.navIDs.nextID') != lastJob.id) {
-       App.router.set('mainJobsController.navIDs.nextID', lastJob.id);
-       }
-       currentEntityMap[lastJob.id] = null;
-       }*/
+      jobsToDelete.forEach(function (id) {
+        var r = App.HiveJob.store.getById('hiveJob', id);
+        if(r) r.destroyRecord();
+      });
 
-      // Delete IDs not seen from server
-      /*var hiveJobsModel = model.find().toArray();
-       hiveJobsModel.forEach(function(job) {
-       if (job && !currentEntityMap[job.get('id')]) {
-       this.deleteRecord(job);
-       }
-       }, this);*/
     }
     App.HiveJob.store.pushMany('hiveJob', hiveJobs);
-  },
-  config: {}
+  }
+
 });

http://git-wip-us.apache.org/repos/asf/ambari/blob/5659f0a2/contrib/views/jobs/src/main/resources/ui/app/scripts/mixins/run_periodically.js
----------------------------------------------------------------------
diff --git a/contrib/views/jobs/src/main/resources/ui/app/scripts/mixins/run_periodically.js b/contrib/views/jobs/src/main/resources/ui/app/scripts/mixins/run_periodically.js
new file mode 100644
index 0000000..a6c4bbf
--- /dev/null
+++ b/contrib/views/jobs/src/main/resources/ui/app/scripts/mixins/run_periodically.js
@@ -0,0 +1,78 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Allow to run object method periodically and stop it
+ * Example:
+ *  <code>
+ *    var obj = Ember.Object.createWithMixins(App.RunPeriodically, {
+ *      method: Ember.K
+ *    });
+ *    obj.set('interval', 10000); // override default value
+ *    obj.loop('method'); // run periodically
+ *    obj.stop(); // stop running
+ *  </code>
+ * @type {Ember.Mixin}
+ */
+App.RunPeriodically = Ember.Mixin.create({
+
+  /**
+   * Interval for loop
+   * @type {number}
+   */
+  interval: 5000,
+
+  /**
+   * setTimeout's return value
+   * @type {number}
+   */
+  timer: null,
+
+  /**
+   * Run <code>methodName</code> periodically with <code>interval</code>
+   * @param {string} methodName method name to run periodically
+   * @param {bool} initRun should methodName be run before setInterval call (default - true)
+   * @method run
+   */
+  loop: function(methodName, initRun) {
+    initRun = Em.isNone(initRun) ? true : initRun;
+    var self = this,
+      interval = this.get('interval');
+    Ember.assert('Interval should be numeric and greated than 0', $.isNumeric(interval) && interval > 0);
+    if (initRun) {
+      this[methodName]();
+    }
+    this.set('timer',
+      setInterval(function () {
+        self[methodName]();
+      }, interval)
+    );
+  },
+
+  /**
+   * Stop running <code>timer</code>
+   * @method stop
+   */
+  stop: function() {
+    var timer = this.get('timer');
+    if (!Em.isNone(timer)) {
+      clearTimeout(timer);
+    }
+  }
+
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/5659f0a2/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 a3784e8..53d309e 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
@@ -21,8 +21,6 @@ App.HiveJob = App.AbstractJob.extend({
 
   queryText : DS.attr('string'),
 
-  stages : DS.attr('array'),
-
   hasTezDag: DS.attr('boolean'),
 
   tezDag : DS.belongsTo('tezDag', {async:true}),

http://git-wip-us.apache.org/repos/asf/ambari/blob/5659f0a2/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
new file mode 100644
index 0000000..a656c97
--- /dev/null
+++ b/contrib/views/jobs/src/main/resources/ui/app/scripts/translations.js
@@ -0,0 +1,81 @@
+/**
+ * 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.
+ */
+
+Ember.I18n.translations = {
+
+  'any': 'Any',
+  'apply': 'Apply',
+
+  'jobs.type':'Jobs Type',
+  'jobs.type.hive':'Hive',
+  'jobs.show.up.to':'Show up to',
+  'jobs.filtered.jobs':'%@ jobs showing',
+  'jobs.filtered.clear':'clear filters',
+  'jobs.column.id':'Id',
+  'jobs.column.user':'User',
+  'jobs.column.start.time':'Start Time',
+  'jobs.column.end.time':'End Time',
+  'jobs.column.duration':'Duration',
+  'jobs.new_jobs.info':'New jobs available on server.',
+  'jobs.loadingTasks': 'Loading...',
+
+  'jobs.nothingToShow': 'No jobs to display',
+  'jobs.error.ats.down': 'Jobs data cannot be shown since YARN App Timeline Server is not running.',
+  'jobs.error.400': 'Unable to load data.',
+  'jobs.table.custom.date.am':'AM',
+  'jobs.table.custom.date.pm':'PM',
+  'jobs.table.custom.date.header':'Select Custom Dates',
+  'jobs.table.job.fail':'Job failed to run',
+  'jobs.customDateFilter.error.required':'This field is required',
+  'jobs.customDateFilter.error.date.order':'End Date must be after Start Date',
+  'jobs.customDateFilter.startTime':'Start Time',
+  'jobs.customDateFilter.endTime':'End Time',
+  'jobs.hive.failed':'JOB FAILED',
+  'jobs.hive.more':'show more',
+  'jobs.hive.less':'show less',
+  'jobs.hive.query':'Hive Query',
+  'jobs.hive.stages':'Stages',
+  'jobs.hive.yarnApplication':'YARN&nbsp;Application',
+  'jobs.hive.tez.tasks':'Tez Tasks',
+  'jobs.hive.tez.hdfs':'HDFS',
+  '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.operatorPlan':'Operator Plan',
+  'jobs.hive.tez.dag.summary.metric':'Summary Metric',
+  'jobs.hive.tez.dag.error.noDag.title':'No Tez Information',
+  'jobs.hive.tez.dag.error.noDag.message':'This job does not identify any Tez information.',
+  'jobs.hive.tez.dag.error.noDagId.title':'No Tez Information',
+  'jobs.hive.tez.dag.error.noDagId.message':'No Tez information was found for this job. Either it is waiting to be run, or has exited unexpectedly.',
+  'jobs.hive.tez.dag.error.noDagForId.title':'No Tez Information',
+  'jobs.hive.tez.dag.error.noDagForId.message':'No details were found for the Tez ID given to this job.',
+  'jobs.hive.tez.metric.input':'Input',
+  'jobs.hive.tez.metric.output':'Output',
+  'jobs.hive.tez.metric.recordsRead':'Records Read',
+  'jobs.hive.tez.metric.recordsWrite':'Records Written',
+  'jobs.hive.tez.metric.tezTasks':'Tez Tasks',
+  'jobs.hive.tez.metric.spilledRecords':'Spilled Records',
+  'jobs.hive.tez.edge.':'Unknown',
+  'jobs.hive.tez.edge.contains':'Contains',
+  'jobs.hive.tez.edge.broadcast':'Broadcast',
+  'jobs.hive.tez.edge.scatter_gather':'Shuffle',
+
+};

http://git-wip-us.apache.org/repos/asf/ambari/blob/5659f0a2/contrib/views/jobs/src/main/resources/ui/app/scripts/views/filter_view.js
----------------------------------------------------------------------
diff --git a/contrib/views/jobs/src/main/resources/ui/app/scripts/views/filter_view.js b/contrib/views/jobs/src/main/resources/ui/app/scripts/views/filter_view.js
new file mode 100644
index 0000000..0506286
--- /dev/null
+++ b/contrib/views/jobs/src/main/resources/ui/app/scripts/views/filter_view.js
@@ -0,0 +1,488 @@
+/**
+ * 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.
+ */
+
+/**
+ * Wrapper View for all filter components. Layout template and common actions are located inside of it.
+ * Logic specific for data component(input, select, or custom multi select, which fire any changes on interface) are
+ * located in inner view - <code>filterView</code>.
+ *
+ * If we want to have input filter, put <code>textFieldView</code> to it.
+ * All inner views implemented below this view.
+ * @type {*}
+ */
+
+var wrapperView = Ember.View.extend({
+  classNames: ['view-wrapper'],
+  layoutName: 'wrapper_layout',
+  templateName: 'wrapper_template',
+
+  value: null,
+
+  /**
+   * Column index
+   */
+  column: null,
+
+  /**
+   * If this field is exists we dynamically create hidden input element and set value there.
+   * Used for some cases, where this values will be used outside of component
+   */
+  fieldId: null,
+
+  clearFilter: function(){
+    this.set('value', this.get('emptyValue'));
+    if(this.get('setPropertyOnApply')){
+      this.setValueOnApply();
+    }
+    return false;
+  },
+
+  setValueOnApply: function() {
+    if(this.get('value') == null){
+      this.set('value', '')
+    }
+    this.set(this.get('setPropertyOnApply'), this.get('value'));
+    return false;
+  },
+
+  actions: {
+    actionSetValueOnApply: function() {
+      this.setValueOnApply();
+    }
+  },
+
+  /**
+   * Use to determine whether filter is clear or not. Also when we want to set empty value
+   */
+  emptyValue: '',
+
+  /**
+   * Whether our <code>value</code> is empty or not
+   * @return {Boolean}
+   */
+  isEmpty: function(){
+    if(this.get('value') === null){
+      return true;
+    }
+    return this.get('value').toString() === this.get('emptyValue').toString();
+  },
+
+  /**
+   * Show/Hide <code>Clear filter</code> button.
+   * Also this method updates computed field related to <code>fieldId</code> if it exists.
+   * Call <code>onChangeValue</code> callback when everything is done.
+   */
+  showClearFilter: function(){
+    if(!this.get('parentNode')){
+      return;
+    }
+    // get the sort view element in the same column to current filter view to highlight them together
+    var relatedSort = $(this.get('element')).parents('thead').find('.sort-view-' + this.get('column'));
+    if(this.isEmpty()){
+      this.get('parentNode').removeClass('active-filter');
+      this.get('parentNode').addClass('notActive');
+      relatedSort.removeClass('active-sort');
+    } else {
+      this.get('parentNode').removeClass('notActive');
+      this.get('parentNode').addClass('active-filter');
+      relatedSort.addClass('active-sort');
+    }
+
+    if(this.get('fieldId')){
+      this.$('> input').eq(0).val(this.get('value'));
+    }
+
+    this.onChangeValue();
+  }.observes('value'),
+
+  /**
+   * Callback for value changes
+   */
+  onChangeValue: function(){
+
+  },
+
+  /**
+   * Filter components is located here. Should be redefined
+   */
+  filterView: Em.View,
+
+  /**
+   * Update class of parentNode(hide clear filter button) on page load
+   */
+  didInsertElement: function(){
+    var parent = this.$().parent();
+    this.set('parentNode', parent);
+    parent.addClass('notActive');
+  }
+});
+
+/**
+ * Simple input control for wrapperView
+ */
+var textFieldView = Ember.TextField.extend({
+  type:'text',
+  placeholder: Em.I18n.t('any'),
+  valueBinding: "parentView.value"
+});
+
+/**
+ * Simple multiselect control for wrapperView.
+ * Used to render blue button and popup, which opens on button click.
+ * All content related logic should be implemented manually outside of it
+ */
+var componentFieldView = Ember.View.extend({
+  classNames: ['btn-group'],
+  classNameBindings: ['isFilterOpen:open:'],
+
+  /**
+   * Whether popup is shown or not
+   */
+  isFilterOpen: false,
+
+  /**
+   * We have <code>value</code> property similar to inputs <code>value</code> property
+   */
+  valueBinding: 'parentView.value',
+
+  /**
+   * Clear filter to initial state
+   */
+  clearFilter: function(){
+    this.set('value', '');
+  },
+
+  /**
+   * Onclick handler for <code>cancel filter</code> button
+   */
+  closeFilter:function () {
+    $(document).unbind('click');
+    this.set('isFilterOpen', false);
+  },
+
+  /**
+   * Onclick handler for <code>apply filter</code> button
+   */
+  applyFilter:function() {
+    this.closeFilter();
+  },
+
+  /**
+   * Onclick handler for <code>show component filter</code> button.
+   * Also this function is used in some other places
+   */
+  clickFilterButton:function () {
+    var self = this;
+    this.set('isFilterOpen', !this.get('isFilterOpen'));
+    if (this.get('isFilterOpen')) {
+
+      var dropDown = this.$('.filter-components');
+      var firstClick = true;
+      $(document).bind('click', function (e) {
+        if (!firstClick && $(e.target).closest(dropDown).length == 0) {
+          self.set('isFilterOpen', false);
+          $(document).unbind('click');
+        }
+        firstClick = false;
+      });
+    }
+  }
+});
+
+/**
+ * Simple select control for wrapperView
+ */
+var selectFieldView = Ember.Select.extend({
+  selectionBinding: 'parentView.value',
+  contentBinding: 'parentView.content'
+});
+
+/**
+ * Result object, which will be accessible outside
+ * @type {Object}
+ */
+App.Filters = {
+  /**
+   * You can access wrapperView outside
+   */
+  wrapperView : wrapperView,
+
+  /**
+   * And also controls views if need it
+   */
+  textFieldView : textFieldView,
+  selectFieldView: selectFieldView,
+  componentFieldView: componentFieldView,
+
+  /**
+   * Quick create input filters
+   * @param config parameters of <code>wrapperView</code>
+   */
+  createTextView : function(config){
+    config.fieldType = config.fieldType || 'input-medium';
+    config.filterView = textFieldView.extend({
+      classNames : [ config.fieldType ]
+    });
+
+    return wrapperView.extend(config);
+  },
+
+  /**
+   * Quick create multiSelect filters
+   * @param config parameters of <code>wrapperView</code>
+   */
+  createComponentView : function(config){
+    config.clearFilter = function(){
+      this.forEachChildView(function(item){
+        if(item.clearFilter){
+          item.clearFilter();
+        }
+      });
+      return false;
+    };
+
+    return wrapperView.extend(config);
+  },
+
+  /**
+   * Quick create select filters
+   * @param config parameters of <code>wrapperView</code>
+   */
+  createSelectView: function(config){
+
+    config.fieldType = config.fieldType || 'input-medium';
+    config.filterView = selectFieldView.extend({
+      classNames : [ config.fieldType ],
+      attributeBindings: ['disabled','multiple'],
+      disabled: false
+    });
+    config.emptyValue = Em.I18n.t('any');
+
+    return wrapperView.extend(config);
+  },
+  /**
+   * returns the filter function, which depends on the type of property
+   * @param type
+   * @param isGlobal check is search global
+   * @return {Function}
+   */
+  getFilterByType: function(type, isGlobal){
+    switch (type){
+      case 'ambari-bandwidth':
+        return function(rowValue, rangeExp){
+          var compareChar = isNaN(rangeExp.charAt(0)) ? rangeExp.charAt(0) : false;
+          var compareScale = rangeExp.charAt(rangeExp.length - 1);
+          var compareValue = compareChar ? parseFloat(rangeExp.substr(1, rangeExp.length)) : parseFloat(rangeExp.substr(0, rangeExp.length));
+          var match = false;
+          if (rangeExp.length == 1 && compareChar !== false) {
+            // User types only '=' or '>' or '<', so don't filter column values
+            match = true;
+            return match;
+          }
+          switch (compareScale) {
+            case 'g':
+              compareValue *= 1073741824;
+              break;
+            case 'm':
+              compareValue *= 1048576;
+              break;
+            case 'k':
+              compareValue *= 1024;
+              break;
+            default:
+              //default value in GB
+              compareValue *= 1073741824;
+          }
+          rowValue = (jQuery(rowValue).text()) ? jQuery(rowValue).text() : rowValue;
+
+          var convertedRowValue;
+          if (rowValue === '<1KB') {
+            convertedRowValue = 1;
+          } else {
+            var rowValueScale = rowValue.substr(rowValue.length - 2, 2);
+            switch (rowValueScale) {
+              case 'KB':
+                convertedRowValue = parseFloat(rowValue)*1024;
+                break;
+              case 'MB':
+                convertedRowValue = parseFloat(rowValue)*1048576;
+                break;
+              case 'GB':
+                convertedRowValue = parseFloat(rowValue)*1073741824;
+                break;
+            }
+          }
+
+          switch (compareChar) {
+            case '<':
+              if (compareValue > convertedRowValue) match = true;
+              break;
+            case '>':
+              if (compareValue < convertedRowValue) match = true;
+              break;
+            case false:
+            case '=':
+              if (compareValue == convertedRowValue) match = true;
+              break;
+          }
+          return match;
+        };
+        break;
+      case 'duration':
+        return function (rowValue, rangeExp) {
+          var compareChar = isNaN(rangeExp.charAt(0)) ? rangeExp.charAt(0) : false;
+          var compareScale = rangeExp.charAt(rangeExp.length - 1);
+          var compareValue = compareChar ? parseFloat(rangeExp.substr(1, rangeExp.length)) : parseFloat(rangeExp.substr(0, rangeExp.length));
+          var match = false;
+          if (rangeExp.length == 1 && compareChar !== false) {
+            // User types only '=' or '>' or '<', so don't filter column values
+            match = true;
+            return match;
+          }
+          switch (compareScale) {
+            case 's':
+              compareValue *= 1000;
+              break;
+            case 'm':
+              compareValue *= 60000;
+              break;
+            case 'h':
+              compareValue *= 3600000;
+              break;
+            default:
+              compareValue *= 1000;
+          }
+          rowValue = (jQuery(rowValue).text()) ? jQuery(rowValue).text() : rowValue;
+
+          switch (compareChar) {
+            case '<':
+              if (compareValue > rowValue) match = true;
+              break;
+            case '>':
+              if (compareValue < rowValue) match = true;
+              break;
+            case false:
+            case '=':
+              if (compareValue == rowValue) match = true;
+              break;
+          }
+          return match;
+        };
+        break;
+      case 'date':
+        return function (rowValue, rangeExp) {
+          var match = false;
+          var timePassed = App.dateTime() - rowValue;
+          switch (rangeExp) {
+            case 'Past 1 hour':
+              match = timePassed <= 3600000;
+              break;
+            case 'Past 1 Day':
+              match = timePassed <= 86400000;
+              break;
+            case 'Past 2 Days':
+              match = timePassed <= 172800000;
+              break;
+            case 'Past 7 Days':
+              match = timePassed <= 604800000;
+              break;
+            case 'Past 14 Days':
+              match = timePassed <= 1209600000;
+              break;
+            case 'Past 30 Days':
+              match = timePassed <= 2592000000;
+              break;
+            case 'Any':
+              match = true;
+              break;
+          }
+          return match;
+        };
+        break;
+      case 'number':
+        return function(rowValue, rangeExp){
+          var compareChar = rangeExp.charAt(0);
+          var compareValue;
+          var match = false;
+          if (rangeExp.length == 1) {
+            if (isNaN(parseInt(compareChar))) {
+              // User types only '=' or '>' or '<', so don't filter column values
+              match = true;
+              return match;
+            }
+            else {
+              compareValue = parseFloat(parseFloat(rangeExp).toFixed(2));
+            }
+          }
+          else {
+            if (isNaN(parseInt(compareChar))) {
+              compareValue = parseFloat(parseFloat(rangeExp.substr(1, rangeExp.length)).toFixed(2));
+            }
+            else {
+              compareValue = parseFloat(parseFloat(rangeExp.substr(0, rangeExp.length)).toFixed(2));
+            }
+          }
+          rowValue = parseFloat((jQuery(rowValue).text()) ? jQuery(rowValue).text() : rowValue);
+          match = false;
+          switch (compareChar) {
+            case '<':
+              if (compareValue > rowValue) match = true;
+              break;
+            case '>':
+              if (compareValue < rowValue) match = true;
+              break;
+            case '=':
+              if (compareValue == rowValue) match = true;
+              break;
+            default:
+              if (rangeExp == rowValue) match = true;
+          }
+          return match;
+        };
+        break;
+      case 'multiple':
+        return function(origin, compareValue){
+          var options = compareValue.split(','),
+            rowValue = (typeof (origin) === "string") ? origin : origin.mapProperty('componentName').join(" ");
+          var str = new RegExp(compareValue, "i");
+          for (var i = 0; i < options.length; i++) {
+            if(!isGlobal) {
+              str = new RegExp('(\\W|^)' + options[i] + '(\\W|$)');
+            }
+            if (rowValue.search(str) !== -1) {
+              return true;
+            }
+          }
+          return false;
+        };
+        break;
+      case 'boolean':
+        return function (origin, compareValue){
+          return origin === compareValue;
+        };
+        break;
+      case 'string':
+      default:
+        return function(origin, compareValue){
+          var regex = new RegExp(compareValue,"i");
+          return regex.test(origin);
+        }
+    }
+  }
+
+};

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

http://git-wip-us.apache.org/repos/asf/ambari/blob/5659f0a2/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
new file mode 100644
index 0000000..26124b5
--- /dev/null
+++ b/contrib/views/jobs/src/main/resources/ui/app/scripts/views/jobs_view.js
@@ -0,0 +1,305 @@
+/**
+ * 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.JobsView = App.TableView.extend({
+
+  templateName: 'jobs',
+
+  content: [],
+
+
+  /**
+   * If no jobs table rows to show.
+   */
+  noDataToShow: true,
+
+  filterCondition:[],
+
+  /*
+   If no jobs to display set noDataToShow to true, else set emptyData to false.
+   */
+  noDataToShowObserver: function () {
+    if(this.get("controller.content.length") > 0){
+      this.set("noDataToShow",false);
+    }else{
+      this.set("noDataToShow",true);
+    }
+  }.observes("controller.content.length"),
+
+  willInsertElement: function () {
+    this._super();
+    this.clearFilters();
+    this.onApplyIdFilter();
+    this.set('tableFilteringComplete', true);
+  },
+
+  didInsertElement: function () {
+    if(!this.get('controller.sortingColumn')){
+      var columns = this.get('childViews')[0].get('childViews');
+      if(columns && columns.findProperty('name', 'startTime')){
+        columns.findProperty('name','startTime').set('status', 'sorting_desc');
+        this.get('controller').set('sortingColumn', columns.findProperty('name','startTime'))
+      }
+    }
+  },
+
+  onApplyIdFilter: function() {
+    var isIdFilterApplied = this.get('controller.filterObject.isIdFilterApplied');
+    this.get('childViews').forEach(function(childView) {
+      if (childView['clearFilter'] && childView.get('column') != 1) {
+        if(isIdFilterApplied){
+          childView.clearFilter();
+        }
+        var childOfChild = childView.get('childViews')[0];
+        if(childOfChild){
+          Em.run.next(function() {
+            childOfChild.set('disabled', isIdFilterApplied);
+          })
+        }
+      }
+    });
+  }.observes('controller.filterObject.isIdFilterApplied'),
+
+  saveFilter: function () {
+    if(this.get('tableFilteringComplete')){
+      this.updateFilter(1, this.get('controller.filterObject.id'), 'string');
+      this.updateFilter(2, this.get('controller.filterObject.user'), 'string');
+      this.updateFilter(4, this.get('controller.filterObject.windowEnd'), 'date');
+    }
+  }.observes(
+      'controller.filterObject.id',
+      'controller.filterObject.user',
+      'controller.filterObject.windowEnd'
+    ),
+
+  sortView: App.Sorts.wrapperView,
+
+  idSort: App.Sorts.fieldView.extend({
+    column: 1,
+    name: 'id',
+    displayName: Em.I18n.t('jobs.column.id'),
+    type: 'string'
+  }),
+
+  userSort: App.Sorts.fieldView.extend({
+    column: 2,
+    name: 'user',
+    displayName: Em.I18n.t('jobs.column.user'),
+    type: 'string'
+  }),
+
+  startTimeSort: App.Sorts.fieldView.extend({
+    column: 3,
+    name: 'startTime',
+    displayName: Em.I18n.t('jobs.column.start.time'),
+    type: 'number'
+  }),
+
+  endTimeSort: App.Sorts.fieldView.extend({
+    column: 4,
+    name: 'endTime',
+    displayName: Em.I18n.t('jobs.column.end.time'),
+    type: 'number'
+  }),
+
+  durationSort: App.Sorts.fieldView.extend({
+    column: 5,
+    name: 'duration',
+    displayName: Em.I18n.t('jobs.column.duration'),
+    type: 'number'
+  }),
+
+  /**
+   * Select View with list of "rows-per-page" options
+   * @type {Ember.View}
+   */
+  rowsPerPageSelectView: Ember.Select.extend({
+    content: ['10', '25', '50', '100', "250", "500"],
+    valueBinding: "controller.filterObject.jobsLimit",
+    attributeBindings: ['disabled'],
+    disabled: false,
+    disabledObserver: function () {
+      this.set('disabled', !!this.get("parentView.hasBackLinks"));
+    }.observes('parentView.hasBackLinks'),
+    change: function () {
+      this.get('controller').set('navIDs.nextID', '');
+    }
+  }),
+
+  /**
+   * return filtered number of all content number information displayed on the page footer bar
+   * @returns {String}
+   */
+  filteredJobs: function () {
+    return Em.I18n.t('jobs.filtered.jobs').fmt(this.get('controller.content.length'));
+  }.property('controller.content.length', 'controller.totalOfJobs'),
+
+  pageContentObserver: function () {
+    if (!this.get('controller.loading')) {
+      var tooltip = $('.tooltip');
+      if (tooltip.length) {
+        Ember.run.later(this, function() {
+          if (tooltip.length > 1) {
+            tooltip.first().remove();
+          }
+        }, 500);
+      }
+    }
+  }.observes('controller.loading'),
+
+  init: function() {
+    this._super();
+    App.tooltip($('body'), {
+      selector: '[rel="tooltip"]'
+    });
+  },
+
+  willDestroyElement : function() {
+    $('.tooltip').remove();
+  },
+
+  /**
+   * Filter-field for Jobs ID.
+   * Based on <code>filters</code> library
+   */
+  jobsIdFilterView: App.Filters.createTextView({
+    column: 1,
+    showApply: true,
+    setPropertyOnApply: 'controller.filterObject.id'
+  }),
+
+  /**
+   * Filter-list for User.
+   * Based on <code>filters</code> library
+   */
+  userFilterView: App.Filters.createTextView({
+    column: 2,
+    fieldType: 'input-small',
+    showApply: true,
+    setPropertyOnApply: 'controller.filterObject.user'
+  }),
+
+  /**
+   * Filter-field for Start Time.
+   * Based on <code>filters</code> library
+   */
+  startTimeFilterView: App.Filters.createSelectView({
+    fieldType: 'input-120',
+    column: 3,
+    content: ['Any', 'Past 1 hour',  'Past 1 Day', 'Past 2 Days', 'Past 7 Days', 'Past 14 Days', 'Past 30 Days', 'Custom'],
+    valueBinding: "controller.filterObject.startTime",
+    onChangeValue: function () {
+      this.get('parentView').updateFilter(this.get('column'), this.get('value'), 'date');
+    }
+  }),
+
+  jobNameView: Em.View.extend({
+
+    isLink: 'is-not-link',
+
+    isLinkObserver: function () {
+      this.refreshLinks();
+    }.observes('controller.sortingDone'),
+
+    refreshLinks: function () {
+      this.set('isLink', this.get('job.hasTezDag') ? "" : "is-not-link");
+    },
+
+    templateName: 'jobs/jobs_name',
+
+    click: function(event) {
+      /*if (this.get('job.hasTezDag')) {
+        App.router.transitionTo('main.jobs.jobDetails', this.get('job'));
+      }*/
+      return false;
+    },
+
+    didInsertElement: function () {
+      this.refreshLinks();
+    }
+  }),
+
+  /**
+   * associations between content (jobs list) property and column index
+   */
+  colPropAssoc: function () {
+    var associations = [];
+    associations[1] = 'id';
+    associations[2] = 'user';
+    associations[3] = 'startTime';
+    associations[4] = 'endTime';
+    return associations;
+  }.property(),
+
+  clearFilters: function() {
+    this.get('childViews').forEach(function(childView) {
+      if (childView['clearFilter']) {
+        childView.clearFilter();
+      }
+    });
+  },
+
+  jobFailMessage: function() {
+    return Em.I18n.t('jobs.table.job.fail');
+  }.property(),
+
+  jobsPaginationLeft: Ember.View.extend({
+    tagName: 'a',
+    templateName: 'table/navigation/pagination_left',
+    classNameBindings: ['class'],
+    class: function () {
+      if (this.get("parentView.hasBackLinks") && !this.get('controller.filterObject.isAnyFilterApplied')) {
+        return "paginate_previous";
+      }
+      return "paginate_disabled_previous";
+    }.property('parentView.hasBackLinks', 'controller.filterObject.isAnyFilterApplied'),
+
+    click: function () {
+      if (this.get("parentView.hasBackLinks") && !this.get('controller.filterObject.isAnyFilterApplied')) {
+        this.get('controller').navigateBack();
+      }
+    }
+  }),
+
+  jobsPaginationRight: Ember.View.extend({
+    tagName: 'a',
+    templateName: 'table/navigation/pagination_right',
+    classNameBindings: ['class'],
+    class: function () {
+      if (this.get("parentView.hasNextJobs") && !this.get('controller.filterObject.isAnyFilterApplied')) {
+        return "paginate_next";
+      }
+      return "paginate_disabled_next";
+    }.property("parentView.hasNextJobs", 'controller.filterObject.isAnyFilterApplied'),
+
+    click: function () {
+      if (this.get("parentView.hasNextJobs") && !this.get('controller.filterObject.isAnyFilterApplied')) {
+        this.get('controller').navigateNext();
+      }
+    }
+  }),
+
+  hasNextJobs: function() {
+    return (this.get("controller.navIDs.nextID.length") > 0);
+  }.property('controller.navIDs.nextID'),
+
+  hasBackLinks: function() {
+    return (this.get("controller.navIDs.backIDs").length > 1);
+  }.property('controller.navIDs.backIDs.[].length')
+
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/5659f0a2/contrib/views/jobs/src/main/resources/ui/app/scripts/views/sort_view.js
----------------------------------------------------------------------
diff --git a/contrib/views/jobs/src/main/resources/ui/app/scripts/views/sort_view.js b/contrib/views/jobs/src/main/resources/ui/app/scripts/views/sort_view.js
new file mode 100644
index 0000000..a8d5d39
--- /dev/null
+++ b/contrib/views/jobs/src/main/resources/ui/app/scripts/views/sort_view.js
@@ -0,0 +1,253 @@
+/**
+ * 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.
+ */
+
+/**
+ * Wrapper View for all sort components. Layout template and common actions are located inside of it.
+ * Logic specific for sort fields
+ * located in inner view - <code>fieldView</code>.
+ *
+ * @type {*}
+ */
+var wrapperView = Em.View.extend({
+  tagName: 'tr',
+
+  classNames: ['sort-wrapper'],
+
+  willInsertElement: function () {
+    if (this.get('parentView.tableFilteringComplete')) {
+      this.get('parentView').set('filteringComplete', true);
+    }
+  },
+
+  /**
+   * Load sort statuses from local storage
+   * Works only after finish filtering in the parent View
+   */
+  loadSortStatuses: function () {
+
+  }.observes('parentView.filteringComplete'),
+
+  /**
+   * Save sort statuses to local storage
+   * Works only after finish filtering in the parent View
+   */
+  saveSortStatuses: function () {
+    if (!this.get('parentView.filteringComplete')) return;
+
+    var statuses = [];
+    this.get('childViews').forEach(function (childView) {
+      statuses.push({
+        name: childView.get('name'),
+        status: childView.get('status')
+      });
+    });
+  },
+
+  /**
+   * sort content by property
+   * @param property {object}
+   * @param order {Boolean} true - DESC, false - ASC
+   * @param returnSorted {Boolean}
+   */
+  sort: function (property, order, returnSorted) {
+    var content = this.get('content').toArray();
+    var sortFunc = this.getSortFunc(property, order);
+    var status = order ? 'sorting_desc' : 'sorting_asc';
+
+    this.resetSort();
+    this.get('childViews').findProperty('name', property.get('name')).set('status', status);
+    this.saveSortStatuses(property, order);
+    content.sort(sortFunc);
+
+    if (!!returnSorted) {
+      return content;
+    } else {
+      this.set('content', content);
+    }
+  },
+
+  isSorting: false,
+
+  onContentChange: function () {
+    if (!this.get('isSorting') && this.get('content.length')) {
+      this.get('childViews').forEach(function (view) {
+        if (view.status !== 'sorting') {
+          var status = view.get('status');
+          this.set('isSorting', true);
+          this.sort(view, status == 'sorting_desc');
+          this.set('isSorting', false);
+          view.set('status', status);
+        }
+      }, this);
+    }
+  }.observes('content.length'),
+
+  /**
+   * reset all sorts fields
+   */
+  resetSort: function () {
+    this.get('childViews').setEach('status', 'sorting');
+  },
+  /**
+   * determines sort function depending on the type of sort field
+   * @param property
+   * @param order
+   * @return {*}
+   */
+  getSortFunc: function (property, order) {
+    var func;
+    switch (property.get('type')) {
+      case 'ip':
+        func = function (a, b) {
+          a = App.Helpers.misc.ipToInt(a.get(property.get('name')));
+          b = App.Helpers.misc.ipToInt(b.get(property.get('name')));
+          return order ? (b - a) : (a - b);
+        };
+        break;
+      case 'number':
+        func = function (a, b) {
+          a = parseFloat(a.get(property.get('name')));
+          b = parseFloat(b.get(property.get('name')));
+          return order ? (b - a) : (a - b);
+        };
+        break;
+      default:
+        func = function (a, b) {
+          if (order) {
+            if (a.get(property.get('name')) > b.get(property.get('name')))
+              return -1;
+            if (a.get(property.get('name')) < b.get(property.get('name')))
+              return 1;
+            return 0;
+          } else {
+            if (a.get(property.get('name')) < b.get(property.get('name')))
+              return -1;
+            if (a.get(property.get('name')) > b.get(property.get('name')))
+              return 1;
+            return 0;
+          }
+        }
+    }
+    return func;
+  }
+});
+
+/**
+ * view that carry on sorting on server-side via <code>refresh()</code> in parentView
+ * @type {*}
+ */
+var serverWrapperView = Em.View.extend({
+  tagName: 'tr',
+
+  classNames: ['sort-wrapper'],
+
+  willInsertElement: function () {
+    this.loadSortStatuses();
+  },
+
+  /**
+   * Initialize and save sorting statuses: publicHostName sorting_asc
+   */
+  loadSortStatuses: function () {
+    var statuses = [];
+    var childViews = this.get('childViews');
+    childViews.forEach(function (childView) {
+      var sortStatus = (childView.get('name') == 'publicHostName' && childView.get('status') == 'sorting') ? 'sorting_asc' : childView.get('status');
+      statuses.push({
+        name: childView.get('name'),
+        status: sortStatus
+      });
+      childView.set('status', sortStatus);
+    });
+    this.get('controller').set('sortingColumn', childViews.findProperty('name', 'publicHostName'));
+  },
+
+  /**
+   * Save sort statuses to local storage
+   * Works only after finish filtering in the parent View
+   */
+  saveSortStatuses: function () {
+    var statuses = [];
+    this.get('childViews').forEach(function (childView) {
+      statuses.push({
+        name: childView.get('name'),
+        status: childView.get('status')
+      });
+    });
+  },
+
+  /**
+   * sort content by property
+   * @param property {object}
+   * @param order {Boolean} true - DESC, false - ASC
+   */
+  sort: function (property, order) {
+    var status = order ? 'sorting_desc' : 'sorting_asc';
+
+    this.resetSort();
+    this.get('childViews').findProperty('name', property.get('name')).set('status', status);
+    this.saveSortStatuses();
+    this.get('parentView').refresh();
+  },
+
+  /**
+   * reset all sorts fields
+   */
+  resetSort: function () {
+    this.get('childViews').setEach('status', 'sorting');
+  }
+});
+
+/**
+ * particular view that contain sort field properties:
+ * name - name of property in content table
+ * type(optional) - specific type to sort
+ * displayName - label to display
+ * @type {*}
+ */
+var fieldView = Em.View.extend({
+  templateName: 'sort_field_template',
+  classNameBindings: ['viewNameClass'],
+  tagName: 'th',
+  name: null,
+  displayName: null,
+  status: 'sorting',
+  viewNameClass: function () {
+    return 'sort-view-' + this.get('column');
+  }.property(),
+  type: null,
+  column: 0,
+  /**
+   * callback that run sorting and define order of sorting
+   * @param event
+   */
+  click: function (event) {
+    this.get('parentView').sort(this, (this.get('status') !== 'sorting_desc'));
+    this.get('controller').set('sortingColumn', this);
+  }
+});
+
+/**
+ * Result object, which will be accessible outside
+ * @type {Object}
+ */
+App.Sorts = {
+  serverWrapperView: serverWrapperView,
+  wrapperView: wrapperView,
+  fieldView: fieldView
+};