Repository: zeppelin
Updated Branches:
  refs/heads/master f2c2941cf -> 90b3be5b6


[ZEPPELIN-2779] Unit test for job module (zeppelin-web)

### What is this PR for?

Added few test cases for the `job` module under `zeppelin-web/`

Additionally,

- removed lodash, q dependency
- converted to `JobModule`
- refactored `jobmanager/*`

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

### What is the Jira issue?

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

### How should this be tested?

1. cd `zeppelin-web`
2. `yarn install` (or `npm install`)
3. `yarn run test` (or `npm run test`)

The test should pass.

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

Author: 1ambda <1am...@gmail.com>

Closes #2497 from 1ambda/ZEPPELIN-2779/add-unit-test-for-job-page and squashes 
the following commits:

0c410c5c [1ambda] fix: lint error
232c406a [1ambda] test: Add jobmanager.service.test
ae3b79a4 [1ambda] fix: Use uppercase for filter
2e12c211 [1ambda] refactor: Move  to service
578aa9fb [1ambda] fix: DO NOT display loading spin always
2bea70a3 [1ambda] refactor: Move http related actions into srv
8c2d32fa [1ambda] fix: Removed the duplicated test
2c0bd0e6 [1ambda] fix: Add comments
929b00c6 [1ambda] fix: DON'T import job.css in index.html
d264a52a [1ambda] fix: Remove q dependency
c58c6ddb [1ambda] test: job.component.test.js
dce44509 [1ambda] refactor: Remove ng-init
40b6cf72 [1ambda] fix: Remove unused var
609c42c5 [1ambda] refactor: Define job releated filter in jobmanager.js
6074c6c6 [1ambda] refactor: Rename variables
a6618b8f [1ambda] fix: Remove lodash dependency in jobmanager
22b15a8d [1ambda] fix: lint errors in /job
cc452b98 [1ambda] fix: Use a seperate HTML file for job
bb334e08 [1ambda] test: Add job.component.test.js
6d8b1a70 [1ambda] fix: Create job.component.js
e0b102d3 [1ambda] refactor: rename jobs to job
dd4598b6 [1ambda] fix: Use the word 'Paragraph' instead of 'Job'
3ed24460 [1ambda] fix: Remove lodash dep in job.controller.js
b34a3762 [1ambda] refactor: job.controller.js
52634bd0 [1ambda] refactor: job.html
99420f86 [1ambda] fix: Remove job-status
959d91c3 [1ambda] fix: Remove job-progress-bar.html
3b21fc83 [1ambda] fix: Refactor job-progress-bar
2b908938 [1ambda] fix: Remove job-control.html
eae50011 [1ambda] refactor: Use ng-bind, remove useless ng-if


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

Branch: refs/heads/master
Commit: 90b3be5b6b9c9b1664a8feafff578c7f57216059
Parents: f2c2941
Author: 1ambda <1am...@gmail.com>
Authored: Sat Jul 22 01:49:45 2017 +0900
Committer: 1ambda <1am...@gmail.com>
Committed: Tue Jul 25 11:00:16 2017 +0900

----------------------------------------------------------------------
 zeppelin-web/src/app/jobmanager/job-status.js   |  54 +++++
 .../src/app/jobmanager/job/job.component.js     | 160 ++++++++++++++
 .../app/jobmanager/job/job.component.test.js    |  63 ++++++
 zeppelin-web/src/app/jobmanager/job/job.css     | 123 +++++++++++
 zeppelin-web/src/app/jobmanager/job/job.html    |  69 +++++++
 .../src/app/jobmanager/jobmanager.component.js  | 186 +++++++++++++++++
 .../app/jobmanager/jobmanager.component.test.js |  26 +++
 .../src/app/jobmanager/jobmanager.controller.js | 207 -------------------
 .../src/app/jobmanager/jobmanager.filter.js     |  37 ++--
 zeppelin-web/src/app/jobmanager/jobmanager.html |  44 ++--
 .../src/app/jobmanager/jobmanager.service.js    |  64 ++++++
 .../app/jobmanager/jobmanager.service.test.js   |  65 ++++++
 .../src/app/jobmanager/jobs/job-control.html    |  42 ----
 .../app/jobmanager/jobs/job-progress-bar.html   |  22 --
 .../src/app/jobmanager/jobs/job-status.js       |  22 --
 .../src/app/jobmanager/jobs/job.controller.js   | 110 ----------
 zeppelin-web/src/app/jobmanager/jobs/job.css    | 118 -----------
 zeppelin-web/src/app/jobmanager/jobs/job.html   |  46 -----
 .../websocket/websocket-event.factory.js        |   4 +-
 .../websocket/websocket-message.service.js      |  10 +-
 zeppelin-web/src/index.html                     |   1 -
 zeppelin-web/src/index.js                       |   4 +-
 22 files changed, 859 insertions(+), 618 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/zeppelin/blob/90b3be5b/zeppelin-web/src/app/jobmanager/job-status.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/jobmanager/job-status.js 
b/zeppelin-web/src/app/jobmanager/job-status.js
new file mode 100644
index 0000000..eda41b1
--- /dev/null
+++ b/zeppelin-web/src/app/jobmanager/job-status.js
@@ -0,0 +1,54 @@
+/*
+ * 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',
+}
+
+export function getJobIconByStatus(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'
+  }
+}
+
+export function getJobColorByStatus(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'
+  }
+}

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/90b3be5b/zeppelin-web/src/app/jobmanager/job/job.component.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/jobmanager/job/job.component.js 
b/zeppelin-web/src/app/jobmanager/job/job.component.js
new file mode 100644
index 0000000..c4d4f51
--- /dev/null
+++ b/zeppelin-web/src/app/jobmanager/job/job.component.js
@@ -0,0 +1,160 @@
+/*
+ * 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.
+ */
+
+import moment from 'moment'
+
+import { ParagraphStatus, } from '../../notebook/paragraph/paragraph.status'
+import { getJobColorByStatus, getJobIconByStatus } from '../job-status'
+
+import jobTemplate from './job.html'
+import './job.css'
+
+class JobController {
+  constructor($http, JobManagerService) {
+    'ngInject'
+    this.$http = $http
+    this.JobManagerService = JobManagerService
+  }
+
+  isRunning() {
+    return this.note.isRunningJob
+  }
+
+  getParagraphs() {
+    return this.note.paragraphs
+  }
+
+  getNoteId() {
+    return this.note.noteId
+  }
+
+  getNoteName() {
+    return this.note.noteName
+  }
+
+  runJob() {
+    BootstrapDialog.confirm({
+      closable: true,
+      title: 'Job Dialog',
+      message: 'Run all paragraphs?',
+      callback: clickOk => {
+        if (!clickOk) { return }
+
+        const noteId = this.getNoteId()
+        // if the request is handled successfully, the job page will get 
updated using websocket
+        this.JobManagerService.sendRunJobRequest(noteId)
+          .catch(response => {
+            let message = (response.data && response.data.message)
+              ? response.data.message : 'SERVER ERROR'
+            this.showErrorDialog('Execution Failure', message)
+          })
+      }
+    })
+  }
+
+  stopJob() {
+    BootstrapDialog.confirm({
+      closable: true,
+      title: 'Job Dialog',
+      message: 'Stop all paragraphs?',
+      callback: clickOk => {
+        if (!clickOk) { return }
+
+        const noteId = this.getNoteId()
+        // if the request is handled successfully, the job page will get 
updated using websocket
+        this.JobManagerService.sendStopJobRequest(noteId)
+          .catch(response => {
+            let message = (response.data && response.data.message)
+              ? response.data.message : 'SERVER ERROR'
+            this.showErrorDialog('Stop Failure', message)
+          })
+      }
+    })
+  }
+
+  showErrorDialog(title, errorMessage) {
+    if (!errorMessage) { errorMessage = 'SERVER ERROR' }
+    BootstrapDialog.alert({
+      closable: true,
+      title: title,
+      message: errorMessage
+    })
+  }
+
+  lastExecuteTime() {
+    const timestamp = this.note.unixTimeLastRun
+    return moment.unix(timestamp / 1000).fromNow()
+  }
+
+  getInterpreterName() {
+    return typeof this.note.interpreter === 'undefined'
+      ? 'interpreter is not set' : this.note.interpreter
+  }
+
+  getInterpreterNameStyle() {
+    return typeof this.note.interpreter === 'undefined'
+      ? { color: 'gray' } : { color: 'black' }
+  }
+
+  getJobTypeIcon() {
+    const noteType = this.note.noteType
+    if (noteType === 'normal') {
+      return 'icon-doc'
+    } else if (noteType === 'cron') {
+      return 'icon-clock'
+    } else {
+      return 'icon-question'
+    }
+  }
+
+  getJobColorByStatus(status) {
+    return getJobColorByStatus(status)
+  }
+
+  getJobIconByStatus(status) {
+    return getJobIconByStatus(status)
+  }
+
+  getProgress() {
+    const paragraphs = this.getParagraphs()
+    let paragraphStatuses = paragraphs.map(p => p.status)
+    let runningOrFinishedParagraphs = paragraphStatuses.filter(status => {
+      return status === ParagraphStatus.RUNNING || status === 
ParagraphStatus.FINISHED
+    })
+
+    let totalCount = paragraphStatuses.length
+    let runningCount = runningOrFinishedParagraphs.length
+    let result = Math.ceil(runningCount / totalCount * 100)
+    result = isNaN(result) ? 0 : result
+
+    return `${result}%`
+  }
+
+  showPercentProgressBar() {
+    return this.getProgress() > 0 && this.getProgress() < 100
+  }
+}
+
+export const JobComponent = {
+  bindings: {
+    note: '<',
+  },
+  template: jobTemplate,
+  controller: JobController,
+}
+
+export const JobModule = angular
+  .module('zeppelinWebApp')
+  .component('job', JobComponent)
+  .name

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/90b3be5b/zeppelin-web/src/app/jobmanager/job/job.component.test.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/jobmanager/job/job.component.test.js 
b/zeppelin-web/src/app/jobmanager/job/job.component.test.js
new file mode 100644
index 0000000..6ca285c
--- /dev/null
+++ b/zeppelin-web/src/app/jobmanager/job/job.component.test.js
@@ -0,0 +1,63 @@
+import { ParagraphStatus } from '../../notebook/paragraph/paragraph.status'
+
+describe('JobComponent', () => {
+  let $componentController
+
+  beforeEach(angular.mock.module('zeppelinWebApp'))
+  beforeEach(angular.mock.inject((_$componentController_) => {
+    $componentController = _$componentController_
+  }))
+
+  it('should get progress when there is a finished paragraph', () => {
+    const paragraphs = [
+      { status: ParagraphStatus.FINISHED },
+    ]
+    const mockNote = createMockNote(paragraphs)
+    const bindings = { note: mockNote, }
+
+    const ctrl = $componentController('job', null, bindings)
+    expect(ctrl).toBeDefined()
+
+    const progress1 = ctrl.getProgress()
+    expect(progress1).toBe('100%')
+  })
+
+  it('should get progress when there is pending and finished paragraphs', () 
=> {
+    const paragraphs = [
+      { status: ParagraphStatus.PENDING },
+      { status: ParagraphStatus.FINISHED},
+    ]
+    const mockNote = createMockNote(paragraphs)
+    const bindings = { note: mockNote, }
+
+    const ctrl = $componentController('job', null, bindings)
+
+    const progress1 = ctrl.getProgress()
+    expect(progress1).toBe('50%')
+  })
+
+  it('should get proper job type icons', () => {
+    const paragraphs = [ { status: ParagraphStatus.PENDING }, ]
+    const mockNote = createMockNote(paragraphs)
+    const bindings = { note: mockNote, }
+
+    const ctrl = $componentController('job', null, bindings)
+
+    let icon = ctrl.getJobTypeIcon()
+    expect(icon).toBe('icon-doc')
+
+    mockNote.noteType = 'cron'
+    icon = ctrl.getJobTypeIcon()
+    expect(icon).toBe('icon-clock')
+  })
+
+  function createMockNote(paragraphs) {
+    return {
+      isRunningJob: false,
+      paragraphs: paragraphs,
+      noteId: 'NT01',
+      noteName: 'TestNote01',
+      noteType: 'normal',
+    }
+  }
+})

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/90b3be5b/zeppelin-web/src/app/jobmanager/job/job.css
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/jobmanager/job/job.css 
b/zeppelin-web/src/app/jobmanager/job/job.css
new file mode 100644
index 0000000..5bcbaea
--- /dev/null
+++ b/zeppelin-web/src/app/jobmanager/job/job.css
@@ -0,0 +1,123 @@
+/*
+ * 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.
+ */
+
+/*
+  job Style
+*/
+
+.job-space {
+  margin-bottom: 5px !important;
+  padding: 10px 10px 10px 10px !important;
+  min-height: 30px;
+}
+
+.job-margin {
+  margin-right: 2px;
+  margin-left: 2px;
+}
+
+.job-types i {
+  font-weight: bold;
+  font-size: 10px;
+}
+
+
+/*
+  job Controls CSS
+*/
+
+.job .runControl {
+  font-size: 1px;
+  color: #AAAAAA;
+  height:4px;
+  margin: 0px 0px 0px 0px;
+}
+
+.job .runControl .progress {
+  position: relative;
+  width: 100%;
+  height: 4px;
+  z-index: 100;
+  border-radius: 0;
+}
+
+.job .control span {
+  margin-left: 4px;
+}
+
+.job .control {
+  background: rgba(255,255,255,0.85);
+  float: right;
+  color: #999;
+  margin-top: 1px;
+  margin-right: 5px;
+  position: absolute;
+  clear: both;
+  right: 15px;
+  text-align: right;
+  font-size: 12px;
+  padding: 4px;
+}
+
+.job .control li {
+  font-size: 12px;
+  margin-bottom: 4px;
+  color: #333333;
+}
+
+.job .control .tooltip {
+  z-index: 10003;
+}
+
+.job .control .job-control-btn {
+  cursor: pointer;
+  color: #3071A9;
+}
+
+@-webkit-keyframes spinnerRotateAnimation
+{
+  from{-webkit-transform:rotate(0deg);}
+  to{-webkit-transform:rotate(360deg);}
+}
+@-moz-keyframes spinnerRotateAnimation
+{
+  from{-moz-transform:rotate(0deg);}
+  to{-moz-transform:rotate(360deg);}
+}
+@-ms-keyframes spinnerRotateAnimation
+{
+  from{-ms-transform:rotate(0deg);}
+  to{-ms-transform:rotate(360deg);}
+}
+
+@keyframes spinnerRotateAnimation {
+  from {transform: rotate(0deg);}
+  to{transform: rotate(360deg);}
+}
+
+.spinAnimation{
+  -webkit-animation-name: spinnerRotateAnimation;
+  -webkit-animation-duration: 1s;
+  -webkit-animation-iteration-count: infinite;
+  -webkit-animation-timing-function: linear;
+  -moz-animation-name: spinnerRotateAnimation;
+  -moz-animation-duration: 1s;
+  -moz-animation-iteration-count: infinite;
+  -moz-animation-timing-function: linear;
+  -ms-animation-name: spinnerRotateAnimation;
+  -ms-animation-duration: 1s;
+  -ms-animation-iteration-count: infinite;
+  -ms-animation-timing-function: linear;
+}
+

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/90b3be5b/zeppelin-web/src/app/jobmanager/job/job.html
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/jobmanager/job/job.html 
b/zeppelin-web/src/app/jobmanager/job/job.html
new file mode 100644
index 0000000..f3d2450
--- /dev/null
+++ b/zeppelin-web/src/app/jobmanager/job/job.html
@@ -0,0 +1,69 @@
+<!--
+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.
+-->
+
+<div class="job" >
+  <div>
+    <!-- job control: start -->
+    <div id="{{$ctrl.getNoteId()}}_control" class="control">
+      <span ng-bind="$ctrl.lastExecuteTime()"></span>
+      <span>
+        <span ng-bind="$ctrl.isRunning() ? 'RUNNING' : 'READY'"></span>
+      </span>
+
+      <span ng-if="$ctrl.isRunning()" ng-bind="$ctrl.getProgress()"></span>
+
+      <span
+        class="job-control-btn" tooltip-placement="left"
+        uib-tooltip-html="!$ctrl.isRunning() ? 'Start All Paragraphs' : 'Stop 
All Paragraphs'"
+        ng-click="!$ctrl.isRunning() ? $ctrl.runJob() : $ctrl.stopJob()"
+        ng-class="!$ctrl.isRunning() ? 'icon-control-play' : 
'icon-control-pause'">
+      </span>
+    </div>
+    <!-- job control: end -->
+
+    <span class="job-types">
+      <i ng-class="$ctrl.getJobTypeIcon()"></i>
+    </span>
+    <a style="text-decoration: none !important;" 
ng-href="#/notebook/{{$ctrl.getNoteId()}}">
+      <span ng-bind="$ctrl.getNoteName() + ' - '"></span>
+      <span ng-style="$ctrl.getInterpreterNameStyle()"
+            ng-bind="$ctrl.getInterpreterName()">
+      </span>
+    </a>
+    <!-- job progress bar: start -->
+    <div id="{{$ctrl.getNoteId()}}_runControl" class="runControl">
+      <div id="{{$ctrl.getNoteId()}}_progress" class="progress" 
ng-if="$ctrl.isRunning() === true">
+        <div class="progress-bar" role="progressbar"
+             ng-style="$ctrl.showPercentProgressBar() ? { 'width': 
$ctrl.getProgress() } : { 'width': '100%' }"
+             ng-class="$ctrl.showPercentProgressBar() ? '' : 
'progress-bar-striped active'">
+        </div>
+      </div>
+    </div>
+    <!-- job progress bar: end -->
+  </div>
+
+  <div>
+    <span ng-repeat="paragraph in $ctrl.getParagraphs()">
+      <a style="text-decoration: none !important;"
+         ng-href="#/notebook/{{$ctrl.getNoteId()}}?paragraph={{paragraph.id}}">
+        <i ng-style="{'color': $ctrl.getJobColorByStatus(paragraph.status)}"
+           ng-class="$ctrl.getJobIconByStatus(paragraph.status)"
+           tooltip-placement="top-left"
+           uib-tooltip="{{paragraph.name}} is {{paragraph.status}}">
+        </i>
+      </a>
+    </span>
+  </div>
+</div>
+

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/90b3be5b/zeppelin-web/src/app/jobmanager/jobmanager.component.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/jobmanager/jobmanager.component.js 
b/zeppelin-web/src/app/jobmanager/jobmanager.component.js
new file mode 100644
index 0000000..364cc45
--- /dev/null
+++ b/zeppelin-web/src/app/jobmanager/jobmanager.component.js
@@ -0,0 +1,186 @@
+/*
+ * 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.
+ */
+
+import './job/job.component'
+import { JobManagerFilter } from './jobmanager.filter'
+import { JobManagerService} from './jobmanager.service'
+
+import { getJobIconByStatus, getJobColorByStatus } from './job-status'
+
+angular.module('zeppelinWebApp')
+  .controller('JobManagerCtrl', JobManagerController)
+  .filter('JobManager', JobManagerFilter)
+  .service('JobManagerService', JobManagerService)
+
+const JobDateSorter = {
+  RECENTLY_UPDATED: 'Recently Update',
+  OLDEST_UPDATED: 'Oldest Updated',
+}
+
+function JobManagerController($scope, ngToast, JobManagerFilter, 
JobManagerService) {
+  'ngInject'
+
+  $scope.isFilterLoaded = false
+  $scope.jobs = []
+  $scope.sorter = {
+    availableDateSorter: Object.keys(JobDateSorter).map(key => { return 
JobDateSorter[key] }),
+    currentDateSorter: JobDateSorter.RECENTLY_UPDATED,
+  }
+  $scope.filteredJobs = $scope.jobs
+  $scope.filterConfig = {
+    isRunningAlwaysTop: true,
+    noteNameFilterValue: '',
+    interpreterFilterValue: '*',
+    isSortByAsc: true,
+  }
+
+  $scope.pagination = {
+    currentPage: 1,
+    itemsPerPage: 10,
+    maxPageCount: 5,
+  }
+
+  ngToast.dismiss()
+  init()
+
+  /** functions */
+
+  $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))
+  }
+
+  let asyncNotebookJobFilter = function (jobs, filterConfig) {
+    return new Promise((resolve, reject) => {
+      $scope.filteredJobs = JobManagerFilter(jobs, filterConfig)
+      resolve($scope.filteredJobs)
+    })
+  }
+
+  $scope.$watch('sorter.currentDateSorter', function() {
+    $scope.filterConfig.isSortByAsc =
+      $scope.sorter.currentDateSorter === JobDateSorter.OLDEST_UPDATED
+    asyncNotebookJobFilter($scope.jobs, $scope.filterConfig)
+  })
+
+  $scope.getJobIconByStatus = getJobIconByStatus
+  $scope.getJobColorByStatus = getJobColorByStatus
+
+  $scope.filterJobs = function (jobs, filterConfig) {
+    asyncNotebookJobFilter(jobs, filterConfig)
+      .then(() => {
+        $scope.isFilterLoaded = true
+      })
+      .catch(error => {
+        console.error('Failed to search jobs from server', error)
+      })
+  }
+
+  $scope.filterValueToName = function (filterValue, maxStringLength) {
+    if (typeof $scope.defaultInterpreters === 'undefined') {
+      return
+    }
+
+    let index = $scope.defaultInterpreters.findIndex(intp => intp.value === 
filterValue)
+    if (typeof $scope.defaultInterpreters[index].name !== 'undefined') {
+      if (typeof maxStringLength !== 'undefined' &&
+        maxStringLength > $scope.defaultInterpreters[index].name) {
+        return $scope.defaultInterpreters[index].name.substr(0, 
maxStringLength - 3) + '...'
+      }
+      return $scope.defaultInterpreters[index].name
+    } else {
+      return 'NONE'
+    }
+  }
+
+  $scope.setFilterValue = function (filterValue) {
+    $scope.filterConfig.interpreterFilterValue = filterValue
+    $scope.filterJobs($scope.jobs, $scope.filterConfig)
+  }
+
+  $scope.setJobs = function(jobs) {
+    $scope.jobs = jobs
+    let interpreters = $scope.jobs
+      .filter(j => typeof j.interpreter !== 'undefined')
+      .map(j => j.interpreter)
+    interpreters = [...new Set(interpreters)] // remove duplicated interpreters
+
+    $scope.defaultInterpreters = [ { name: 'ALL', value: '*' } ]
+    for (let i = 0; i < interpreters.length; i++) {
+      $scope.defaultInterpreters.push({ name: interpreters[i], value: 
interpreters[i] })
+    }
+  }
+
+  function init() {
+    JobManagerService.getJobs()
+    JobManagerService.subscribeSetJobs($scope, setJobsCallback)
+    JobManagerService.subscribeUpdateJobs($scope, updateJobsCallback)
+
+    $scope.$on('$destroy', function () {
+      JobManagerService.disconnect()
+    })
+  }
+
+  /*
+   ** $scope.$on functions below
+   */
+
+  function setJobsCallback(event, response) {
+    const jobs = response.jobs
+    $scope.setJobs(jobs)
+    $scope.filterJobs($scope.jobs, $scope.filterConfig)
+  }
+
+  function updateJobsCallback(event, response) {
+    let jobs = $scope.jobs
+    let jobByNoteId = jobs.reduce((acc, j) => {
+      const noteId = j.noteId
+      acc[noteId] = j
+      return acc
+    }, {})
+
+    let updatedJobs = response.jobs
+    updatedJobs.map(updatedJob => {
+      if (typeof jobByNoteId[updatedJob.noteId] === 'undefined') {
+        let newItem = angular.copy(updatedJob)
+        jobs.push(newItem)
+        jobByNoteId[updatedJob.noteId] = newItem
+      } else {
+        let job = jobByNoteId[updatedJob.noteId]
+
+        if (updatedJob.isRemoved === true) {
+          delete jobByNoteId[updatedJob.noteId]
+          let removeIndex = jobs.findIndex(j => j.noteId === updatedJob.noteId)
+          if (removeIndex) {
+            jobs.splice(removeIndex, 1)
+          }
+        } else {
+          // update the job
+          job.isRunningJob = updatedJob.isRunningJob
+          job.noteName = updatedJob.noteName
+          job.noteType = updatedJob.noteType
+          job.interpreter = updatedJob.interpreter
+          job.unixTimeLastRun = updatedJob.unixTimeLastRun
+          job.paragraphs = updatedJob.paragraphs
+        }
+      }
+    })
+    $scope.filterJobs(jobs, $scope.filterConfig)
+  }
+}

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/90b3be5b/zeppelin-web/src/app/jobmanager/jobmanager.component.test.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/jobmanager/jobmanager.component.test.js 
b/zeppelin-web/src/app/jobmanager/jobmanager.component.test.js
new file mode 100644
index 0000000..a4b858b
--- /dev/null
+++ b/zeppelin-web/src/app/jobmanager/jobmanager.component.test.js
@@ -0,0 +1,26 @@
+describe('JobManagerComponent', () => {
+  let $scope
+  let $controller
+
+  beforeEach(angular.mock.module('zeppelinWebApp'))
+  beforeEach(angular.mock.inject((_$rootScope_, _$controller_) => {
+    $scope = _$rootScope_.$new()
+    $controller = _$controller_
+  }))
+
+  it('should set jobs using `setJobs`', () => {
+    let ctrl = $controller('JobManagerCtrl', { $scope: $scope, })
+    expect(ctrl).toBeDefined()
+
+    const mockJobs = [
+      { noteId: 'TN01', interpreter: 'spark', },
+      { noteId: 'TN02', interpreter: 'spark', },
+    ]
+
+    $scope.setJobs(mockJobs)
+    expect($scope.defaultInterpreters).toEqual([
+      { name: 'ALL', value: '*', },
+      { name: 'spark', value: 'spark', },
+    ])
+  })
+})

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/90b3be5b/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
deleted file mode 100644
index 16d47ba..0000000
--- a/zeppelin-web/src/app/jobmanager/jobmanager.controller.js
+++ /dev/null
@@ -1,207 +0,0 @@
-/*
- * 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.
- */
-
-import { JobStatus, } from './jobs/job-status'
-
-angular.module('zeppelinWebApp')
-  .controller('JobManagerCtrl', JobManagerCtrl)
-
-const JobDateSorter = {
-  RECENTLY_UPDATED: 'Recently Update',
-  OLDEST_UPDATED: 'Oldest Updated',
-}
-
-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) {
-      $scope.JobInfomationsByFilter = $scope.jobTypeFilter(jobInfomations, 
filterConfig)
-      resolve($scope.JobInfomationsByFilter)
-    })
-  }
-
-  $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(
-        () => { $scope.isLoadingFilter = false },
-        (error) => {
-          console.error('Failed to search jobs from server', error)
-        }
-      )
-  }
-
-  $scope.filterValueToName = function (filterValue, maxStringLength) {
-    if ($scope.activeInterpreters === undefined) {
-      return
-    }
-    let index = _.findIndex($scope.activeInterpreters, {value: filterValue})
-    if ($scope.activeInterpreters[index].name !== undefined) {
-      if (maxStringLength !== undefined && maxStringLength > 
$scope.activeInterpreters[index].name) {
-        return $scope.activeInterpreters[index].name.substr(0, maxStringLength 
- 3) + '...'
-      }
-      return $scope.activeInterpreters[index].name
-    } else {
-      return 'NONE'
-    }
-  }
-
-  $scope.setFilterValue = function (filterValue) {
-    $scope.filterConfig.filterValueInterpreter = filterValue
-    $scope.doFiltering($scope.jobInfomations, $scope.filterConfig)
-  }
-
-  $scope.init = function () {
-    $scope.isLoadingFilter = true
-    $scope.jobInfomations = []
-    $scope.JobInfomationsByFilter = $scope.jobInfomations
-    $scope.filterConfig = {
-      isRunningAlwaysTop: true,
-      filterValueNotebookName: '',
-      filterValueInterpreter: '*',
-      isSortByAsc: $scope.sorter.currentDateSorter === 
JobDateSorter.OLDEST_UPDATED,
-    }
-    $scope.sortTooltipMsg = 'Switch to sort by desc'
-    $scope.jobTypeFilter = jobManagerFilter
-
-    websocketMsgSrv.getNoteJobsList()
-
-    $scope.$on('$destroy', function () {
-      websocketMsgSrv.unsubscribeJobManager()
-    })
-  }
-
-  /*
-   ** $scope.$on functions below
-   */
-
-  $scope.$on('setNoteJobs', function (event, responseData) {
-    $scope.lastJobServerUnixTime = responseData.lastResponseUnixTime
-    $scope.jobInfomations = responseData.jobs
-    $scope.jobInfomationsIndexs = $scope.jobInfomations ? 
_.indexBy($scope.jobInfomations, 'noteId') : {}
-    $scope.jobTypeFilter($scope.jobInfomations, $scope.filterConfig)
-    $scope.activeInterpreters = [
-      {
-        name: 'ALL',
-        value: '*'
-      }
-    ]
-    let interpreterLists = _.uniq(_.pluck($scope.jobInfomations, 
'interpreter'), false)
-    for (let index = 0, length = interpreterLists.length; index < length; 
index++) {
-      $scope.activeInterpreters.push({
-        name: interpreterLists[index],
-        value: interpreterLists[index]
-      })
-    }
-    $scope.doFiltering($scope.jobInfomations, $scope.filterConfig)
-  })
-
-  $scope.$on('setUpdateNoteJobs', function (event, responseData) {
-    let jobInfomations = $scope.jobInfomations
-    let indexStore = $scope.jobInfomationsIndexs
-    $scope.lastJobServerUnixTime = responseData.lastResponseUnixTime
-    let notes = responseData.jobs
-    notes.map(function (changedItem) {
-      if (indexStore[changedItem.noteId] === undefined) {
-        let newItem = angular.copy(changedItem)
-        jobInfomations.push(newItem)
-        indexStore[changedItem.noteId] = newItem
-      } else {
-        let changeOriginTarget = indexStore[changedItem.noteId]
-
-        if (changedItem.isRemoved !== undefined && changedItem.isRemoved === 
true) {
-          // remove Item.
-          let removeIndex = _.findIndex(indexStore, changedItem.noteId)
-          if (removeIndex > -1) {
-            indexStore.splice(removeIndex, 1)
-          }
-
-          removeIndex = _.findIndex(jobInfomations, {'noteId': 
changedItem.noteId})
-          if (removeIndex) {
-            jobInfomations.splice(removeIndex, 1)
-          }
-        } else {
-          // change value for item.
-          changeOriginTarget.isRunningJob = changedItem.isRunningJob
-          changeOriginTarget.noteName = changedItem.noteName
-          changeOriginTarget.noteType = changedItem.noteType
-          changeOriginTarget.interpreter = changedItem.interpreter
-          changeOriginTarget.unixTimeLastRun = changedItem.unixTimeLastRun
-          changeOriginTarget.paragraphs = changedItem.paragraphs
-        }
-      }
-    })
-    $scope.doFiltering(jobInfomations, $scope.filterConfig)
-  })
-}

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/90b3be5b/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 2b724a4..d6c8d69 100644
--- a/zeppelin-web/src/app/jobmanager/jobmanager.filter.js
+++ b/zeppelin-web/src/app/jobmanager/jobmanager.filter.js
@@ -12,36 +12,37 @@
  * limitations under the License.
  */
 
-angular.module('zeppelinWebApp').filter('jobManager', jobManagerFilter)
-
-function jobManagerFilter () {
-  function filterContext (jobItems, filterConfig) {
-    let filterValueInterpreter = filterConfig.filterValueInterpreter
-    let filterValueNotebookName = filterConfig.filterValueNotebookName
+export function JobManagerFilter() {
+  function filterContext (jobs, filterConfig) {
+    let interpreter = filterConfig.interpreterFilterValue
+    let noteName = filterConfig.noteNameFilterValue
     let isSortByAsc = filterConfig.isSortByAsc
-    let filterItems = jobItems
+    let filteredJobs = jobs
 
-    if (filterValueInterpreter === undefined) {
-      filterItems = filterItems.filter((jobItem) => {
-        return jobItem.interpreter === undefined
+    if (typeof interpreter === 'undefined') {
+      filteredJobs = filteredJobs.filter((jobItem) => {
+        return typeof jobItem.interpreter === 'undefined'
       })
-    } else if (filterValueInterpreter !== '*') {
-      filterItems = _.where(filterItems, {interpreter: filterValueInterpreter})
+    } else if (interpreter !== '*') {
+      filteredJobs = filteredJobs.filter(j => j.interpreter === interpreter)
     }
 
-    if (filterValueNotebookName !== '') {
-      filterItems = filterItems.filter((jobItem) => {
-        let lowerFilterValue = filterValueNotebookName.toLocaleLowerCase()
+    // filter by note name
+    if (noteName !== '') {
+      filteredJobs = filteredJobs.filter((jobItem) => {
+        let lowerFilterValue = noteName.toLocaleLowerCase()
         let lowerNotebookName = jobItem.noteName.toLocaleLowerCase()
         return lowerNotebookName.match(new RegExp('.*' + lowerFilterValue + 
'.*'))
       })
     }
 
-    filterItems = filterItems.sort((jobItem) => {
+    // sort by name
+    filteredJobs = filteredJobs.sort((jobItem) => {
       return jobItem.noteName.toLowerCase()
     })
 
-    filterItems = filterItems.sort((x, y) => {
+    // sort by timestamp
+    filteredJobs = filteredJobs.sort((x, y) => {
       if (isSortByAsc) {
         return x.unixTimeLastRun - y.unixTimeLastRun
       } else {
@@ -49,7 +50,7 @@ function jobManagerFilter () {
       }
     })
 
-    return filterItems
+    return filteredJobs
   }
   return filterContext
 }

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/90b3be5b/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 e5c030a..55ebb96 100644
--- a/zeppelin-web/src/app/jobmanager/jobmanager.html
+++ b/zeppelin-web/src/app/jobmanager/jobmanager.html
@@ -11,8 +11,7 @@ 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 id="job-manager-header" class="job-manager-header" data-ng-init="init()">
+<div id="job-manager-header" class="job-manager-header">
   <div class="header">
     <div class="row">
       <div class="col-md-12">
@@ -40,9 +39,9 @@ limitations under the License.
             <input class="form-control btn-xs"
                    placeholder="&#xf002 Search jobs..."
                    type="text"
-                   ng-model="filterConfig.filterValueNotebookName"
+                   ng-model="filterConfig.noteNameFilterValue"
                    ng-model-options="{ updateOn: 'default blur', debounce: { 
'default': 300, 'blur': 0 } }"
-                   ng-change="doFiltering(jobInfomations, filterConfig)" />
+                   ng-change="filterJobs(jobs, filterConfig)" />
           </div>
 
           <!-- search tool: default interpreter dropdown -->
@@ -50,17 +49,17 @@ limitations under the License.
                data-toggle="dropdown">
             <span>
               <span class="dropdown-text-desc">Interpreter: </span>
-              <span 
class="dropdown-text-value">{{filterValueToName(filterConfig.filterValueInterpreter)}}</span>
+              <span 
class="dropdown-text-value">{{filterValueToName(filterConfig.interpreterFilterValue)}}</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 ng-click="setFilterValue(interpreterOption.value)"
-                ng-style="(filterValueToName(interpreterOption.value) === 
'ALL' || filterValueToName(interpreterOption.value) === 'NONE') ? { 
'font-weight': 500 } : {}"
+           <li ng-repeat="interpreter in defaultInterpreters">
+             <a ng-click="setFilterValue(interpreter.value)"
+                ng-style="(filterValueToName(interpreter.value) === 'ALL' || 
filterValueToName(interpreter.value) === 'NONE') ? { 'font-weight': 500 } : {}"
                 class="dropdown-list-value">
-               {{filterValueToName(interpreterOption.value)}}
+               {{filterValueToName(interpreter.value)}}
              </a>
            </li>
           </ul>
@@ -78,7 +77,7 @@ limitations under the License.
             </span>
           </div>
           <ul class="dropdown-menu dropdown-menu-right 
search-tool-dropdown-content" role="menu">
-           <li ng-repeat="dateSorter in sorter.AvailableDateSorter">
+           <li ng-repeat="dateSorter in sorter.availableDateSorter">
              <a ng-click="setJobDateSorter(dateSorter)" 
class="dropdown-list-value">
                {{dateSorter}}
              </a>
@@ -87,7 +86,7 @@ limitations under the License.
         </span>
         <span class="job-counter">
           <span class="job-counter-label">Total: </span>
-          <span 
class="job-counter-value">{{JobInfomationsByFilter.length}}</span>
+          <span class="job-counter-value">{{filteredJobs.length}}</span>
         </span>
       </div>
     </div>
@@ -97,8 +96,9 @@ limitations under the License.
       <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}}
+             ng-style="{'color': getJobColorByStatus(jobStatus)}"
+             ng-class="getJobIconByStatus(jobStatus)" ></i>
+          {{jobStatus}}
         </span>
       </span>
     </div>
@@ -110,20 +110,20 @@ limitations under the License.
 
 <div>
   <div class="note-jump"></div>
-  <div ng-if="isLoadingFilter === true" class="paragraph-col">
+  <div ng-if="!isFilterLoaded" class="paragraph-col">
     <div class="job-space box job-margin text-center">
-      <i style="color: blue" class="fa fa-spinner spinAnimation"></i>Loading...
+      <i style="color: blue" class="fa fa-spinner spinAnimation"></i>
+      Loading...
     </div>
   </div>
-  <div ng-if="JobInfomationsByFilter.length > 0"
-       ng-repeat="notebookJob in getJobsInCurrentPage(JobInfomationsByFilter)"
+  <div ng-if="filteredJobs.length > 0"
+       ng-repeat="note in getJobsInCurrentPage(filteredJobs)"
        class="paragraph-col">
-    <div ng-include src="'app/jobmanager/jobs/job.html'"
-         class="job-space box job-margin"
-         ng-controller="JobCtrl">
+    <div class="job-space box job-margin">
+      <job note="note"></job>
     </div>
   </div>
-  <div ng-if="isLoadingFilter === false && JobInfomationsByFilter.length <= 0"
+  <div ng-if="isFilterLoaded === false && filteredJobs.length <= 0"
        class="paragraph-col">
     <div class="job-space box job-margin text-center">No Job found</div>
   </div>
@@ -131,7 +131,7 @@ limitations under the License.
   <!-- pagination -->
   <div class="job-pagination-container">
     <ul uib-pagination class="pagination-sm"
-        total-items="JobInfomationsByFilter.length"
+        total-items="filteredJobs.length"
         ng-model="pagination.currentPage"
         items-per-page="pagination.itemsPerPage"
         boundary-links="true" rotate="false"

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/90b3be5b/zeppelin-web/src/app/jobmanager/jobmanager.service.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/jobmanager/jobmanager.service.js 
b/zeppelin-web/src/app/jobmanager/jobmanager.service.js
new file mode 100644
index 0000000..603950f
--- /dev/null
+++ b/zeppelin-web/src/app/jobmanager/jobmanager.service.js
@@ -0,0 +1,64 @@
+/*
+ * 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 class JobManagerService {
+  constructor($http, $rootScope, baseUrlSrv, websocketMsgSrv) {
+    'ngInject'
+
+    this.$http = $http
+    this.$rootScope = $rootScope
+    this.BaseUrlService = baseUrlSrv
+    this.WebsocketMessageService = websocketMsgSrv
+  }
+
+  sendStopJobRequest(noteId) {
+    const apiURL = this.BaseUrlService.getRestApiBase() + 
`/notebook/job/${noteId}`
+    return this.$http({ method: 'DELETE', url: apiURL, })
+  }
+
+  sendRunJobRequest(noteId) {
+    const apiURL = this.BaseUrlService.getRestApiBase() + 
`/notebook/job/${noteId}`
+    return this.$http({ method: 'POST', url: apiURL, })
+  }
+
+  getJobs() {
+    this.WebsocketMessageService.getJobs()
+  }
+
+  disconnect() {
+    this.WebsocketMessageService.disconnectJobEvent()
+  }
+
+  subscribeSetJobs(controllerScope, receiveCallback) {
+    const event = 'jobmanager:set-jobs'
+    console.log(`(Event) Subscribed: ${event}`)
+    const unsubscribeHandler = this.$rootScope.$on(event, receiveCallback)
+
+    controllerScope.$on('$destroy', () => {
+      console.log(`(Event) Unsubscribed: ${event}`)
+      unsubscribeHandler()
+    })
+  }
+
+  subscribeUpdateJobs(controllerScope, receiveCallback) {
+    const event = 'jobmanager:update-jobs'
+    console.log(`(Event) Subscribed: ${event}`)
+    const unsubscribeHandler = this.$rootScope.$on(event, receiveCallback)
+
+    controllerScope.$on('$destroy', () => {
+      console.log(`(Event) Unsubscribed: ${event}`)
+      unsubscribeHandler()
+    })
+  }
+}

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/90b3be5b/zeppelin-web/src/app/jobmanager/jobmanager.service.test.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/jobmanager/jobmanager.service.test.js 
b/zeppelin-web/src/app/jobmanager/jobmanager.service.test.js
new file mode 100644
index 0000000..fbb0829
--- /dev/null
+++ b/zeppelin-web/src/app/jobmanager/jobmanager.service.test.js
@@ -0,0 +1,65 @@
+import { ParagraphStatus } from '../notebook/paragraph/paragraph.status'
+import { JobManagerService } from './jobmanager.service'
+
+describe('JobManagerService', () => {
+  const baseUrlSrvMock = { getRestApiBase: () => '' }
+  let service
+  let $httpBackend
+
+  beforeEach(angular.mock.module('zeppelinWebApp'))
+  beforeEach(angular.mock.inject((_$rootScope_, _$httpBackend_, _$http_, 
_websocketMsgSrv_) => {
+    $httpBackend = _$httpBackend_
+    service = new JobManagerService(_$http_, _$rootScope_, baseUrlSrvMock, 
_websocketMsgSrv_)
+  }))
+
+  it('should sent valid request to run a job', () => {
+    const paragraphs = [ { status: ParagraphStatus.PENDING }, ]
+    const mockNote = createMockNote(paragraphs)
+
+    const noteId = mockNote.noteId
+    service.sendRunJobRequest(noteId)
+
+    const url = `/notebook/job/${noteId}`
+
+    $httpBackend
+      .when('POST', url)
+      .respond(200, { /** return nothing */ })
+    $httpBackend.expectPOST(url)
+    $httpBackend.flush()
+
+    checkUnknownHttpRequests()
+  })
+
+  it('should sent valid request to stop a job', () => {
+    const paragraphs = [ { status: ParagraphStatus.PENDING }, ]
+    const mockNote = createMockNote(paragraphs)
+
+    const noteId = mockNote.noteId
+    service.sendStopJobRequest(noteId)
+
+    const url = `/notebook/job/${noteId}`
+
+    $httpBackend
+      .when('DELETE', url)
+      .respond(200, { /** return nothing */ })
+    $httpBackend.expectDELETE(url)
+    $httpBackend.flush()
+
+    checkUnknownHttpRequests()
+  })
+
+  function checkUnknownHttpRequests() {
+    $httpBackend.verifyNoOutstandingExpectation()
+    $httpBackend.verifyNoOutstandingRequest()
+  }
+
+  function createMockNote(paragraphs) {
+    return {
+      isRunningJob: false,
+      paragraphs: paragraphs,
+      noteId: 'NT01',
+      noteName: 'TestNote01',
+      noteType: 'normal',
+    }
+  }
+})

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/90b3be5b/zeppelin-web/src/app/jobmanager/jobs/job-control.html
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/jobmanager/jobs/job-control.html 
b/zeppelin-web/src/app/jobmanager/jobs/job-control.html
deleted file mode 100644
index b23f8da..0000000
--- a/zeppelin-web/src/app/jobmanager/jobs/job-control.html
+++ /dev/null
@@ -1,42 +0,0 @@
-<!--
-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.
--->
-
-<div id="{{notebookJob.noteId}}_control" class="control">
-  <span>
-    {{lastExecuteTime(notebookJob.unixTimeLastRun)}}
-  </span>
-  <span>
-    <span ng-if="notebookJob.isRunningJob">
-      RUNNING
-    </span>
-    <span ng-if="!notebookJob.isRunningJob">
-      READY
-    </span>
-  </span>
-
-  <span ng-if="notebookJob.isRunningJob">
-    {{getProgress()}}%
-  </span>
-  <!-- Run / Cancel button -->
-  <span
-    ng-if="!notebookJob.isRunningJob"
-    class="icon-control-play" style="cursor:pointer;color:#3071A9" 
tooltip-placement="left" uib-tooltip="START ALL Job"
-    ng-click="runNotebookJob(notebookJob.noteId)">
-  </span>
-  <span
-    ng-if="notebookJob.isRunningJob"
-    class="icon-control-pause" style="cursor:pointer;color:#3071A9" 
tooltip-placement="left" uib-tooltip="STOP ALL Job"
-    ng-click="stopNotebookJob(notebookJob.noteId)">
-  </span>
-</div>

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/90b3be5b/zeppelin-web/src/app/jobmanager/jobs/job-progress-bar.html
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/jobmanager/jobs/job-progress-bar.html 
b/zeppelin-web/src/app/jobmanager/jobs/job-progress-bar.html
deleted file mode 100644
index 00b290b..0000000
--- a/zeppelin-web/src/app/jobmanager/jobs/job-progress-bar.html
+++ /dev/null
@@ -1,22 +0,0 @@
-<!--
-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.
--->
-
-<div id="{{notebookJob.noteId}}_runControl" class="runControl">
-  <div id="{{notebookJob.noteId}}_progress" class="progress" 
ng-if="notebookJob.isRunningJob === true">
-      <div ng-if="getProgress()>0 && getProgress()<100 && 
notebookJob.isRunningJob === true"
-        class="progress-bar" role="progressbar" 
ng-style="{width:getProgress()+'%'}"></div>
-      <div ng-if="(getProgress()<=0 || getProgress()>=100) && 
(notebookJob.isRunningJob === true)"
-          class="progress-bar progress-bar-striped active" role="progressbar" 
style="width:100%;"></div>
-  </div>
-</div>

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/90b3be5b/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
deleted file mode 100644
index fa14637..0000000
--- a/zeppelin-web/src/app/jobmanager/jobs/job-status.js
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * 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/90b3be5b/zeppelin-web/src/app/jobmanager/jobs/job.controller.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/jobmanager/jobs/job.controller.js 
b/zeppelin-web/src/app/jobmanager/jobs/job.controller.js
deleted file mode 100644
index e811f7b..0000000
--- a/zeppelin-web/src/app/jobmanager/jobs/job.controller.js
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * 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.
- */
-
-import moment from 'moment'
-
-import { ParagraphStatus, } from '../../notebook/paragraph/paragraph.status'
-
-angular.module('zeppelinWebApp').controller('JobCtrl', JobCtrl)
-
-function JobCtrl ($scope, $http, baseUrlSrv) {
-  'ngInject'
-
-  $scope.init = function (jobInformation) {
-    $scope.progressValue = 0
-  }
-
-  $scope.getProgress = function () {
-    let statusList = _.pluck($scope.notebookJob.paragraphs, 'status')
-    let runningJob = _.countBy(statusList, function (status) {
-      if (status === ParagraphStatus.RUNNING || status === 
ParagraphStatus.FINISHED) {
-        return 'matchCount'
-      } else {
-        return 'none'
-      }
-    })
-    let totalCount = statusList.length
-    let runningJobCount = runningJob.matchCount
-    let result = Math.ceil(runningJobCount / totalCount * 100)
-    return isNaN(result) ? 0 : result
-  }
-
-  $scope.runNotebookJob = function (notebookId) {
-    BootstrapDialog.confirm({
-      closable: true,
-      title: '',
-      message: 'Run all paragraphs?',
-      callback: function (result) {
-        if (result) {
-          $http({
-            method: 'POST',
-            url: baseUrlSrv.getRestApiBase() + '/notebook/job/' + notebookId,
-            headers: {
-              'Content-Type': 'application/x-www-form-urlencoded'
-            }
-          }).then(function successCallback (response) {
-            // success
-          }, function errorCallback (errorResponse) {
-            let errorText = 'SERVER ERROR'
-            // eslint-disable-next-line no-extra-boolean-cast
-            if (!!errorResponse.data.message) {
-              errorText = errorResponse.data.message
-            }
-            BootstrapDialog.alert({
-              closable: true,
-              title: 'Execution Failure',
-              message: errorText
-            })
-          })
-        }
-      }
-    })
-  }
-
-  $scope.stopNotebookJob = function (notebookId) {
-    BootstrapDialog.confirm({
-      closable: true,
-      title: '',
-      message: 'Stop all paragraphs?',
-      callback: function (result) {
-        if (result) {
-          $http({
-            method: 'DELETE',
-            url: baseUrlSrv.getRestApiBase() + '/notebook/job/' + notebookId,
-            headers: {
-              'Content-Type': 'application/x-www-form-urlencoded'
-            }
-          }).then(function successCallback (response) {
-            // success
-          }, function errorCallback (errorResponse) {
-            let errorText = 'SERVER ERROR'
-            // eslint-disable-next-line no-extra-boolean-cast
-            if (!!errorResponse.data.message) {
-              errorText = errorResponse.data.message
-            }
-            BootstrapDialog.alert({
-              closable: true,
-              title: 'Stop Failure',
-              message: errorText
-            })
-          })
-        }
-      }
-    })
-  }
-
-  $scope.lastExecuteTime = function (unixtime) {
-    return moment.unix(unixtime / 1000).fromNow()
-  }
-}

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/90b3be5b/zeppelin-web/src/app/jobmanager/jobs/job.css
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/jobmanager/jobs/job.css 
b/zeppelin-web/src/app/jobmanager/jobs/job.css
deleted file mode 100644
index 2bf7a56..0000000
--- a/zeppelin-web/src/app/jobmanager/jobs/job.css
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * 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.
- */
-
-/*
-  job Style
-*/
-
-.job-space {
-  margin-bottom: 5px !important;
-  padding: 10px 10px 10px 10px !important;
-  min-height: 30px;
-}
-
-.job-margin {
-  margin-right: 2px;
-  margin-left: 2px;
-}
-
-.job-types i {
-  font-weight: bold;
-  font-size: 10px;
-}
-
-
-/*
-  job Controls CSS
-*/
-
-.job .runControl {
-  font-size: 1px;
-  color: #AAAAAA;
-  height:4px;
-  margin: 0px 0px 0px 0px;
-}
-
-.job .runControl .progress {
-  position: relative;
-  width: 100%;
-  height: 4px;
-  z-index: 100;
-  border-radius: 0;
-}
-
-.job .control span {
-  margin-left: 4px;
-}
-
-.job .control {
-  background: rgba(255,255,255,0.85);
-  float: right;
-  color: #999;
-  margin-top: 1px;
-  margin-right: 5px;
-  position: absolute;
-  clear: both;
-  right: 15px;
-  text-align: right;
-  font-size: 12px;
-  padding: 4px;
-}
-
-.job .control li {
-  font-size: 12px;
-  margin-bottom: 4px;
-  color: #333333;
-}
-
-.job .control .tooltip {
-  z-index: 10003;
-}
-
-@-webkit-keyframes spinnerRotateAnimation
-{
-  from{-webkit-transform:rotate(0deg);}
-  to{-webkit-transform:rotate(360deg);}
-}
-@-moz-keyframes spinnerRotateAnimation
-{
-  from{-moz-transform:rotate(0deg);}
-  to{-moz-transform:rotate(360deg);}
-}
-@-ms-keyframes spinnerRotateAnimation
-{
-  from{-ms-transform:rotate(0deg);}
-  to{-ms-transform:rotate(360deg);}
-}
-
-@keyframes spinnerRotateAnimation {
-  from {transform: rotate(0deg);}
-  to{transform: rotate(360deg);}
-}
-
-.spinAnimation{
-  -webkit-animation-name: spinnerRotateAnimation;
-  -webkit-animation-duration: 1s;
-  -webkit-animation-iteration-count: infinite;
-  -webkit-animation-timing-function: linear;
-  -moz-animation-name: spinnerRotateAnimation;
-  -moz-animation-duration: 1s;
-  -moz-animation-iteration-count: infinite;
-  -moz-animation-timing-function: linear;
-  -ms-animation-name: spinnerRotateAnimation;
-  -ms-animation-duration: 1s;
-  -ms-animation-iteration-count: infinite;
-  -ms-animation-timing-function: linear;
-}
-

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/90b3be5b/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
deleted file mode 100644
index b6dd6be..0000000
--- a/zeppelin-web/src/app/jobmanager/jobs/job.html
+++ /dev/null
@@ -1,46 +0,0 @@
-<!--
-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.
--->
-
-<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">
-      <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>
-    <a style="text-decoration: none !important;" 
ng-href="#/notebook/{{notebookJob.noteId}}">
-      <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-progress-bar.html'"></div>
-  </div>
-
-  <div>
-    <span ng-repeat="paragraphJob in notebookJob.paragraphs">
-      <a style="text-decoration: none !important;"
-         
ng-href="#/notebook/{{notebookJob.noteId}}?paragraph={{paragraphJob.id}}">
-        <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>
-      </a>
-    </span>
-  </div>
-</div>

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/90b3be5b/zeppelin-web/src/components/websocket/websocket-event.factory.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/components/websocket/websocket-event.factory.js 
b/zeppelin-web/src/components/websocket/websocket-event.factory.js
index f8391ab..db058bb 100644
--- a/zeppelin-web/src/components/websocket/websocket-event.factory.js
+++ b/zeppelin-web/src/components/websocket/websocket-event.factory.js
@@ -66,9 +66,9 @@ function WebsocketEventFactory ($rootScope, $websocket, 
$location, baseUrlSrv) {
     } else if (op === 'NOTES_INFO') {
       $rootScope.$broadcast('setNoteMenu', data.notes)
     } else if (op === 'LIST_NOTE_JOBS') {
-      $rootScope.$broadcast('setNoteJobs', data.noteJobs)
+      $rootScope.$emit('jobmanager:set-jobs', data.noteJobs)
     } else if (op === 'LIST_UPDATE_NOTE_JOBS') {
-      $rootScope.$broadcast('setUpdateNoteJobs', data.noteRunningJobs)
+      $rootScope.$emit('jobmanager:update-jobs', data.noteRunningJobs)
     } else if (op === 'AUTH_INFO') {
       let btn = []
       if ($rootScope.ticket.roles === '[]') {

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/90b3be5b/zeppelin-web/src/components/websocket/websocket-message.service.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/components/websocket/websocket-message.service.js 
b/zeppelin-web/src/components/websocket/websocket-message.service.js
index 9ca631b..0dc02c3 100644
--- a/zeppelin-web/src/components/websocket/websocket-message.service.js
+++ b/zeppelin-web/src/components/websocket/websocket-message.service.js
@@ -308,20 +308,20 @@ function WebsocketMessageService ($rootScope, 
websocketEvents) {
       return websocketEvents.isConnected()
     },
 
-    getNoteJobsList: function () {
+    getJobs: function () {
       websocketEvents.sendNewEvent({op: 'LIST_NOTE_JOBS'})
     },
 
+    disconnectJobEvent: function () {
+      websocketEvents.sendNewEvent({op: 'UNSUBSCRIBE_UPDATE_NOTE_JOBS'})
+    },
+
     getUpdateNoteJobsList: function (lastUpdateServerUnixTime) {
       websocketEvents.sendNewEvent(
         {op: 'LIST_UPDATE_NOTE_JOBS', data: {lastUpdateUnixTime: 
lastUpdateServerUnixTime * 1}}
       )
     },
 
-    unsubscribeJobManager: function () {
-      websocketEvents.sendNewEvent({op: 'UNSUBSCRIBE_UPDATE_NOTE_JOBS'})
-    },
-
     getInterpreterBindings: function (noteId) {
       websocketEvents.sendNewEvent({op: 'GET_INTERPRETER_BINDINGS', data: 
{noteId: noteId}})
     },

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/90b3be5b/zeppelin-web/src/index.html
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/index.html b/zeppelin-web/src/index.html
index 97a8600..4b43179 100644
--- a/zeppelin-web/src/index.html
+++ b/zeppelin-web/src/index.html
@@ -57,7 +57,6 @@ limitations under the License.
     <link rel="stylesheet" href="app/notebook/paragraph/result/result.css" />
     <link rel="stylesheet" 
href="app/notebook/paragraph/result/display-table.css" />
     <link rel="stylesheet" href="app/jobmanager/jobmanager.css" />
-    <link rel="stylesheet" href="app/jobmanager/jobs/job.css" />
     <link rel="stylesheet" href="app/interpreter/interpreter.css" />
     <link rel="stylesheet" href="app/helium/helium.css" />
     <link rel="stylesheet" href="app/credential/credential.css" />

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/90b3be5b/zeppelin-web/src/index.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/index.js b/zeppelin-web/src/index.js
index 75faca0..3cf052b 100644
--- a/zeppelin-web/src/index.js
+++ b/zeppelin-web/src/index.js
@@ -35,9 +35,7 @@ import 
'./app/visualization/builtins/visualization-areachart.js'
 import './app/visualization/builtins/visualization-linechart.js'
 import './app/visualization/builtins/visualization-scatterchart.js'
 
-import './app/jobmanager/jobmanager.controller.js'
-import './app/jobmanager/jobs/job.controller.js'
-import './app/jobmanager/jobmanager.filter.js'
+import './app/jobmanager/jobmanager.component.js'
 import './app/interpreter/interpreter.controller.js'
 import './app/interpreter/interpreter.filter.js'
 import './app/interpreter/interpreter-item.directive.js'

Reply via email to