Repository: zeppelin
Updated Branches:
  refs/heads/master c90a3c2e5 -> 3183a5967


[ZEPPELIN-2501] Better Job Navigation Experience

### What is this PR for?

Improve UX for Job Navigation. Please refer the *TODOs* section and screenshots 
attached below for detail.

FYI, **pagination** is added to improve page loading speed. Currently, it is 
going to be slow when there are 50+ jobs.

### What type of PR is it?
[Improvement | Feature]

### Todos
* [x] - improve UI
* [x] - add pagination to improve page loading speed
* [x] - interactive job searching without *enter key*
* [x] - add date sorter
* [x] - display total job count

### What is the Jira issue?

[ZEPPELIN-2501](https://issues.apache.org/jira/browse/ZEPPELIN-2501)

### How should this be tested?

1. build: `mvn clean package -DskipTests; ./bin/zeppelin-daemon.sh restart`
2. open the job page: `http://localhost:8080/#/jobmanager`
3. try to search, filter, sort.

### Screenshots (if appropriate)

#### Before

![image](https://cloud.githubusercontent.com/assets/4968473/26729918/39f51c8a-47ea-11e7-88a1-30ffa5947e3a.png)

#### After: Larger UI components

![image](https://cloud.githubusercontent.com/assets/4968473/26729934/48ea4c74-47ea-11e7-91f0-4aa97bb93161.png)

#### After: Interactive search without enter-key

![2501_interactive](https://cloud.githubusercontent.com/assets/4968473/26730000/7f39045a-47ea-11e7-8377-fc9d12b12237.gif)

#### After Newly added timestamp sorter

![2501_timestamp](https://cloud.githubusercontent.com/assets/4968473/26730073/c50ea656-47ea-11e7-8c5d-db61064ca745.gif)

#### After: Newly added pagination
![2501_pagination](https://cloud.githubusercontent.com/assets/4968473/26730130/ef173c06-47ea-11e7-9e5d-818693d3220a.gif)

#### Total Job Count

![image](https://cloud.githubusercontent.com/assets/4968473/26747803/55bf15a8-4833-11e7-9444-b0c69f4bd9b6.png)

### Questions:
* Does the licenses files need update? - NO
* Is there breaking changes for older versions? - NO
* Does this needs documentation? - NO

Author: 1ambda <[email protected]>

Closes #2391 from 1ambda/ZEPPELIN-2501/pagination-for-job-page and squashes the 
following commits:

53769661e [1ambda] feat: Add total job count
c5ff1f5a2 [1ambda] fix: Add license notation to job-status
3dd508155 [1ambda] feat: Add date sorter
e20d1dbe1 [1ambda] fix: Apply interactive search
081b9bb4e [1ambda] feat: re-style search tools
d52008e1f [1ambda] fix: Remove all styles in search tools
d7017f41c [1ambda] fix: Better layout for jobmanager header
084407a1e [1ambda] feat: Add pagination for JOB page
018048af1 [1ambda] fix: Simplify job names
f8fab3f10 [1ambda] refactor: Remove duplicated switch DOMs
0ae31a01b [1ambda] style: Reindent jobmanager.html


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

Branch: refs/heads/master
Commit: 3183a596750ee81440ce785d73d9b9d5b0b8873a
Parents: c90a3c2
Author: 1ambda <[email protected]>
Authored: Sat Jun 3 08:04:14 2017 +0900
Committer: Khalid Huseynov <[email protected]>
Committed: Thu Jun 22 10:56:30 2017 +0900

----------------------------------------------------------------------
 zeppelin-web/src/app/app.js                     |   2 +-
 .../src/app/jobmanager/jobmanager.controller.js | 128 +++++++-----
 zeppelin-web/src/app/jobmanager/jobmanager.css  |  88 +++++++-
 .../src/app/jobmanager/jobmanager.filter.js     |  20 +-
 zeppelin-web/src/app/jobmanager/jobmanager.html | 199 +++++++++----------
 .../src/app/jobmanager/jobs/job-status.js       |  22 ++
 zeppelin-web/src/app/jobmanager/jobs/job.html   |  83 +-------
 7 files changed, 293 insertions(+), 249 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/zeppelin/blob/3183a596/zeppelin-web/src/app/app.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/app.js b/zeppelin-web/src/app/app.js
index c6674f1..dad04f0 100644
--- a/zeppelin-web/src/app/app.js
+++ b/zeppelin-web/src/app/app.js
@@ -105,7 +105,7 @@ let zeppelinWebApp = angular.module('zeppelinWebApp', 
requiredModules)
       })
       .when('/jobmanager', {
         templateUrl: 'app/jobmanager/jobmanager.html',
-        controller: 'JobmanagerCtrl'
+        controller: 'JobManagerCtrl'
       })
       .when('/interpreter', {
         templateUrl: 'app/interpreter/interpreter.html',

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/3183a596/zeppelin-web/src/app/jobmanager/jobmanager.controller.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/jobmanager/jobmanager.controller.js 
b/zeppelin-web/src/app/jobmanager/jobmanager.controller.js
index ec6f1ab..16d47ba 100644
--- a/zeppelin-web/src/app/jobmanager/jobmanager.controller.js
+++ b/zeppelin-web/src/app/jobmanager/jobmanager.controller.js
@@ -12,12 +12,40 @@
  * limitations under the License.
  */
 
+import { JobStatus, } from './jobs/job-status'
+
 angular.module('zeppelinWebApp')
-  .controller('JobmanagerCtrl', JobmanagerCtrl)
+  .controller('JobManagerCtrl', JobManagerCtrl)
+
+const JobDateSorter = {
+  RECENTLY_UPDATED: 'Recently Update',
+  OLDEST_UPDATED: 'Oldest Updated',
+}
 
-function JobmanagerCtrl ($scope, websocketMsgSrv, $interval, ngToast, $q, 
$timeout, jobManagerFilter) {
+function JobManagerCtrl ($scope, websocketMsgSrv, $interval, ngToast, $q, 
$timeout, jobManagerFilter) {
   'ngInject'
 
+  $scope.pagination = {
+    currentPage: 1,
+    itemsPerPage: 10,
+    maxPageCount: 5,
+  }
+
+  $scope.sorter = {
+    AvailableDateSorter: Object.keys(JobDateSorter).map(key => { return 
JobDateSorter[key] }),
+    currentDateSorter: JobDateSorter.RECENTLY_UPDATED,
+  }
+
+  $scope.setJobDateSorter = function(dateSorter) {
+    $scope.sorter.currentDateSorter = dateSorter
+  }
+
+  $scope.getJobsInCurrentPage = function(jobs) {
+    const cp = $scope.pagination.currentPage
+    const itp = $scope.pagination.itemsPerPage
+    return jobs.slice((cp - 1) * itp, (cp * itp))
+  }
+
   ngToast.dismiss()
   let asyncNotebookJobFilter = function (jobInfomations, filterConfig) {
     return $q(function (resolve, reject) {
@@ -26,15 +54,52 @@ function JobmanagerCtrl ($scope, websocketMsgSrv, 
$interval, ngToast, $q, $timeo
     })
   }
 
+  $scope.$watch('sorter.currentDateSorter', function() {
+    $scope.filterConfig.isSortByAsc =
+      $scope.sorter.currentDateSorter === JobDateSorter.OLDEST_UPDATED
+    asyncNotebookJobFilter($scope.jobInfomations, $scope.filterConfig)
+  })
+
+  $scope.getJobIconByStatus = function(jobStatus) {
+    if (jobStatus === JobStatus.READY) {
+      return 'fa fa-circle-o'
+    } else if (jobStatus === JobStatus.FINISHED) {
+      return 'fa fa-circle'
+    } else if (jobStatus === JobStatus.ABORT) {
+      return 'fa fa-circle'
+    } else if (jobStatus === JobStatus.ERROR) {
+      return 'fa fa-circle'
+    } else if (jobStatus === JobStatus.PENDING) {
+      return 'fa fa-circle'
+    } else if (jobStatus === JobStatus.RUNNING) {
+      return 'fa fa-spinner'
+    }
+  }
+
+  $scope.getJobColorByStatus = function(jobStatus) {
+    if (jobStatus === JobStatus.READY) {
+      return 'green'
+    } else if (jobStatus === JobStatus.FINISHED) {
+      return 'green'
+    } else if (jobStatus === JobStatus.ABORT) {
+      return 'orange'
+    } else if (jobStatus === JobStatus.ERROR) {
+      return 'red'
+    } else if (jobStatus === JobStatus.PENDING) {
+      return 'gray'
+    } else if (jobStatus === JobStatus.RUNNING) {
+      return 'blue'
+    }
+  }
+
   $scope.doFiltering = function (jobInfomations, filterConfig) {
-    asyncNotebookJobFilter(jobInfomations, filterConfig).then(
-      function () {
-        // success
-        $scope.isLoadingFilter = false
-      },
-      function () {
-        // failed
-      })
+    asyncNotebookJobFilter(jobInfomations, filterConfig)
+      .then(
+        () => { $scope.isLoadingFilter = false },
+        (error) => {
+          console.error('Failed to search jobs from server', error)
+        }
+      )
   }
 
   $scope.filterValueToName = function (filterValue, maxStringLength) {
@@ -48,7 +113,7 @@ function JobmanagerCtrl ($scope, websocketMsgSrv, $interval, 
ngToast, $q, $timeo
       }
       return $scope.activeInterpreters[index].name
     } else {
-      return 'Interpreter is not set'
+      return 'NONE'
     }
   }
 
@@ -57,37 +122,6 @@ function JobmanagerCtrl ($scope, websocketMsgSrv, 
$interval, ngToast, $q, $timeo
     $scope.doFiltering($scope.jobInfomations, $scope.filterConfig)
   }
 
-  $scope.onChangeRunJobToAlwaysTopToggle = function () {
-    $scope.filterConfig.isRunningAlwaysTop = 
!$scope.filterConfig.isRunningAlwaysTop
-    $scope.doFiltering($scope.jobInfomations, $scope.filterConfig)
-  }
-
-  $scope.onChangeSortAsc = function () {
-    $scope.filterConfig.isSortByAsc = !$scope.filterConfig.isSortByAsc
-    $scope.doFiltering($scope.jobInfomations, $scope.filterConfig)
-  }
-
-  $scope.doFilterInputTyping = function (keyEvent, jobInfomations, 
filterConfig) {
-    let RETURN_KEY_CODE = 13
-    $timeout.cancel($scope.dofilterTimeoutObject)
-    $scope.isActiveSearchTimer = true
-    $scope.dofilterTimeoutObject = $timeout(function () {
-      $scope.doFiltering(jobInfomations, filterConfig)
-      $scope.isActiveSearchTimer = false
-    }, 10000)
-    if (keyEvent.which === RETURN_KEY_CODE) {
-      $timeout.cancel($scope.dofilterTimeoutObject)
-      $scope.doFiltering(jobInfomations, filterConfig)
-      $scope.isActiveSearchTimer = false
-    }
-  }
-
-  $scope.doForceFilterInputTyping = function (keyEvent, jobInfomations, 
filterConfig) {
-    $timeout.cancel($scope.dofilterTimeoutObject)
-    $scope.doFiltering(jobInfomations, filterConfig)
-    $scope.isActiveSearchTimer = false
-  }
-
   $scope.init = function () {
     $scope.isLoadingFilter = true
     $scope.jobInfomations = []
@@ -96,21 +130,13 @@ function JobmanagerCtrl ($scope, websocketMsgSrv, 
$interval, ngToast, $q, $timeo
       isRunningAlwaysTop: true,
       filterValueNotebookName: '',
       filterValueInterpreter: '*',
-      isSortByAsc: true
+      isSortByAsc: $scope.sorter.currentDateSorter === 
JobDateSorter.OLDEST_UPDATED,
     }
     $scope.sortTooltipMsg = 'Switch to sort by desc'
     $scope.jobTypeFilter = jobManagerFilter
 
     websocketMsgSrv.getNoteJobsList()
 
-    $scope.$watch('filterConfig.isSortByAsc', function (value) {
-      if (value) {
-        $scope.sortTooltipMsg = 'Switch to sort by desc'
-      } else {
-        $scope.sortTooltipMsg = 'Switch to sort by asc'
-      }
-    })
-
     $scope.$on('$destroy', function () {
       websocketMsgSrv.unsubscribeJobManager()
     })

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/3183a596/zeppelin-web/src/app/jobmanager/jobmanager.css
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/jobmanager/jobmanager.css 
b/zeppelin-web/src/app/jobmanager/jobmanager.css
index 5f7ffb7..7b5dc04 100644
--- a/zeppelin-web/src/app/jobmanager/jobmanager.css
+++ b/zeppelin-web/src/app/jobmanager/jobmanager.css
@@ -22,7 +22,7 @@
   min-height: 32px;
 }
 
-.jobManagerHead {
+.job-manager-header {
   margin: -10px -10px 20px;
   padding: 10px 15px 15px 15px;
   background: white;
@@ -30,18 +30,88 @@
   border-bottom: 1px solid #E5E5E5;
 }
 
-.jobManagerHead .header {
+.job-manager-header .header {
   font-family: 'Roboto', sans-serif;
 }
 
-.job-note-name-query {
-  padding: 6px;
-  height: 25px;
-  width: 200px;
+.job-search-tool {
+  display: inline-block;
+  margin-left: 15px;
 }
 
-.job-note-name-font-family {
-  font: inherit;
+#job-manager-header .job-search-tool .search-input {
+  margin-right: 7px;
+  min-width: 215px;
+}
+
+#job-manager-header .job-search-tool .search-input > input {
+  font-family: 'FontAwesome', 'Helvetica Neue', Helvetica, Arial, sans-serif;
+  border-radius: 4px;
   font-size: 14px;
-  font-weight: normal;
+  padding-left: 10px;
+}
+
+#job-manager-header .job-search-tool .dropdown-toggle {
+  border-radius: 3px;
+  float: none;
+  min-width: 150px;
+  text-align: left;
+  margin-right: 5px;
+}
+
+
+#job-manager-header .job-search-tool .date-sort-button {
+  min-width: 180px;
+}
+
+.job-search-tool .dropdown-text-desc {
+  color: gray;
+  font-weight: 400;
+  font-family: "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif;
+}
+
+.job-search-tool .dropdown-text-value {
+  margin-left: 2px;
+  font-family: "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif;
+}
+
+.search-tool-dropdown-content > li > a {
+  border-bottom: 1px solid #eee;
+}
+
+.search-tool-dropdown-content > li:last-child > a {
+  border-bottom: none;
+}
+
+.job-icon-desc-container {
+  display: inline-block;
+  margin-top: 10px;
+  margin-right: 30px;
+  float: right;
+}
+
+.job-desc-icon {
+  margin-right: 3px;
+}
+
+.job-pagination-container {
+  text-align: center;
+  margin-top: 50px;
+  padding-bottom: 100px;
+}
+
+.job-counter {
+  float: right;
+  clear: both;
+  margin-left: 8px;
+  margin-top: 6px;
+}
+
+.job-counter .job-counter-label {
+  color: gray;
+}
+
+.job-counter .job-counter-value {
+  font-size: 15px;
+  font-weight: bold;
 }

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/3183a596/zeppelin-web/src/app/jobmanager/jobmanager.filter.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/jobmanager/jobmanager.filter.js 
b/zeppelin-web/src/app/jobmanager/jobmanager.filter.js
index 9211498..2b724a4 100644
--- a/zeppelin-web/src/app/jobmanager/jobmanager.filter.js
+++ b/zeppelin-web/src/app/jobmanager/jobmanager.filter.js
@@ -22,26 +22,34 @@ function jobManagerFilter () {
     let filterItems = jobItems
 
     if (filterValueInterpreter === undefined) {
-      filterItems = _.filter(filterItems, function (jobItem) {
-        return jobItem.interpreter === undefined ? true : false
+      filterItems = filterItems.filter((jobItem) => {
+        return jobItem.interpreter === undefined
       })
     } else if (filterValueInterpreter !== '*') {
       filterItems = _.where(filterItems, {interpreter: filterValueInterpreter})
     }
 
     if (filterValueNotebookName !== '') {
-      filterItems = _.filter(filterItems, function (jobItem) {
+      filterItems = filterItems.filter((jobItem) => {
         let lowerFilterValue = filterValueNotebookName.toLocaleLowerCase()
         let lowerNotebookName = jobItem.noteName.toLocaleLowerCase()
         return lowerNotebookName.match(new RegExp('.*' + lowerFilterValue + 
'.*'))
       })
     }
 
-    filterItems = _.sortBy(filterItems, function (sortItem) {
-      return sortItem.noteName.toLowerCase()
+    filterItems = filterItems.sort((jobItem) => {
+      return jobItem.noteName.toLowerCase()
     })
 
-    return isSortByAsc ? filterItems : filterItems.reverse()
+    filterItems = filterItems.sort((x, y) => {
+      if (isSortByAsc) {
+        return x.unixTimeLastRun - y.unixTimeLastRun
+      } else {
+        return y.unixTimeLastRun - x.unixTimeLastRun
+      }
+    })
+
+    return filterItems
   }
   return filterContext
 }

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/3183a596/zeppelin-web/src/app/jobmanager/jobmanager.html
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/jobmanager/jobmanager.html 
b/zeppelin-web/src/app/jobmanager/jobmanager.html
index 49ab2ee..e5c030a 100644
--- a/zeppelin-web/src/app/jobmanager/jobmanager.html
+++ b/zeppelin-web/src/app/jobmanager/jobmanager.html
@@ -11,8 +11,8 @@ 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.
 -->
-<!-- Here the controller <JobmanagerCtrl> is not needed because explicitly set 
in the app.js (route) -->
-<div class="jobManagerHead" data-ng-init="init()">
+<!-- Here the controller <JobManagerCtrl> is not needed because explicitly set 
in the app.js (route) -->
+<div id="job-manager-header" class="job-manager-header" data-ng-init="init()">
   <div class="header">
     <div class="row">
       <div class="col-md-12">
@@ -27,138 +27,117 @@ limitations under the License.
       </div>
     </div>
   </div>
-  <div style="margin: 0px">
-    <hr style="margin-top: 10px; margin-bottom: 10px;" />
-  </div>
+
+  <hr style="margin-top: 15px; margin-bottom: 15px;" />
 
   <div class="row">
-    <div class="col-md-6 text-left">
+    <!-- search tools (input, drop-down, sorting) -->
+    <div class="job-search-tool">
       <div class="form-inline">
-        <span class="labelBtn btn-group">
-          <button type="button"
-                  class="btn btn-default"
-                  style="width: 25px; height: 25px; margin-right: 0px; 
padding: 1px 0px 3px 3px"
-                  ng-click="onChangeSortAsc()"
-                  tooltip-placement="right" uib-tooltip="{{sortTooltipMsg}}">
-            <i class="fa" ng-class="{true: 'fa-sort-amount-asc', false : 
'fa-sort-amount-desc'}[filterConfig.isSortByAsc]"></i>
-          </button>
-        </span>
-        <span class="labelBtn btn-group" style="margin-left: 0px; 
padding-left: 3px;">
-          <button type="button" class="btn btn-default btn-xs dropdown-toggle"
-                 data-toggle="dropdown"
-                 style="min-width: 100px; text-align: right !important;">
-           <span class="text-right job-note-name-font-family">
-             {{filterValueToName(filterConfig.filterValueInterpreter)}}<span 
class="caret" style="margin-left: 10px"></span>
-           </span>
-          </button>
-          <ul class="dropdown-menu pull-left" role="menu">
+        <span class="btn-group">
+          <!-- search tool: input -->
+          <div class="input-group search-input">
+            <input class="form-control btn-xs"
+                   placeholder="&#xf002 Search jobs..."
+                   type="text"
+                   ng-model="filterConfig.filterValueNotebookName"
+                   ng-model-options="{ updateOn: 'default blur', debounce: { 
'default': 300, 'blur': 0 } }"
+                   ng-change="doFiltering(jobInfomations, filterConfig)" />
+          </div>
+
+          <!-- search tool: default interpreter dropdown -->
+          <div class="btn btn-default dropdown-toggle"
+               data-toggle="dropdown">
+            <span>
+              <span class="dropdown-text-desc">Interpreter: </span>
+              <span 
class="dropdown-text-value">{{filterValueToName(filterConfig.filterValueInterpreter)}}</span>
+              <span class="caret" style="margin-top: 8px; float: 
right;"></span>
+              <span style="clear: both;"></span>
+            </span>
+          </div>
+          <ul class="dropdown-menu dropdown-menu-right 
search-tool-dropdown-content" role="menu">
            <li ng-repeat="interpreterOption in activeInterpreters">
-             <a class="job-note-name-font-family" style="cursor:pointer"
-                ng-click="setFilterValue(interpreterOption.value)">
+             <a ng-click="setFilterValue(interpreterOption.value)"
+                ng-style="(filterValueToName(interpreterOption.value) === 
'ALL' || filterValueToName(interpreterOption.value) === 'NONE') ? { 
'font-weight': 500 } : {}"
+                class="dropdown-list-value">
                {{filterValueToName(interpreterOption.value)}}
              </a>
            </li>
           </ul>
-          <div class="input-group">
-            <input
-             class="job-note-name-query job-note-name-font-family form-control 
btn-xs"
-             style="margin-left: 5px;"
-             placeholder="Search for Job"
-             type="text" ng-model="filterConfig.filterValueNotebookName"
-             ng-keyup="doFilterInputTyping($event, jobInfomations, 
filterConfig, isLoadingFilter)"/>
-            <span
-                class="input-group-addon text-right" ng-class="{true : 
'btn-primary active', false: ''}[isActiveSearchTimer]"
-                style="height: 5px; padding: 0px 6px 0px 9px !important;"
-                ng-click="doForceFilterInputTyping($event, jobInfomations, 
filterConfig, isLoadingFilter)">
-              <i class="fa fa-search fa-sm"></i></span>
-            </div>
-        </span>
-      </div>
-    </div>
-    <div
-      class="col-md-6 text-right"
-      style="padding-top: 6px;">
-      <span
-        ng-repeat="jobStatus in ['READY', 'FINISHED', 'ABORT', 
'ERROR','PENDING','RUNNING']"
-        ng-switch="jobStatus">
-        <span
-          ng-switch-when="FINISHED">
-          <i style="color: green; margin-right: 3px;" class="fa fa-circle"
-             ng-click="">
-          </i>
-          {{jobStatus}}
-        </span>
-        <span
-          ng-switch-when="RUNNING"
-          style="margin-right: 3px;">
-          <i style="color: blue" class="fa fa-spinner"
-             ng-click="">
-          </i>
-          {{jobStatus}}
-        </span>
-        <span
-          ng-switch-when="READY"
-          style="margin-right: 3px;">
-          <i style="color: green" class="fa fa-circle-o"
-             ng-click="">
-          </i>
-          {{jobStatus}}
+
         </span>
-        <span
-          ng-switch-when="PENDING"
-          style="margin-right: 3px;">
-          <i style="color: gray" class="fa fa-circle"
-             ng-click="">
-          </i>
-          {{jobStatus}}
+
+        <span class="btn-group">
+          <!-- search tool: date dropdown -->
+          <div class="date-sort-button btn btn-default dropdown-toggle" 
data-toggle="dropdown">
+            <span>
+              <span class="dropdown-text-desc">Sort: </span>
+              <span 
class="dropdown-text-value">{{sorter.currentDateSorter}}</span>
+              <span class="caret" style="margin-top: 8px; float: 
right;"></span>
+              <span style="clear: both;"></span>
+            </span>
+          </div>
+          <ul class="dropdown-menu dropdown-menu-right 
search-tool-dropdown-content" role="menu">
+           <li ng-repeat="dateSorter in sorter.AvailableDateSorter">
+             <a ng-click="setJobDateSorter(dateSorter)" 
class="dropdown-list-value">
+               {{dateSorter}}
+             </a>
+           </li>
+          </ul>
         </span>
-        <span
-          ng-switch-when="ABORT"
-          style="margin-right: 3px;">
-          <i style="color: orange" class="fa fa-circle"
-             ng-click="">
-          </i>
-          {{jobStatus}}
+        <span class="job-counter">
+          <span class="job-counter-label">Total: </span>
+          <span 
class="job-counter-value">{{JobInfomationsByFilter.length}}</span>
         </span>
-        <span
-          ng-switch-when="ERROR"
-          style="margin-right: 3px;">
-          <i style="color: red" class="fa fa-circle"
-             ng-click="">
-          </i>
-          {{jobStatus}}
+      </div>
+    </div>
+
+    <!-- job icon descriptions -->
+    <div class="job-icon-desc-container hidden-xs hidden-sm hidden-md">
+      <span ng-repeat="jobStatus in ['READY', 'FINISHED', 'ABORT', 
'ERROR','PENDING','RUNNING']">
+        <span style="margin-right: 2px;">
+          <i class="job-desc-icon"
+            ng-style="{'color': getJobColorByStatus(jobStatus)}"
+            ng-class="getJobIconByStatus(jobStatus)" ></i>{{jobStatus}}
         </span>
       </span>
     </div>
+
+    <div style="clear: both;"></div>
+
   </div>
 </div>
+
 <div>
   <div class="note-jump"></div>
-  <div
-    ng-if="isLoadingFilter === true"
-    class="paragraph-col">
-    <div
-      class="job-space box job-margin text-center">
-      <i style="color: blue" class="fa fa-spinner spinAnimation">
-      </i>&nbsp;Loading...
+  <div ng-if="isLoadingFilter === true" class="paragraph-col">
+    <div class="job-space box job-margin text-center">
+      <i style="color: blue" class="fa fa-spinner spinAnimation"></i>Loading...
     </div>
   </div>
-  <div
-    ng-if="JobInfomationsByFilter.length > 0"
-    ng-repeat="notebookJob in JobInfomationsByFilter track by $index"
-    class="paragraph-col">
+  <div ng-if="JobInfomationsByFilter.length > 0"
+       ng-repeat="notebookJob in getJobsInCurrentPage(JobInfomationsByFilter)"
+       class="paragraph-col">
     <div ng-include src="'app/jobmanager/jobs/job.html'"
          class="job-space box job-margin"
          ng-controller="JobCtrl">
     </div>
   </div>
-  <div
-    ng-if="isLoadingFilter === false && JobInfomationsByFilter.length <= 0"
-    class="paragraph-col">
-    <div
-      class="job-space box job-margin text-center">
-      No Job found
-    </div>
+  <div ng-if="isLoadingFilter === false && JobInfomationsByFilter.length <= 0"
+       class="paragraph-col">
+    <div class="job-space box job-margin text-center">No Job found</div>
   </div>
-  <div style="clear:both;height:10px"></div>
+
+  <!-- pagination -->
+  <div class="job-pagination-container">
+    <ul uib-pagination class="pagination-sm"
+        total-items="JobInfomationsByFilter.length"
+        ng-model="pagination.currentPage"
+        items-per-page="pagination.itemsPerPage"
+        boundary-links="true" rotate="false"
+        max-size="pagination.maxPageCount"
+        previous-text="&lsaquo;" next-text="&rsaquo;"
+        first-text="&laquo;" last-text="&raquo;"></ul>
+  </div>
+
 </div>

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/3183a596/zeppelin-web/src/app/jobmanager/jobs/job-status.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/jobmanager/jobs/job-status.js 
b/zeppelin-web/src/app/jobmanager/jobs/job-status.js
new file mode 100644
index 0000000..fa14637
--- /dev/null
+++ b/zeppelin-web/src/app/jobmanager/jobs/job-status.js
@@ -0,0 +1,22 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export const JobStatus = {
+  READY: 'READY',
+  FINISHED: 'FINISHED',
+  ABORT: 'ABORT',
+  ERROR: 'ERROR',
+  PENDING: 'PENDING',
+  RUNNING: 'RUNNING',
+}

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/3183a596/zeppelin-web/src/app/jobmanager/jobs/job.html
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/jobmanager/jobs/job.html 
b/zeppelin-web/src/app/jobmanager/jobs/job.html
index b4becee..e555771 100644
--- a/zeppelin-web/src/app/jobmanager/jobs/job.html
+++ b/zeppelin-web/src/app/jobmanager/jobs/job.html
@@ -15,89 +15,28 @@ limitations under the License.
 <div class="job" data-ng-init="init(notebookJob)">
   <div>
     <div ng-include src="'app/jobmanager/jobs/job-control.html'"></div>
-    <span
-      class="job-types"
-      ng-switch="notebookJob.noteType">
+    <span class="job-types"
+          ng-switch="notebookJob.noteType">
       <i ng-switch-when="normal" class="icon-doc"></i>
       <i ng-switch-when="cron" class="icon-clock"></i>
       <i ng-switch-default class="icon-question"></i>
     </span>
-    &nbsp;
     <a style="text-decoration: none !important;" 
ng-href="#/notebook/{{notebookJob.noteId}}">
-      <span>
-       {{notebookJob.noteName}}
-      </span>
-      <span>
-        &nbsp;-&nbsp;
-      </span>
-      <span>
-        <span ng-if="notebookJob.interpreter === undefined" style="color: 
orange">
-          Interpreter is not set
-        </span>
-        <span ng-if="notebookJob.interpreter !== undefined" style="color: 
gray">
-          {{notebookJob.interpreter}}
-        </span>
-      </span>
+      <span>{{notebookJob.noteName}} - </span>
+      <span ng-if="notebookJob.interpreter === undefined" style="color: gray;">
+        interpreter is not set</span>
+      <span ng-if="notebookJob.interpreter !== undefined" style="color: 
black;">
+        {{notebookJob.interpreter}}</span>
     </a>
     <div ng-include src="'app/jobmanager/jobs/job-progressBar.html'"></div>
   </div>
 
   <div>
-    <span
-      ng-repeat="paragraphJob in notebookJob.paragraphs"
-      ng-switch="paragraphJob.status">
-      <a ng-switch-when="READY"
-         style="text-decoration: none !important;"
+    <span ng-repeat="paragraphJob in notebookJob.paragraphs">
+      <a style="text-decoration: none !important;"
          
ng-href="#/notebook/{{notebookJob.noteId}}?paragraph={{paragraphJob.id}}">
-        <i style="color: green" class="fa fa-circle-o"
-           tooltip-placement="top-left"
-           uib-tooltip="{{paragraphJob.name}} is READY">
-        </i>
-      </a>
-      <a ng-switch-when="FINISHED"
-         style="text-decoration: none !important;"
-         
ng-href="#/notebook/{{notebookJob.noteId}}?paragraph={{paragraphJob.id}}">
-        <i style="color: green" class="fa fa-circle"
-           tooltip-placement="top-left"
-           uib-tooltip="{{paragraphJob.name}} is FINISHED">
-        </i>
-      </a>
-      <a ng-switch-when="ABORT"
-         style="text-decoration: none !important;"
-         
ng-href="#/notebook/{{notebookJob.noteId}}?paragraph={{paragraphJob.id}}">
-        <i style="color: orange" class="fa fa-circle"
-           tooltip-placement="top-left"
-           uib-tooltip="{{paragraphJob.name}} is ABORT">
-        </i>
-      </a>
-      <a ng-switch-when="ERROR"
-         style="text-decoration: none !important;"
-         
ng-href="#/notebook/{{notebookJob.noteId}}?paragraph={{paragraphJob.id}}">
-        <i style="color: red" class="fa fa-circle"
-           tooltip-placement="top-left"
-           uib-tooltip="{{paragraphJob.name}} is ERROR">
-        </i>
-      </a>
-      <a ng-switch-when="PENDING"
-         style="text-decoration: none !important;"
-         
ng-href="#/notebook/{{notebookJob.noteId}}?paragraph={{paragraphJob.id}}">
-        <i style="color: gray" class="fa fa-circle"
-           tooltip-placement="top-left"
-           uib-tooltip="{{paragraphJob.name}} is PENDING">
-        </i>
-      </a>
-      <a ng-switch-when="RUNNING"
-         style="text-decoration: none !important;"
-         
ng-href="#/notebook/{{notebookJob.noteId}}?paragraph={{paragraphJob.id}}">
-        <i style="color: blue" class="fa fa-spinner spinAnimation"
-           tooltip-placement="top-left"
-           uib-tooltip="{{paragraphJob.name}} is RUNNING">
-        </i>
-      </a>
-      <a ng-switch-default class="icon-question"
-         style="text-decoration: none !important;"
-         
ng-href="#/notebook/{{notebookJob.noteId}}?paragraph={{paragraphJob.id}}">
-        <i class="icon-question"
+        <i ng-style="{'color': 
$parent.getJobColorByStatus(paragraphJob.status)}"
+           ng-class="$parent.getJobIconByStatus(paragraphJob.status)"
            tooltip-placement="top-left"
            uib-tooltip="{{paragraphJob.name}} is {{paragraphJob.status}}">
         </i>

Reply via email to