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=" 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'