5659f0a23b2cb8346d496246d0cce635aeefa87d diff --git contrib/views/jobs/src/main/resources/ui/Gruntfile.js contrib/views/jobs/src/main/resources/ui/Gruntfile.js index b5ce07b..7dc777d 100644 --- contrib/views/jobs/src/main/resources/ui/Gruntfile.js +++ contrib/views/jobs/src/main/resources/ui/Gruntfile.js @@ -257,7 +257,7 @@ module.exports = function (grunt) { src: [ '*.{ico,txt}', '.htaccess', - 'images/{,*/}*.{webp,gif}', + 'img/*', 'styles/fonts/*', 'scripts/assets/**/*' ] diff --git contrib/views/jobs/src/main/resources/ui/app/img/glyphicons-halflings.png contrib/views/jobs/src/main/resources/ui/app/img/glyphicons-halflings.png new file mode 100644 index 0000000..79bc568 Binary files /dev/null and contrib/views/jobs/src/main/resources/ui/app/img/glyphicons-halflings.png differ diff --git contrib/views/jobs/src/main/resources/ui/app/index.html contrib/views/jobs/src/main/resources/ui/app/index.html index 850126e..ea74dcf 100644 --- contrib/views/jobs/src/main/resources/ui/app/index.html +++ contrib/views/jobs/src/main/resources/ui/app/index.html @@ -27,11 +27,14 @@
- - + + + + + diff --git contrib/views/jobs/src/main/resources/ui/app/scripts/app.js contrib/views/jobs/src/main/resources/ui/app/scripts/app.js index 5622f4a..c5d68f2 100644 --- contrib/views/jobs/src/main/resources/ui/app/scripts/app.js +++ contrib/views/jobs/src/main/resources/ui/app/scripts/app.js @@ -27,11 +27,19 @@ App.initializer({ initialize: function(container, application) { application.reopen({ + /** * Test mode is automatically enabled if running on localhost * @type {bool} */ - testMode: (location.hostname == 'localhost') + testMode: (location.hostname == 'localhost'), + + /** + * Prefix for API-requests + * @type {string} + */ + urlPrefix: '/api/v1' + }); } @@ -39,8 +47,10 @@ App.initializer({ /* Order and include as you please. */ +require('scripts/translations'); require('scripts/router'); require('scripts/store'); +require('scripts/mixins/*'); require('scripts/helpers/*'); require('scripts/models/**/*'); require('scripts/mappers/server_data_mapper.js'); @@ -48,4 +58,7 @@ require('scripts/mappers/**/*'); require('scripts/controllers/*'); require('scripts/routes/*'); require('scripts/components/*'); +require('scripts/views/sort_view'); +require('scripts/views/filter_view'); +require('scripts/views/table_view'); require('scripts/views/*'); diff --git contrib/views/jobs/src/main/resources/ui/app/scripts/assets/hive-queries.json contrib/views/jobs/src/main/resources/ui/app/scripts/assets/hive-queries.json index b601670..8bd8f58 100644 --- contrib/views/jobs/src/main/resources/ui/app/scripts/assets/hive-queries.json +++ contrib/views/jobs/src/main/resources/ui/app/scripts/assets/hive-queries.json @@ -227,6 +227,162 @@ }, "entity": "root_20140221171313_c9710dd6-0d1c-4d9c-9dff-031edbd20b66", "entitytype": "HIVE_QUERY_ID" + }, + { + "starttime": 1393443850756, + "events": [ + { + "timestamp": 1393443850756, + "eventtype": "QUERY_COMPLETED", + "eventinfo": {} + }, + { + "timestamp": 1393443850756, + "eventtype": "QUERY_SUBMITTED", + "eventinfo": {} + } + ], + "otherinfo": { + "status": false, + "query": "{}" + }, + "primaryfilters": { + "user": [ + "hive" + ] + }, + "entity": "hive_20188952544444_6301b51e-d52c-4618-995f-573e3f59006c", + "entitytype": "HIVE_QUERY_ID" + }, + { + "starttime": 1393443850756, + "events": [ + { + "timestamp": 1393443850756, + "eventtype": "QUERY_COMPLETED", + "eventinfo": {} + }, + { + "timestamp": 1393443850756, + "eventtype": "QUERY_SUBMITTED", + "eventinfo": {} + } + ], + "otherinfo": { + "status": false, + "query": "{}" + }, + "primaryfilters": { + "user": [ + "hive" + ] + }, + "entity": "hive_20196139444444_6301b51e-d52c-4618-995f-573e3f59006c", + "entitytype": "HIVE_QUERY_ID" + }, + { + "starttime": 1393443850756, + "events": [ + { + "timestamp": 1393443850756, + "eventtype": "QUERY_COMPLETED", + "eventinfo": {} + }, + { + "timestamp": 1393443850756, + "eventtype": "QUERY_SUBMITTED", + "eventinfo": {} + } + ], + "otherinfo": { + "status": false, + "query": "{}" + }, + "primaryfilters": { + "user": [ + "hive" + ] + }, + "entity": "hive_20127273144444_6301b51e-d52c-4618-995f-573e3f59006c", + "entitytype": "HIVE_QUERY_ID" + }, + { + "starttime": 1393443850756, + "events": [ + { + "timestamp": 1393443850756, + "eventtype": "QUERY_COMPLETED", + "eventinfo": {} + }, + { + "timestamp": 1393443850756, + "eventtype": "QUERY_SUBMITTED", + "eventinfo": {} + } + ], + "otherinfo": { + "status": false, + "query": "{}" + }, + "primaryfilters": { + "user": [ + "hive" + ] + }, + "entity": "hive_20113100844444_6301b51e-d52c-4618-995f-573e3f59006c", + "entitytype": "HIVE_QUERY_ID" + }, + { + "starttime": 1393443850756, + "events": [ + { + "timestamp": 1393443850756, + "eventtype": "QUERY_COMPLETED", + "eventinfo": {} + }, + { + "timestamp": 1393443850756, + "eventtype": "QUERY_SUBMITTED", + "eventinfo": {} + } + ], + "otherinfo": { + "status": false, + "query": "{}" + }, + "primaryfilters": { + "user": [ + "hive" + ] + }, + "entity": "hive_20167400444444_6301b51e-d52c-4618-995f-573e3f59006c", + "entitytype": "HIVE_QUERY_ID" + }, + { + "starttime": 1393443850756, + "events": [ + { + "timestamp": 1393443850756, + "eventtype": "QUERY_COMPLETED", + "eventinfo": {} + }, + { + "timestamp": 1393443850756, + "eventtype": "QUERY_SUBMITTED", + "eventinfo": {} + } + ], + "otherinfo": { + "status": false, + "query": "{}" + }, + "primaryfilters": { + "user": [ + "hive" + ] + }, + "entity": "hive_20110915544444_6301b51e-d52c-4618-995f-573e3f59006c", + "entitytype": "HIVE_QUERY_ID" } ] } diff --git contrib/views/jobs/src/main/resources/ui/app/scripts/controllers/job_controller.js contrib/views/jobs/src/main/resources/ui/app/scripts/controllers/job_controller.js new file mode 100644 index 0000000..e300323 --- /dev/null +++ contrib/views/jobs/src/main/resources/ui/app/scripts/controllers/job_controller.js @@ -0,0 +1,19 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +App.JobController = Ember.Controller.extend({}); diff --git contrib/views/jobs/src/main/resources/ui/app/scripts/controllers/jobs_controller.js contrib/views/jobs/src/main/resources/ui/app/scripts/controllers/jobs_controller.js new file mode 100644 index 0000000..c2b6560 --- /dev/null +++ contrib/views/jobs/src/main/resources/ui/app/scripts/controllers/jobs_controller.js @@ -0,0 +1,489 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +App.JobsController = Ember.ArrayController.extend(App.RunPeriodically, { + + name:'mainJobsController', + + /** + * Sorted ArrayProxy + */ + sortedContent: [], + + contentAndSortObserver : function() { + Ember.run.once(this, 'contentAndSortUpdater'); + }.observes('content.length', 'content.@each.id', 'content.@each.startTime', 'content.@each.endTime', 'sortProperties', 'sortAscending'), + + contentAndSortUpdater: function() { + this.set('sortingDone', false); + var content = this.get('content'); + var sortedContent = content.toArray(); + var sortProperty = this.get('sortProperty'); + var sortAscending = this.get('sortAscending'); + sortedContent.sort(function(r1, r2) { + var r1id = r1.get(sortProperty); + var r2id = r2.get(sortProperty); + if (r1id < r2id) + return sortAscending ? -1 : 1; + if (r1id > r2id) + return sortAscending ? 1 : -1; + return 0; + }); + var sortedArray = this.get('sortedContent'); + var count = 0; + sortedContent.forEach(function(sortedJob){ + if(sortedArray.length <= count) { + sortedArray.pushObject(Ember.Object.create()); + } + sortedArray[count].set('failed', sortedJob.get('failed')); + sortedArray[count].set('hasTezDag', sortedJob.get('hasTezDag')); + sortedArray[count].set('queryText', sortedJob.get('queryText')); + sortedArray[count].set('name', sortedJob.get('name')); + sortedArray[count].set('user', sortedJob.get('user')); + sortedArray[count].set('id', sortedJob.get('id')); + sortedArray[count].set('startTimeDisplay', sortedJob.get('startTimeDisplay')); + sortedArray[count].set('endTimeDisplay', sortedJob.get('endTimeDisplay')); + sortedArray[count].set('durationDisplay', sortedJob.get('durationDisplay')); + count ++; + }); + if(sortedArray.length > count) { + for(var c = sortedArray.length-1; c >= count; c--){ + sortedArray.removeObject(sortedArray[c]); + } + } + sortedContent.length = 0; + this.set('sortingDone', true); + }, + + navIDs: { + backIDs: [], + nextID: '' + }, + + lastJobID: '', + + hasNewJobs: false, + + loaded : false, + + loading : false, + + resetPagination: false, + + loadJobsTimeout: null, + + loadTimeout: null, + + jobsUpdateInterval: 6000, + + jobsUpdate: null, + + sortingColumn: null, + + sortProperty: 'id', + + sortAscending: true, + + sortingDone: true, + + jobsMessage: Em.I18n.t('jobs.loadingTasks'), + + sortingColumnObserver: function () { + if(this.get('sortingColumn')){ + this.set('sortProperty', this.get('sortingColumn').get('name')); + this.set('sortAscending', this.get('sortingColumn').get('status') !== "sorting_desc"); + } + }.observes('sortingColumn.name','sortingColumn.status'), + + updateJobsByClick: function () { + this.set('navIDs.backIDs', []); + this.set('navIDs.nextID', ''); + this.get('filterObject').set('nextFromId', ''); + this.get('filterObject').set('backFromId', ''); + this.get('filterObject').set('fromTs', ''); + this.set('hasNewJobs', false); + this.set('resetPagination', true); + this.loadJobs(); + }, + + updateJobs: function (controllerName, funcName) { + clearInterval(this.get('jobsUpdate')); + var self = this; + var interval = setInterval(function () { + App.router.get(controllerName)[funcName](); + }, this.jobsUpdateInterval); + this.set('jobsUpdate', interval); + }, + + totalOfJobs: 0, + + setTotalOfJobs: function () { + if(this.get('totalOfJobs') < this.get('content.length')){ + this.set('totalOfJobs', this.get('content.length')); + } + }.observes('content.length'), + + filterObject: Ember.Object.create({ + id: "", + isIdFilterApplied: false, + jobsLimit: '10', + user: "", + windowStart: "", + windowEnd: "", + nextFromId: "", + backFromId: "", + fromTs: "", + isAnyFilterApplied: false, + + onApplyIdFilter: function () { + this.set('isIdFilterApplied', this.get('id') != ""); + }.observes('id'), + + /** + * Direct binding to startTime filter field + */ + startTime: "", + + onStartTimeChange:function(){ + var time = ""; + var curTime = new Date().getTime(); + switch (this.get('startTime')) { + case 'Past 1 hour': + time = curTime - 3600000; + break; + case 'Past 1 Day': + time = curTime - 86400000; + break; + case 'Past 2 Days': + time = curTime - 172800000; + break; + case 'Past 7 Days': + time = curTime - 604800000; + break; + case 'Past 14 Days': + time = curTime - 1209600000; + break; + case 'Past 30 Days': + time = curTime - 2592000000; + break; + case 'Custom': + this.showCustomDatePopup(); + break; + case 'Any': + time = ""; + break; + } + if(this.get('startTime') != "Custom"){ + this.set("windowStart", time); + this.set("windowEnd", ""); + } + }.observes("startTime"), + + // Fields values from Select Custom Dates form + customDateFormFields: Ember.Object.create({ + startDate: null, + hoursForStart: null, + minutesForStart: null, + middayPeriodForStart: null, + endDate: null, + hoursForEnd: null, + minutesForEnd: null, + middayPeriodForEnd: null + }), + + errors: Ember.Object.create({ + isStartDateError: false, + isEndDateError: false + }), + + errorMessages: Ember.Object.create({ + startDate: '', + endDate: '' + }), + + showCustomDatePopup: function () { + var self = this, + windowEnd = "", + windowStart = ""; + /*App.ModalPopup.show({ + header: Em.I18n.t('jobs.table.custom.date.header'), + onPrimary: function () { + self.validate(); + if(self.get('errors.isStartDateError') || self.get('errors.isEndDateError')){ + return; + } + + var windowStart = self.createCustomStartDate(); + var windowEnd = self.createCustomEndDate(); + + self.set("windowStart", windowStart.getTime()); + self.set("windowEnd", windowEnd.getTime()); + this.hide(); + }, + onSecondary: function () { + self.set('startTime','Any'); + this.hide(); + }, + bodyClass: App.JobsCustomDatesSelectView.extend({ + controller: self + }) + });*/ + }, + + createCustomStartDate : function () { + var startDate = this.get('customDateFormFields.startDate'), + hoursForStart = this.get('customDateFormFields.hoursForStart'), + minutesForStart = this.get('customDateFormFields.minutesForStart'), + middayPeriodForStart = this.get('customDateFormFields.middayPeriodForStart'); + if (startDate && hoursForStart && minutesForStart && middayPeriodForStart) { + return new Date(startDate + ' ' + hoursForStart + ':' + minutesForStart + ' ' + middayPeriodForStart); + } + return null; + }, + + createCustomEndDate : function () { + var endDate = this.get('customDateFormFields.endDate'), + hoursForEnd = this.get('customDateFormFields.hoursForEnd'), + minutesForEnd = this.get('customDateFormFields.minutesForEnd'), + middayPeriodForEnd = this.get('customDateFormFields.middayPeriodForEnd'); + if (endDate && hoursForEnd && minutesForEnd && middayPeriodForEnd) { + return new Date(endDate + ' ' + hoursForEnd + ':' + minutesForEnd + ' ' + middayPeriodForEnd); + } + return null; + }, + + clearErrors: function () { + var errorMessages = this.get('errorMessages'); + Em.keys(errorMessages).forEach(function (key) { + errorMessages.set(key, ''); + }, this); + var errors = this.get('errors'); + Em.keys(errors).forEach(function (key) { + errors.set(key, false); + }, this); + }, + + // Validation for every field in customDateFormFields + validate: function () { + var formFields = this.get('customDateFormFields'), + errors = this.get('errors'), + errorMessages = this.get('errorMessages'); + this.clearErrors(); + // Check if feild is empty + Em.keys(errorMessages).forEach(function (key) { + if (!formFields.get(key)) { + errors.set('is' + key.capitalize() + 'Error', true); + errorMessages.set(key, Em.I18n.t('jobs.customDateFilter.error.required')); + } + }, this); + // Check that endDate is after startDate + var startDate = this.createCustomStartDate(), + endDate = this.createCustomEndDate(); + if (startDate && endDate && (startDate > endDate)) { + errors.set('isEndDateError', true); + errorMessages.set('endDate', Em.I18n.t('jobs.customDateFilter.error.date.order')); + } + }, + + /** + * Create link for server request + * @return {String} + */ + createJobsFiltersLink: function() { + var link = "?fields=events,primaryfilters,otherinfo&secondaryFilter=tez:true", + numberOfAppliedFilters = 0; + + if(this.get("id") !== "") { + link = "/" + this.get("id") + link; + numberOfAppliedFilters++; + } + + link += "&limit=" + (parseInt(this.get("jobsLimit")) + 1); + + if(this.get("user") !== ""){ + link += "&primaryFilter=user:" + this.get("user"); + numberOfAppliedFilters++; + } + if(this.get("backFromId") != ""){ + link += "&fromId=" + this.get("backFromId"); + } + if(this.get("nextFromId") != ""){ + link += "&fromId=" + this.get("nextFromId"); + } + if(this.get("fromTs") != ""){ + link += "&fromTs=" + this.get("fromTs"); + } + if(this.get("startTime") !== "" && this.get("startTime") !== "Any"){ + link += this.get("windowStart") !== "" ? ("&windowStart=" + this.get("windowStart")) : ""; + link += this.get("windowEnd") !== "" ? ("&windowEnd=" + this.get("windowEnd")) : ""; + numberOfAppliedFilters++; + } + + this.set('isAnyFilterApplied', numberOfAppliedFilters > 0); + + return link; + } + }), + + /*columnsName: Ember.ArrayController.create({ + content: [ + { name: Em.I18n.t('jobs.column.id'), index: 0 }, + { name: Em.I18n.t('jobs.column.user'), index: 1 }, + { name: Em.I18n.t('jobs.column.start.time'), index: 2 }, + { name: Em.I18n.t('jobs.column.end.time'), index: 3 }, + { name: Em.I18n.t('jobs.column.duration'), index: 4 } + ], + columnsCount: function () { + return this.get('content.length') + 1; + }.property('content.length') + }),*/ + + lastIDSuccessCallback: function(data) { + if(!data.entities[0]){ + return; + } + var lastReceivedID = data.entities[0].entity; + if(this.get('lastJobID') == '') { + this.set('lastJobID', lastReceivedID); + if (this.get('loaded') && App.HiveJob.find().get('length') < 1) { + this.set('hasNewJobs', true); + } + } + else + if (this.get('lastJobID') !== lastReceivedID) { + this.set('lastJobID', lastReceivedID); + if(!App.HiveJob.find().findProperty('id', lastReceivedID)) { + this.set('hasNewJobs', true); + } + } + }, + + lastIDErrorCallback: function(data, jqXHR, textStatus) { + console.debug(jqXHR); + }, + + checkDataLoadingError: function (jqXHR){ + /*var atsComponent = App.HostComponent.find().findProperty('componentName','APP_TIMELINE_SERVER'); + if(atsComponent && atsComponent.get('workStatus') != "STARTED") { + this.set('jobsMessage', Em.I18n.t('jobs.error.ats.down')); + }else if (jqXHR && jqXHR.status == 400) { + this.set('jobsMessage', Em.I18n.t('jobs.error.400')); + }else if ((!jqXHR && this.get('loaded') && !this.get('loading')) || (jqXHR && jqXHR.status == 500)) { + this.set('jobsMessage', Em.I18n.t('jobs.nothingToShow')); + }else{ + this.set('jobsMessage', Em.I18n.t('jobs.loadingTasks')); + }*/ + }, + + init: function() { + this.set('interval', 6000); + this.loop('loadJobs'); + }, + + loadJobs : function() { + //var yarnService = App.YARNService.find().objectAt(0), + //atsComponent = App.HostComponent.find().findProperty('componentName','APP_TIMELINE_SERVER'), + //atsInValidState = !!atsComponent && atsComponent.get('workStatus') === "STARTED", + //retryLoad = this.checkDataLoadingError(); + //if (yarnService != null && atsInValidState) { + this.set('loading', true); + /*var historyServerHostName = yarnService.get('appTimelineServer.hostName'), + filtersLink = this.get('filterObject').createJobsFiltersLink(), + hiveQueriesUrl = App.get('testMode') ? "/scripts/assets/hive-queries.json" : "/proxy?url=http://" + historyServerHostName + + ":" + yarnService.get('ahsWebPort') + "/ws/v1/timeline/HIVE_QUERY_ID" + filtersLink;*/ + /*App.ajax.send({ + name: 'jobs.lastID', + sender: self, + data: { + historyServerHostName: '',//historyServerHostName, + ahsWebPort: ''//yarnService.get('ahsWebPort') + }, + success: 'lastIDSuccessCallback', + error : 'lastIDErrorCallback' + });*/ + App.ajax.send({ + name: 'load_jobs', + sender: this, + data: { + historyServerHostName: '', + ahsWebPort: '', + filtersLink: this.get('filterObject').createJobsFiltersLink() + }, + success: 'loadJobsSuccessCallback', + error : 'loadJobsErrorCallback' + }); + }, + + loadJobsSuccessCallback: function(data) { + App.hiveJobsMapper.map(data); + this.set('loading', false); + if(this.get('loaded') == false || this.get('resetPagination') == true) { + this.initializePagination(); + this.set('resetPagination', false); + } + this.set('loaded', true); + }, + + loadJobsErrorCallback: function(jqXHR) { + App.hiveJobsMapper.map({entities : []}); + this.checkDataLoadingError(jqXHR); + }, + + initializePagination: function() { + var back_link_IDs = this.get('navIDs.backIDs.[]'); + if(!back_link_IDs.contains(this.get('lastJobID'))) { + back_link_IDs.push(this.get('lastJobID')); + } + this.set('filterObject.backFromId', this.get('lastJobID')); + this.get('filterObject').set('fromTs', new Date().getTime()); + }, + + navigateNext: function() { + this.set("filterObject.backFromId", ''); + var back_link_IDs = this.get('navIDs.backIDs.[]'); + var lastBackID = this.get('navIDs.nextID'); + if(!back_link_IDs.contains(lastBackID)) { + back_link_IDs.push(lastBackID); + } + this.set('navIDs.backIDs.[]', back_link_IDs); + this.set("filterObject.nextFromId", this.get('navIDs.nextID')); + this.set('navIDs.nextID', ''); + this.loadJobs(); + }, + + navigateBack: function() { + this.set("filterObject.nextFromId", ''); + var back_link_IDs = this.get('navIDs.backIDs.[]'); + back_link_IDs.pop(); + var lastBackID = back_link_IDs[back_link_IDs.length - 1]; + this.set('navIDs.backIDs.[]', back_link_IDs); + this.set("filterObject.backFromId", lastBackID); + this.loadJobs(); + }, + + refreshLoadedJobs : function() { + this.loadJobs(); + }.observes( + 'filterObject.id', + 'filterObject.jobsLimit', + 'filterObject.user', + 'filterObject.windowStart', + 'filterObject.windowEnd' + ) + +}); diff --git contrib/views/jobs/src/main/resources/ui/app/scripts/helpers/ajax.js contrib/views/jobs/src/main/resources/ui/app/scripts/helpers/ajax.js index 42bae63..2c301ad 100644 --- contrib/views/jobs/src/main/resources/ui/app/scripts/helpers/ajax.js +++ contrib/views/jobs/src/main/resources/ui/app/scripts/helpers/ajax.js @@ -28,7 +28,21 @@ * * @type {Object} */ -var urls = {}; +var urls = { + + 'load_jobs': { + real: '/proxy?url=http://{historyServerHostName}:{ahsWebPort}/ws/v1/timeline/HIVE_QUERY_ID{filtersLink}', + mock: '/scripts/assets/hive-queries.json', + apiPrefix: '' + }, + + 'jobs_lastID': { + real: '/proxy?url=http://{historyServerHostName}:{ahsWebPort}/ws/v1/timeline/HIVE_QUERY_ID?limit=1&secondaryFilter=tez:true', + mock: '/scripts/assets/hive-queries.json', + apiPrefix: '' + } + +}; /** * Replace data-placeholders to its values * diff --git contrib/views/jobs/src/main/resources/ui/app/scripts/helpers/misc.js contrib/views/jobs/src/main/resources/ui/app/scripts/helpers/misc.js index 25af752..f26d658 100644 --- contrib/views/jobs/src/main/resources/ui/app/scripts/helpers/misc.js +++ contrib/views/jobs/src/main/resources/ui/app/scripts/helpers/misc.js @@ -28,7 +28,7 @@ App.Helpers.misc = { } else { if (value < 1048576) { value = (value / 1024).toFixed(1) + 'KB'; - } else if (value >= 1048576 && value < 1073741824){ + } else if (value >= 1048576 && value < 1073741824) { value = (value / 1048576).toFixed(1) + 'MB'; } else { value = (value / 1073741824).toFixed(2) + 'GB'; @@ -36,6 +36,32 @@ App.Helpers.misc = { } } return value; + }, + + /** + * Convert ip address to integer + * @param ip + * @return integer + */ + ipToInt: function (ip) { + // * example 1: ipToInt('192.0.34.166'); + // * returns 1: 3221234342 + // * example 2: ipToInt('255.255.255.256'); + // * returns 2: false + // Verify IP format. + if (!/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(ip)) { + return false; // Invalid format. + } + // Reuse ip variable for component counter. + var d = ip.split('.'); + return ((((((+d[0]) * 256) + (+d[1])) * 256) + (+d[2])) * 256) + (+d[3]); } }; + +App.tooltip = function (self, options) { + self.tooltip(options); + self.on("remove DOMNodeRemoved", function () { + $(this).trigger('mouseleave'); + }); +}; \ No newline at end of file diff --git contrib/views/jobs/src/main/resources/ui/app/scripts/mappers/jobs/hive_jobs_mapper.js contrib/views/jobs/src/main/resources/ui/app/scripts/mappers/jobs/hive_jobs_mapper.js index 9a1fc07..49b373c 100644 --- contrib/views/jobs/src/main/resources/ui/app/scripts/mappers/jobs/hive_jobs_mapper.js +++ contrib/views/jobs/src/main/resources/ui/app/scripts/mappers/jobs/hive_jobs_mapper.js @@ -17,12 +17,46 @@ App.hiveJobsMapper = App.QuickDataMapper.create({ - model: App.HiveJob, + json_map: { + id: 'entity', + name: 'entity', + user: 'primaryfilters.user', + hasTezDag: { + custom: function(source) { + var query = Ember.get(source, 'otherinfo.query'); + return Ember.isNone(query) ? false : query.match("\"Tez\".*\"DagName:\""); + } + }, + queryText: { + custom: function(source) { + var query = Ember.get(source, 'otherinfo.query'); + return Ember.isNone(query) ? '' : $.parseJSON(query).queryText; + } + }, + failed: { + custom: function(source) { + return Ember.get(source ,'otherinfo.status') === false; + } + }, + startTime: { + custom: function(source) { + return source.starttime > 0 ? source.starttime : null + } + }, + endTime: { + custom: function(source) { + return source.endtime > 0 ? source.endtime : null + } + } + }, map: function (json) { var model = this.get('model'), + jobsToDelete = App.HiveJob.store.all('hiveJob').get('content').mapProperty('id'), + map = this.get('json_map'), hiveJobs = []; + if (json) { if (!json.entities) { json.entities = []; @@ -30,67 +64,35 @@ App.hiveJobsMapper = App.QuickDataMapper.create({ json.entities = [json]; } } - var currentEntityMap = {}; + json.entities.forEach(function (entity) { - currentEntityMap[entity.entity] = entity.entity; - var hiveJob = { - id: entity.entity, - name: entity.entity, - user: entity.primaryfilters.user - }; - hiveJob.has_tez_dag = false; - hiveJob.query_text = ''; - if (entity.otherinfo && entity.otherinfo.query) { - // Explicit false match needed for when failure hook not set - hiveJob.failed = entity.otherinfo.status === false; - hiveJob.has_tez_dag = entity.otherinfo.query.match("\"Tez\".*\"DagName:\""); - var queryJson = $.parseJSON(entity.otherinfo.query); - if (queryJson && queryJson.queryText) { - hiveJob.query_text = queryJson.queryText; - } - } + var hiveJob = Ember.JsonMapper.map(entity, map); + if (entity.events != null) { entity.events.forEach(function (event) { switch (event.eventtype) { case "QUERY_SUBMITTED": - hiveJob.start_time = event.timestamp; + hiveJob.startTime = event.timestamp; break; case "QUERY_COMPLETED": - hiveJob.end_time = event.timestamp; + hiveJob.endTime = event.timestamp; break; default: break; } }); } - if (!hiveJob.start_time && entity.starttime > 0) { - hiveJob.start_time = entity.starttime; - } - if (!hiveJob.end_time && entity.endtime > 0) { - hiveJob.end_time = entity.endtime; - } hiveJobs.push(hiveJob); - hiveJob = null; - entity = null; + jobsToDelete = jobsToDelete.without(hiveJob.id); }); - /*if(hiveJobs.length > App.router.get('mainJobsController.filterObject.jobsLimit')) { - var lastJob = hiveJobs.pop(); - if(App.router.get('mainJobsController.navIDs.nextID') != lastJob.id) { - App.router.set('mainJobsController.navIDs.nextID', lastJob.id); - } - currentEntityMap[lastJob.id] = null; - }*/ + jobsToDelete.forEach(function (id) { + var r = App.HiveJob.store.getById('hiveJob', id); + if(r) r.destroyRecord(); + }); - // Delete IDs not seen from server - /*var hiveJobsModel = model.find().toArray(); - hiveJobsModel.forEach(function(job) { - if (job && !currentEntityMap[job.get('id')]) { - this.deleteRecord(job); - } - }, this);*/ } App.HiveJob.store.pushMany('hiveJob', hiveJobs); - }, - config: {} + } + }); diff --git contrib/views/jobs/src/main/resources/ui/app/scripts/mixins/run_periodically.js contrib/views/jobs/src/main/resources/ui/app/scripts/mixins/run_periodically.js new file mode 100644 index 0000000..a6c4bbf --- /dev/null +++ contrib/views/jobs/src/main/resources/ui/app/scripts/mixins/run_periodically.js @@ -0,0 +1,78 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Allow to run object method periodically and stop it + * Example: + *
+ * var obj = Ember.Object.createWithMixins(App.RunPeriodically, {
+ * method: Ember.K
+ * });
+ * obj.set('interval', 10000); // override default value
+ * obj.loop('method'); // run periodically
+ * obj.stop(); // stop running
+ *
+ * @type {Ember.Mixin}
+ */
+App.RunPeriodically = Ember.Mixin.create({
+
+ /**
+ * Interval for loop
+ * @type {number}
+ */
+ interval: 5000,
+
+ /**
+ * setTimeout's return value
+ * @type {number}
+ */
+ timer: null,
+
+ /**
+ * Run methodName periodically with interval
+ * @param {string} methodName method name to run periodically
+ * @param {bool} initRun should methodName be run before setInterval call (default - true)
+ * @method run
+ */
+ loop: function(methodName, initRun) {
+ initRun = Em.isNone(initRun) ? true : initRun;
+ var self = this,
+ interval = this.get('interval');
+ Ember.assert('Interval should be numeric and greated than 0', $.isNumeric(interval) && interval > 0);
+ if (initRun) {
+ this[methodName]();
+ }
+ this.set('timer',
+ setInterval(function () {
+ self[methodName]();
+ }, interval)
+ );
+ },
+
+ /**
+ * Stop running timer
+ * @method stop
+ */
+ stop: function() {
+ var timer = this.get('timer');
+ if (!Em.isNone(timer)) {
+ clearTimeout(timer);
+ }
+ }
+
+});
diff --git contrib/views/jobs/src/main/resources/ui/app/scripts/models/jobs/hive_job.js contrib/views/jobs/src/main/resources/ui/app/scripts/models/jobs/hive_job.js
index a3784e8..53d309e 100644
--- contrib/views/jobs/src/main/resources/ui/app/scripts/models/jobs/hive_job.js
+++ contrib/views/jobs/src/main/resources/ui/app/scripts/models/jobs/hive_job.js
@@ -21,8 +21,6 @@ App.HiveJob = App.AbstractJob.extend({
queryText : DS.attr('string'),
- stages : DS.attr('array'),
-
hasTezDag: DS.attr('boolean'),
tezDag : DS.belongsTo('tezDag', {async:true}),
diff --git contrib/views/jobs/src/main/resources/ui/app/scripts/translations.js contrib/views/jobs/src/main/resources/ui/app/scripts/translations.js
new file mode 100644
index 0000000..a656c97
--- /dev/null
+++ contrib/views/jobs/src/main/resources/ui/app/scripts/translations.js
@@ -0,0 +1,81 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+Ember.I18n.translations = {
+
+ 'any': 'Any',
+ 'apply': 'Apply',
+
+ 'jobs.type':'Jobs Type',
+ 'jobs.type.hive':'Hive',
+ 'jobs.show.up.to':'Show up to',
+ 'jobs.filtered.jobs':'%@ jobs showing',
+ 'jobs.filtered.clear':'clear filters',
+ 'jobs.column.id':'Id',
+ 'jobs.column.user':'User',
+ 'jobs.column.start.time':'Start Time',
+ 'jobs.column.end.time':'End Time',
+ 'jobs.column.duration':'Duration',
+ 'jobs.new_jobs.info':'New jobs available on server.',
+ 'jobs.loadingTasks': 'Loading...',
+
+ 'jobs.nothingToShow': 'No jobs to display',
+ 'jobs.error.ats.down': 'Jobs data cannot be shown since YARN App Timeline Server is not running.',
+ 'jobs.error.400': 'Unable to load data.',
+ 'jobs.table.custom.date.am':'AM',
+ 'jobs.table.custom.date.pm':'PM',
+ 'jobs.table.custom.date.header':'Select Custom Dates',
+ 'jobs.table.job.fail':'Job failed to run',
+ 'jobs.customDateFilter.error.required':'This field is required',
+ 'jobs.customDateFilter.error.date.order':'End Date must be after Start Date',
+ 'jobs.customDateFilter.startTime':'Start Time',
+ 'jobs.customDateFilter.endTime':'End Time',
+ 'jobs.hive.failed':'JOB FAILED',
+ 'jobs.hive.more':'show more',
+ 'jobs.hive.less':'show less',
+ 'jobs.hive.query':'Hive Query',
+ 'jobs.hive.stages':'Stages',
+ 'jobs.hive.yarnApplication':'YARN Application',
+ 'jobs.hive.tez.tasks':'Tez Tasks',
+ 'jobs.hive.tez.hdfs':'HDFS',
+ 'jobs.hive.tez.localFiles':'Local Files',
+ 'jobs.hive.tez.spilledRecords':'Spilled Records',
+ 'jobs.hive.tez.records':'Records',
+ 'jobs.hive.tez.reads':'{0} reads',
+ 'jobs.hive.tez.writes':'{0} writes',
+ 'jobs.hive.tez.records.count':'{0} Records',
+ 'jobs.hive.tez.operatorPlan':'Operator Plan',
+ 'jobs.hive.tez.dag.summary.metric':'Summary Metric',
+ 'jobs.hive.tez.dag.error.noDag.title':'No Tez Information',
+ 'jobs.hive.tez.dag.error.noDag.message':'This job does not identify any Tez information.',
+ 'jobs.hive.tez.dag.error.noDagId.title':'No Tez Information',
+ 'jobs.hive.tez.dag.error.noDagId.message':'No Tez information was found for this job. Either it is waiting to be run, or has exited unexpectedly.',
+ 'jobs.hive.tez.dag.error.noDagForId.title':'No Tez Information',
+ 'jobs.hive.tez.dag.error.noDagForId.message':'No details were found for the Tez ID given to this job.',
+ 'jobs.hive.tez.metric.input':'Input',
+ 'jobs.hive.tez.metric.output':'Output',
+ 'jobs.hive.tez.metric.recordsRead':'Records Read',
+ 'jobs.hive.tez.metric.recordsWrite':'Records Written',
+ 'jobs.hive.tez.metric.tezTasks':'Tez Tasks',
+ 'jobs.hive.tez.metric.spilledRecords':'Spilled Records',
+ 'jobs.hive.tez.edge.':'Unknown',
+ 'jobs.hive.tez.edge.contains':'Contains',
+ 'jobs.hive.tez.edge.broadcast':'Broadcast',
+ 'jobs.hive.tez.edge.scatter_gather':'Shuffle',
+
+};
diff --git contrib/views/jobs/src/main/resources/ui/app/scripts/views/filter_view.js contrib/views/jobs/src/main/resources/ui/app/scripts/views/filter_view.js
new file mode 100644
index 0000000..0506286
--- /dev/null
+++ contrib/views/jobs/src/main/resources/ui/app/scripts/views/filter_view.js
@@ -0,0 +1,488 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Wrapper View for all filter components. Layout template and common actions are located inside of it.
+ * Logic specific for data component(input, select, or custom multi select, which fire any changes on interface) are
+ * located in inner view - filterView.
+ *
+ * If we want to have input filter, put textFieldView to it.
+ * All inner views implemented below this view.
+ * @type {*}
+ */
+
+var wrapperView = Ember.View.extend({
+ classNames: ['view-wrapper'],
+ layoutName: 'wrapper_layout',
+ templateName: 'wrapper_template',
+
+ value: null,
+
+ /**
+ * Column index
+ */
+ column: null,
+
+ /**
+ * If this field is exists we dynamically create hidden input element and set value there.
+ * Used for some cases, where this values will be used outside of component
+ */
+ fieldId: null,
+
+ clearFilter: function(){
+ this.set('value', this.get('emptyValue'));
+ if(this.get('setPropertyOnApply')){
+ this.setValueOnApply();
+ }
+ return false;
+ },
+
+ setValueOnApply: function() {
+ if(this.get('value') == null){
+ this.set('value', '')
+ }
+ this.set(this.get('setPropertyOnApply'), this.get('value'));
+ return false;
+ },
+
+ actions: {
+ actionSetValueOnApply: function() {
+ this.setValueOnApply();
+ }
+ },
+
+ /**
+ * Use to determine whether filter is clear or not. Also when we want to set empty value
+ */
+ emptyValue: '',
+
+ /**
+ * Whether our value is empty or not
+ * @return {Boolean}
+ */
+ isEmpty: function(){
+ if(this.get('value') === null){
+ return true;
+ }
+ return this.get('value').toString() === this.get('emptyValue').toString();
+ },
+
+ /**
+ * Show/Hide Clear filter button.
+ * Also this method updates computed field related to fieldId if it exists.
+ * Call onChangeValue callback when everything is done.
+ */
+ showClearFilter: function(){
+ if(!this.get('parentNode')){
+ return;
+ }
+ // get the sort view element in the same column to current filter view to highlight them together
+ var relatedSort = $(this.get('element')).parents('thead').find('.sort-view-' + this.get('column'));
+ if(this.isEmpty()){
+ this.get('parentNode').removeClass('active-filter');
+ this.get('parentNode').addClass('notActive');
+ relatedSort.removeClass('active-sort');
+ } else {
+ this.get('parentNode').removeClass('notActive');
+ this.get('parentNode').addClass('active-filter');
+ relatedSort.addClass('active-sort');
+ }
+
+ if(this.get('fieldId')){
+ this.$('> input').eq(0).val(this.get('value'));
+ }
+
+ this.onChangeValue();
+ }.observes('value'),
+
+ /**
+ * Callback for value changes
+ */
+ onChangeValue: function(){
+
+ },
+
+ /**
+ * Filter components is located here. Should be redefined
+ */
+ filterView: Em.View,
+
+ /**
+ * Update class of parentNode(hide clear filter button) on page load
+ */
+ didInsertElement: function(){
+ var parent = this.$().parent();
+ this.set('parentNode', parent);
+ parent.addClass('notActive');
+ }
+});
+
+/**
+ * Simple input control for wrapperView
+ */
+var textFieldView = Ember.TextField.extend({
+ type:'text',
+ placeholder: Em.I18n.t('any'),
+ valueBinding: "parentView.value"
+});
+
+/**
+ * Simple multiselect control for wrapperView.
+ * Used to render blue button and popup, which opens on button click.
+ * All content related logic should be implemented manually outside of it
+ */
+var componentFieldView = Ember.View.extend({
+ classNames: ['btn-group'],
+ classNameBindings: ['isFilterOpen:open:'],
+
+ /**
+ * Whether popup is shown or not
+ */
+ isFilterOpen: false,
+
+ /**
+ * We have value property similar to inputs value property
+ */
+ valueBinding: 'parentView.value',
+
+ /**
+ * Clear filter to initial state
+ */
+ clearFilter: function(){
+ this.set('value', '');
+ },
+
+ /**
+ * Onclick handler for cancel filter button
+ */
+ closeFilter:function () {
+ $(document).unbind('click');
+ this.set('isFilterOpen', false);
+ },
+
+ /**
+ * Onclick handler for apply filter button
+ */
+ applyFilter:function() {
+ this.closeFilter();
+ },
+
+ /**
+ * Onclick handler for show component filter button.
+ * Also this function is used in some other places
+ */
+ clickFilterButton:function () {
+ var self = this;
+ this.set('isFilterOpen', !this.get('isFilterOpen'));
+ if (this.get('isFilterOpen')) {
+
+ var dropDown = this.$('.filter-components');
+ var firstClick = true;
+ $(document).bind('click', function (e) {
+ if (!firstClick && $(e.target).closest(dropDown).length == 0) {
+ self.set('isFilterOpen', false);
+ $(document).unbind('click');
+ }
+ firstClick = false;
+ });
+ }
+ }
+});
+
+/**
+ * Simple select control for wrapperView
+ */
+var selectFieldView = Ember.Select.extend({
+ selectionBinding: 'parentView.value',
+ contentBinding: 'parentView.content'
+});
+
+/**
+ * Result object, which will be accessible outside
+ * @type {Object}
+ */
+App.Filters = {
+ /**
+ * You can access wrapperView outside
+ */
+ wrapperView : wrapperView,
+
+ /**
+ * And also controls views if need it
+ */
+ textFieldView : textFieldView,
+ selectFieldView: selectFieldView,
+ componentFieldView: componentFieldView,
+
+ /**
+ * Quick create input filters
+ * @param config parameters of wrapperView
+ */
+ createTextView : function(config){
+ config.fieldType = config.fieldType || 'input-medium';
+ config.filterView = textFieldView.extend({
+ classNames : [ config.fieldType ]
+ });
+
+ return wrapperView.extend(config);
+ },
+
+ /**
+ * Quick create multiSelect filters
+ * @param config parameters of wrapperView
+ */
+ createComponentView : function(config){
+ config.clearFilter = function(){
+ this.forEachChildView(function(item){
+ if(item.clearFilter){
+ item.clearFilter();
+ }
+ });
+ return false;
+ };
+
+ return wrapperView.extend(config);
+ },
+
+ /**
+ * Quick create select filters
+ * @param config parameters of wrapperView
+ */
+ createSelectView: function(config){
+
+ config.fieldType = config.fieldType || 'input-medium';
+ config.filterView = selectFieldView.extend({
+ classNames : [ config.fieldType ],
+ attributeBindings: ['disabled','multiple'],
+ disabled: false
+ });
+ config.emptyValue = Em.I18n.t('any');
+
+ return wrapperView.extend(config);
+ },
+ /**
+ * returns the filter function, which depends on the type of property
+ * @param type
+ * @param isGlobal check is search global
+ * @return {Function}
+ */
+ getFilterByType: function(type, isGlobal){
+ switch (type){
+ case 'ambari-bandwidth':
+ return function(rowValue, rangeExp){
+ var compareChar = isNaN(rangeExp.charAt(0)) ? rangeExp.charAt(0) : false;
+ var compareScale = rangeExp.charAt(rangeExp.length - 1);
+ var compareValue = compareChar ? parseFloat(rangeExp.substr(1, rangeExp.length)) : parseFloat(rangeExp.substr(0, rangeExp.length));
+ var match = false;
+ if (rangeExp.length == 1 && compareChar !== false) {
+ // User types only '=' or '>' or '<', so don't filter column values
+ match = true;
+ return match;
+ }
+ switch (compareScale) {
+ case 'g':
+ compareValue *= 1073741824;
+ break;
+ case 'm':
+ compareValue *= 1048576;
+ break;
+ case 'k':
+ compareValue *= 1024;
+ break;
+ default:
+ //default value in GB
+ compareValue *= 1073741824;
+ }
+ rowValue = (jQuery(rowValue).text()) ? jQuery(rowValue).text() : rowValue;
+
+ var convertedRowValue;
+ if (rowValue === '<1KB') {
+ convertedRowValue = 1;
+ } else {
+ var rowValueScale = rowValue.substr(rowValue.length - 2, 2);
+ switch (rowValueScale) {
+ case 'KB':
+ convertedRowValue = parseFloat(rowValue)*1024;
+ break;
+ case 'MB':
+ convertedRowValue = parseFloat(rowValue)*1048576;
+ break;
+ case 'GB':
+ convertedRowValue = parseFloat(rowValue)*1073741824;
+ break;
+ }
+ }
+
+ switch (compareChar) {
+ case '<':
+ if (compareValue > convertedRowValue) match = true;
+ break;
+ case '>':
+ if (compareValue < convertedRowValue) match = true;
+ break;
+ case false:
+ case '=':
+ if (compareValue == convertedRowValue) match = true;
+ break;
+ }
+ return match;
+ };
+ break;
+ case 'duration':
+ return function (rowValue, rangeExp) {
+ var compareChar = isNaN(rangeExp.charAt(0)) ? rangeExp.charAt(0) : false;
+ var compareScale = rangeExp.charAt(rangeExp.length - 1);
+ var compareValue = compareChar ? parseFloat(rangeExp.substr(1, rangeExp.length)) : parseFloat(rangeExp.substr(0, rangeExp.length));
+ var match = false;
+ if (rangeExp.length == 1 && compareChar !== false) {
+ // User types only '=' or '>' or '<', so don't filter column values
+ match = true;
+ return match;
+ }
+ switch (compareScale) {
+ case 's':
+ compareValue *= 1000;
+ break;
+ case 'm':
+ compareValue *= 60000;
+ break;
+ case 'h':
+ compareValue *= 3600000;
+ break;
+ default:
+ compareValue *= 1000;
+ }
+ rowValue = (jQuery(rowValue).text()) ? jQuery(rowValue).text() : rowValue;
+
+ switch (compareChar) {
+ case '<':
+ if (compareValue > rowValue) match = true;
+ break;
+ case '>':
+ if (compareValue < rowValue) match = true;
+ break;
+ case false:
+ case '=':
+ if (compareValue == rowValue) match = true;
+ break;
+ }
+ return match;
+ };
+ break;
+ case 'date':
+ return function (rowValue, rangeExp) {
+ var match = false;
+ var timePassed = App.dateTime() - rowValue;
+ switch (rangeExp) {
+ case 'Past 1 hour':
+ match = timePassed <= 3600000;
+ break;
+ case 'Past 1 Day':
+ match = timePassed <= 86400000;
+ break;
+ case 'Past 2 Days':
+ match = timePassed <= 172800000;
+ break;
+ case 'Past 7 Days':
+ match = timePassed <= 604800000;
+ break;
+ case 'Past 14 Days':
+ match = timePassed <= 1209600000;
+ break;
+ case 'Past 30 Days':
+ match = timePassed <= 2592000000;
+ break;
+ case 'Any':
+ match = true;
+ break;
+ }
+ return match;
+ };
+ break;
+ case 'number':
+ return function(rowValue, rangeExp){
+ var compareChar = rangeExp.charAt(0);
+ var compareValue;
+ var match = false;
+ if (rangeExp.length == 1) {
+ if (isNaN(parseInt(compareChar))) {
+ // User types only '=' or '>' or '<', so don't filter column values
+ match = true;
+ return match;
+ }
+ else {
+ compareValue = parseFloat(parseFloat(rangeExp).toFixed(2));
+ }
+ }
+ else {
+ if (isNaN(parseInt(compareChar))) {
+ compareValue = parseFloat(parseFloat(rangeExp.substr(1, rangeExp.length)).toFixed(2));
+ }
+ else {
+ compareValue = parseFloat(parseFloat(rangeExp.substr(0, rangeExp.length)).toFixed(2));
+ }
+ }
+ rowValue = parseFloat((jQuery(rowValue).text()) ? jQuery(rowValue).text() : rowValue);
+ match = false;
+ switch (compareChar) {
+ case '<':
+ if (compareValue > rowValue) match = true;
+ break;
+ case '>':
+ if (compareValue < rowValue) match = true;
+ break;
+ case '=':
+ if (compareValue == rowValue) match = true;
+ break;
+ default:
+ if (rangeExp == rowValue) match = true;
+ }
+ return match;
+ };
+ break;
+ case 'multiple':
+ return function(origin, compareValue){
+ var options = compareValue.split(','),
+ rowValue = (typeof (origin) === "string") ? origin : origin.mapProperty('componentName').join(" ");
+ var str = new RegExp(compareValue, "i");
+ for (var i = 0; i < options.length; i++) {
+ if(!isGlobal) {
+ str = new RegExp('(\\W|^)' + options[i] + '(\\W|$)');
+ }
+ if (rowValue.search(str) !== -1) {
+ return true;
+ }
+ }
+ return false;
+ };
+ break;
+ case 'boolean':
+ return function (origin, compareValue){
+ return origin === compareValue;
+ };
+ break;
+ case 'string':
+ default:
+ return function(origin, compareValue){
+ var regex = new RegExp(compareValue,"i");
+ return regex.test(origin);
+ }
+ }
+ }
+
+};
diff --git contrib/views/jobs/src/main/resources/ui/app/scripts/views/job_view.js contrib/views/jobs/src/main/resources/ui/app/scripts/views/job_view.js
new file mode 100644
index 0000000..eae86c2
--- /dev/null
+++ contrib/views/jobs/src/main/resources/ui/app/scripts/views/job_view.js
@@ -0,0 +1,19 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+App.JobView = Ember.View.extend({});
diff --git contrib/views/jobs/src/main/resources/ui/app/scripts/views/jobs_view.js contrib/views/jobs/src/main/resources/ui/app/scripts/views/jobs_view.js
new file mode 100644
index 0000000..26124b5
--- /dev/null
+++ contrib/views/jobs/src/main/resources/ui/app/scripts/views/jobs_view.js
@@ -0,0 +1,305 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+App.JobsView = App.TableView.extend({
+
+ templateName: 'jobs',
+
+ content: [],
+
+
+ /**
+ * If no jobs table rows to show.
+ */
+ noDataToShow: true,
+
+ filterCondition:[],
+
+ /*
+ If no jobs to display set noDataToShow to true, else set emptyData to false.
+ */
+ noDataToShowObserver: function () {
+ if(this.get("controller.content.length") > 0){
+ this.set("noDataToShow",false);
+ }else{
+ this.set("noDataToShow",true);
+ }
+ }.observes("controller.content.length"),
+
+ willInsertElement: function () {
+ this._super();
+ this.clearFilters();
+ this.onApplyIdFilter();
+ this.set('tableFilteringComplete', true);
+ },
+
+ didInsertElement: function () {
+ if(!this.get('controller.sortingColumn')){
+ var columns = this.get('childViews')[0].get('childViews');
+ if(columns && columns.findProperty('name', 'startTime')){
+ columns.findProperty('name','startTime').set('status', 'sorting_desc');
+ this.get('controller').set('sortingColumn', columns.findProperty('name','startTime'))
+ }
+ }
+ },
+
+ onApplyIdFilter: function() {
+ var isIdFilterApplied = this.get('controller.filterObject.isIdFilterApplied');
+ this.get('childViews').forEach(function(childView) {
+ if (childView['clearFilter'] && childView.get('column') != 1) {
+ if(isIdFilterApplied){
+ childView.clearFilter();
+ }
+ var childOfChild = childView.get('childViews')[0];
+ if(childOfChild){
+ Em.run.next(function() {
+ childOfChild.set('disabled', isIdFilterApplied);
+ })
+ }
+ }
+ });
+ }.observes('controller.filterObject.isIdFilterApplied'),
+
+ saveFilter: function () {
+ if(this.get('tableFilteringComplete')){
+ this.updateFilter(1, this.get('controller.filterObject.id'), 'string');
+ this.updateFilter(2, this.get('controller.filterObject.user'), 'string');
+ this.updateFilter(4, this.get('controller.filterObject.windowEnd'), 'date');
+ }
+ }.observes(
+ 'controller.filterObject.id',
+ 'controller.filterObject.user',
+ 'controller.filterObject.windowEnd'
+ ),
+
+ sortView: App.Sorts.wrapperView,
+
+ idSort: App.Sorts.fieldView.extend({
+ column: 1,
+ name: 'id',
+ displayName: Em.I18n.t('jobs.column.id'),
+ type: 'string'
+ }),
+
+ userSort: App.Sorts.fieldView.extend({
+ column: 2,
+ name: 'user',
+ displayName: Em.I18n.t('jobs.column.user'),
+ type: 'string'
+ }),
+
+ startTimeSort: App.Sorts.fieldView.extend({
+ column: 3,
+ name: 'startTime',
+ displayName: Em.I18n.t('jobs.column.start.time'),
+ type: 'number'
+ }),
+
+ endTimeSort: App.Sorts.fieldView.extend({
+ column: 4,
+ name: 'endTime',
+ displayName: Em.I18n.t('jobs.column.end.time'),
+ type: 'number'
+ }),
+
+ durationSort: App.Sorts.fieldView.extend({
+ column: 5,
+ name: 'duration',
+ displayName: Em.I18n.t('jobs.column.duration'),
+ type: 'number'
+ }),
+
+ /**
+ * Select View with list of "rows-per-page" options
+ * @type {Ember.View}
+ */
+ rowsPerPageSelectView: Ember.Select.extend({
+ content: ['10', '25', '50', '100', "250", "500"],
+ valueBinding: "controller.filterObject.jobsLimit",
+ attributeBindings: ['disabled'],
+ disabled: false,
+ disabledObserver: function () {
+ this.set('disabled', !!this.get("parentView.hasBackLinks"));
+ }.observes('parentView.hasBackLinks'),
+ change: function () {
+ this.get('controller').set('navIDs.nextID', '');
+ }
+ }),
+
+ /**
+ * return filtered number of all content number information displayed on the page footer bar
+ * @returns {String}
+ */
+ filteredJobs: function () {
+ return Em.I18n.t('jobs.filtered.jobs').fmt(this.get('controller.content.length'));
+ }.property('controller.content.length', 'controller.totalOfJobs'),
+
+ pageContentObserver: function () {
+ if (!this.get('controller.loading')) {
+ var tooltip = $('.tooltip');
+ if (tooltip.length) {
+ Ember.run.later(this, function() {
+ if (tooltip.length > 1) {
+ tooltip.first().remove();
+ }
+ }, 500);
+ }
+ }
+ }.observes('controller.loading'),
+
+ init: function() {
+ this._super();
+ App.tooltip($('body'), {
+ selector: '[rel="tooltip"]'
+ });
+ },
+
+ willDestroyElement : function() {
+ $('.tooltip').remove();
+ },
+
+ /**
+ * Filter-field for Jobs ID.
+ * Based on filters library
+ */
+ jobsIdFilterView: App.Filters.createTextView({
+ column: 1,
+ showApply: true,
+ setPropertyOnApply: 'controller.filterObject.id'
+ }),
+
+ /**
+ * Filter-list for User.
+ * Based on filters library
+ */
+ userFilterView: App.Filters.createTextView({
+ column: 2,
+ fieldType: 'input-small',
+ showApply: true,
+ setPropertyOnApply: 'controller.filterObject.user'
+ }),
+
+ /**
+ * Filter-field for Start Time.
+ * Based on filters library
+ */
+ startTimeFilterView: App.Filters.createSelectView({
+ fieldType: 'input-120',
+ column: 3,
+ content: ['Any', 'Past 1 hour', 'Past 1 Day', 'Past 2 Days', 'Past 7 Days', 'Past 14 Days', 'Past 30 Days', 'Custom'],
+ valueBinding: "controller.filterObject.startTime",
+ onChangeValue: function () {
+ this.get('parentView').updateFilter(this.get('column'), this.get('value'), 'date');
+ }
+ }),
+
+ jobNameView: Em.View.extend({
+
+ isLink: 'is-not-link',
+
+ isLinkObserver: function () {
+ this.refreshLinks();
+ }.observes('controller.sortingDone'),
+
+ refreshLinks: function () {
+ this.set('isLink', this.get('job.hasTezDag') ? "" : "is-not-link");
+ },
+
+ templateName: 'jobs/jobs_name',
+
+ click: function(event) {
+ /*if (this.get('job.hasTezDag')) {
+ App.router.transitionTo('main.jobs.jobDetails', this.get('job'));
+ }*/
+ return false;
+ },
+
+ didInsertElement: function () {
+ this.refreshLinks();
+ }
+ }),
+
+ /**
+ * associations between content (jobs list) property and column index
+ */
+ colPropAssoc: function () {
+ var associations = [];
+ associations[1] = 'id';
+ associations[2] = 'user';
+ associations[3] = 'startTime';
+ associations[4] = 'endTime';
+ return associations;
+ }.property(),
+
+ clearFilters: function() {
+ this.get('childViews').forEach(function(childView) {
+ if (childView['clearFilter']) {
+ childView.clearFilter();
+ }
+ });
+ },
+
+ jobFailMessage: function() {
+ return Em.I18n.t('jobs.table.job.fail');
+ }.property(),
+
+ jobsPaginationLeft: Ember.View.extend({
+ tagName: 'a',
+ templateName: 'table/navigation/pagination_left',
+ classNameBindings: ['class'],
+ class: function () {
+ if (this.get("parentView.hasBackLinks") && !this.get('controller.filterObject.isAnyFilterApplied')) {
+ return "paginate_previous";
+ }
+ return "paginate_disabled_previous";
+ }.property('parentView.hasBackLinks', 'controller.filterObject.isAnyFilterApplied'),
+
+ click: function () {
+ if (this.get("parentView.hasBackLinks") && !this.get('controller.filterObject.isAnyFilterApplied')) {
+ this.get('controller').navigateBack();
+ }
+ }
+ }),
+
+ jobsPaginationRight: Ember.View.extend({
+ tagName: 'a',
+ templateName: 'table/navigation/pagination_right',
+ classNameBindings: ['class'],
+ class: function () {
+ if (this.get("parentView.hasNextJobs") && !this.get('controller.filterObject.isAnyFilterApplied')) {
+ return "paginate_next";
+ }
+ return "paginate_disabled_next";
+ }.property("parentView.hasNextJobs", 'controller.filterObject.isAnyFilterApplied'),
+
+ click: function () {
+ if (this.get("parentView.hasNextJobs") && !this.get('controller.filterObject.isAnyFilterApplied')) {
+ this.get('controller').navigateNext();
+ }
+ }
+ }),
+
+ hasNextJobs: function() {
+ return (this.get("controller.navIDs.nextID.length") > 0);
+ }.property('controller.navIDs.nextID'),
+
+ hasBackLinks: function() {
+ return (this.get("controller.navIDs.backIDs").length > 1);
+ }.property('controller.navIDs.backIDs.[].length')
+
+});
diff --git contrib/views/jobs/src/main/resources/ui/app/scripts/views/sort_view.js contrib/views/jobs/src/main/resources/ui/app/scripts/views/sort_view.js
new file mode 100644
index 0000000..a8d5d39
--- /dev/null
+++ contrib/views/jobs/src/main/resources/ui/app/scripts/views/sort_view.js
@@ -0,0 +1,253 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Wrapper View for all sort components. Layout template and common actions are located inside of it.
+ * Logic specific for sort fields
+ * located in inner view - fieldView.
+ *
+ * @type {*}
+ */
+var wrapperView = Em.View.extend({
+ tagName: 'tr',
+
+ classNames: ['sort-wrapper'],
+
+ willInsertElement: function () {
+ if (this.get('parentView.tableFilteringComplete')) {
+ this.get('parentView').set('filteringComplete', true);
+ }
+ },
+
+ /**
+ * Load sort statuses from local storage
+ * Works only after finish filtering in the parent View
+ */
+ loadSortStatuses: function () {
+
+ }.observes('parentView.filteringComplete'),
+
+ /**
+ * Save sort statuses to local storage
+ * Works only after finish filtering in the parent View
+ */
+ saveSortStatuses: function () {
+ if (!this.get('parentView.filteringComplete')) return;
+
+ var statuses = [];
+ this.get('childViews').forEach(function (childView) {
+ statuses.push({
+ name: childView.get('name'),
+ status: childView.get('status')
+ });
+ });
+ },
+
+ /**
+ * sort content by property
+ * @param property {object}
+ * @param order {Boolean} true - DESC, false - ASC
+ * @param returnSorted {Boolean}
+ */
+ sort: function (property, order, returnSorted) {
+ var content = this.get('content').toArray();
+ var sortFunc = this.getSortFunc(property, order);
+ var status = order ? 'sorting_desc' : 'sorting_asc';
+
+ this.resetSort();
+ this.get('childViews').findProperty('name', property.get('name')).set('status', status);
+ this.saveSortStatuses(property, order);
+ content.sort(sortFunc);
+
+ if (!!returnSorted) {
+ return content;
+ } else {
+ this.set('content', content);
+ }
+ },
+
+ isSorting: false,
+
+ onContentChange: function () {
+ if (!this.get('isSorting') && this.get('content.length')) {
+ this.get('childViews').forEach(function (view) {
+ if (view.status !== 'sorting') {
+ var status = view.get('status');
+ this.set('isSorting', true);
+ this.sort(view, status == 'sorting_desc');
+ this.set('isSorting', false);
+ view.set('status', status);
+ }
+ }, this);
+ }
+ }.observes('content.length'),
+
+ /**
+ * reset all sorts fields
+ */
+ resetSort: function () {
+ this.get('childViews').setEach('status', 'sorting');
+ },
+ /**
+ * determines sort function depending on the type of sort field
+ * @param property
+ * @param order
+ * @return {*}
+ */
+ getSortFunc: function (property, order) {
+ var func;
+ switch (property.get('type')) {
+ case 'ip':
+ func = function (a, b) {
+ a = App.Helpers.misc.ipToInt(a.get(property.get('name')));
+ b = App.Helpers.misc.ipToInt(b.get(property.get('name')));
+ return order ? (b - a) : (a - b);
+ };
+ break;
+ case 'number':
+ func = function (a, b) {
+ a = parseFloat(a.get(property.get('name')));
+ b = parseFloat(b.get(property.get('name')));
+ return order ? (b - a) : (a - b);
+ };
+ break;
+ default:
+ func = function (a, b) {
+ if (order) {
+ if (a.get(property.get('name')) > b.get(property.get('name')))
+ return -1;
+ if (a.get(property.get('name')) < b.get(property.get('name')))
+ return 1;
+ return 0;
+ } else {
+ if (a.get(property.get('name')) < b.get(property.get('name')))
+ return -1;
+ if (a.get(property.get('name')) > b.get(property.get('name')))
+ return 1;
+ return 0;
+ }
+ }
+ }
+ return func;
+ }
+});
+
+/**
+ * view that carry on sorting on server-side via refresh() in parentView
+ * @type {*}
+ */
+var serverWrapperView = Em.View.extend({
+ tagName: 'tr',
+
+ classNames: ['sort-wrapper'],
+
+ willInsertElement: function () {
+ this.loadSortStatuses();
+ },
+
+ /**
+ * Initialize and save sorting statuses: publicHostName sorting_asc
+ */
+ loadSortStatuses: function () {
+ var statuses = [];
+ var childViews = this.get('childViews');
+ childViews.forEach(function (childView) {
+ var sortStatus = (childView.get('name') == 'publicHostName' && childView.get('status') == 'sorting') ? 'sorting_asc' : childView.get('status');
+ statuses.push({
+ name: childView.get('name'),
+ status: sortStatus
+ });
+ childView.set('status', sortStatus);
+ });
+ this.get('controller').set('sortingColumn', childViews.findProperty('name', 'publicHostName'));
+ },
+
+ /**
+ * Save sort statuses to local storage
+ * Works only after finish filtering in the parent View
+ */
+ saveSortStatuses: function () {
+ var statuses = [];
+ this.get('childViews').forEach(function (childView) {
+ statuses.push({
+ name: childView.get('name'),
+ status: childView.get('status')
+ });
+ });
+ },
+
+ /**
+ * sort content by property
+ * @param property {object}
+ * @param order {Boolean} true - DESC, false - ASC
+ */
+ sort: function (property, order) {
+ var status = order ? 'sorting_desc' : 'sorting_asc';
+
+ this.resetSort();
+ this.get('childViews').findProperty('name', property.get('name')).set('status', status);
+ this.saveSortStatuses();
+ this.get('parentView').refresh();
+ },
+
+ /**
+ * reset all sorts fields
+ */
+ resetSort: function () {
+ this.get('childViews').setEach('status', 'sorting');
+ }
+});
+
+/**
+ * particular view that contain sort field properties:
+ * name - name of property in content table
+ * type(optional) - specific type to sort
+ * displayName - label to display
+ * @type {*}
+ */
+var fieldView = Em.View.extend({
+ templateName: 'sort_field_template',
+ classNameBindings: ['viewNameClass'],
+ tagName: 'th',
+ name: null,
+ displayName: null,
+ status: 'sorting',
+ viewNameClass: function () {
+ return 'sort-view-' + this.get('column');
+ }.property(),
+ type: null,
+ column: 0,
+ /**
+ * callback that run sorting and define order of sorting
+ * @param event
+ */
+ click: function (event) {
+ this.get('parentView').sort(this, (this.get('status') !== 'sorting_desc'));
+ this.get('controller').set('sortingColumn', this);
+ }
+});
+
+/**
+ * Result object, which will be accessible outside
+ * @type {Object}
+ */
+App.Sorts = {
+ serverWrapperView: serverWrapperView,
+ wrapperView: wrapperView,
+ fieldView: fieldView
+};
diff --git contrib/views/jobs/src/main/resources/ui/app/scripts/views/table_view.js contrib/views/jobs/src/main/resources/ui/app/scripts/views/table_view.js
new file mode 100644
index 0000000..a404a49
--- /dev/null
+++ contrib/views/jobs/src/main/resources/ui/app/scripts/views/table_view.js
@@ -0,0 +1,362 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+App.TableView = Em.View.extend({
+
+ /**
+ * Defines to show pagination or show all records
+ * @type {Boolean}
+ */
+ pagination: true,
+
+ /**
+ * Shows if all data is loaded and filtered
+ * @type {Boolean}
+ */
+ filteringComplete: false,
+
+ /**
+ * intermediary for filteringComplete
+ * @type {Boolean}
+ */
+ tableFilteringComplete: false,
+
+ /**
+ * The number of rows to show on every page
+ * The value should be a number converted into string type in order to support select element API
+ * Example: "10", "25"
+ * @type {String}
+ */
+ displayLength: '10',
+
+ /**
+ * default value of display length
+ * The value should be a number converted into string type in order to support select element API
+ * Example: "10", "25"
+ */
+ defaultDisplayLength: "10",
+
+ /**
+ * number of hosts in table after applying filters
+ */
+ filteredCount: function () {
+ return this.get('filteredContent.length');
+ }.property('filteredContent.length'),
+
+ /**
+ * Do filtering, using saved in the local storage filter conditions
+ */
+ willInsertElement:function () {
+ this.initFilters();
+ },
+
+ /**
+ * initialize filters
+ * restore values from local DB
+ * or clear filters in case there is no filters to restore
+ */
+ initFilters: function () {
+ this.clearFilters();
+ this.set('tableFilteringComplete', true);
+ },
+
+ /**
+ * Return pagination information displayed on the page
+ * @type {String}
+ */
+ paginationInfo: function () {
+ return this.t('tableView.filters.paginationInfo').format(this.get('startIndex'), this.get('endIndex'), this.get('filteredCount'));
+ }.property('filteredCount', 'endIndex'),
+
+ paginationLeft: Ember.View.extend({
+ tagName: 'a',
+ templateName: 'table/navigation/pagination_left',
+ classNameBindings: ['class'],
+ class: function () {
+ if (this.get("parentView.startIndex") > 1) {
+ return "paginate_previous";
+ }
+ return "paginate_disabled_previous";
+ }.property("parentView.startIndex", 'parentView.filteredCount'),
+
+ click: function () {
+ if (this.get('class') === "paginate_previous") {
+ this.get('parentView').previousPage();
+ }
+ }
+ }),
+
+ paginationRight: Ember.View.extend({
+ tagName: 'a',
+ templateName: 'table/navigation/pagination_right',
+ classNameBindings: ['class'],
+ class: function () {
+ if ((this.get("parentView.endIndex")) < this.get("parentView.filteredCount")) {
+ return "paginate_next";
+ }
+ return "paginate_disabled_next";
+ }.property("parentView.endIndex", 'parentView.filteredCount'),
+
+ click: function () {
+ if (this.get('class') === "paginate_next") {
+ this.get('parentView').nextPage();
+ }
+ }
+ }),
+
+ paginationFirst: Ember.View.extend({
+ tagName: 'a',
+ templateName: 'table/navigation/pagination_first',
+ classNameBindings: ['class'],
+ class: function () {
+ if ((this.get("parentView.endIndex")) > parseInt(this.get("parentView.displayLength"))) {
+ return "paginate_previous";
+ }
+ return "paginate_disabled_previous";
+ }.property("parentView.endIndex", 'parentView.filteredCount'),
+
+ click: function () {
+ if (this.get('class') === "paginate_previous") {
+ this.get('parentView').firstPage();
+ }
+ }
+ }),
+
+ paginationLast: Ember.View.extend({
+ tagName: 'a',
+ templateName: 'table/navigation/pagination_last',
+ classNameBindings: ['class'],
+ class: function () {
+ if (this.get("parentView.endIndex") !== this.get("parentView.filteredCount")) {
+ return "paginate_next";
+ }
+ return "paginate_disabled_next";
+ }.property("parentView.endIndex", 'parentView.filteredCount'),
+
+ click: function () {
+ if (this.get('class') === "paginate_next") {
+ this.get('parentView').lastPage();
+ }
+ }
+ }),
+
+ /**
+ * Select View with list of "rows-per-page" options
+ * @type {Ember.View}
+ */
+ rowsPerPageSelectView: Em.Select.extend({
+ content: ['10', '25', '50', '100'],
+ change: function () {
+ this.get('parentView').saveDisplayLength();
+ }
+ }),
+
+ /**
+ * Start index for displayed content on the page
+ */
+ startIndex: 1,
+
+ /**
+ * Calculate end index for displayed content on the page
+ */
+ endIndex: function () {
+ if (this.get('pagination') && this.get('displayLength')) {
+ return Math.min(this.get('filteredCount'), this.get('startIndex') + parseInt(this.get('displayLength')) - 1);
+ } else {
+ return this.get('filteredCount') || 0;
+ }
+ }.property('startIndex', 'displayLength', 'filteredCount'),
+
+ /**
+ * Onclick handler for previous page button on the page
+ */
+ previousPage: function () {
+ var result = this.get('startIndex') - parseInt(this.get('displayLength'));
+ this.set('startIndex', (result < 2) ? 1 : result);
+ },
+
+ /**
+ * Onclick handler for next page button on the page
+ */
+ nextPage: function () {
+ var result = this.get('startIndex') + parseInt(this.get('displayLength'));
+ if (result - 1 < this.get('filteredCount')) {
+ this.set('startIndex', result);
+ }
+ },
+ /**
+ * Onclick handler for first page button on the page
+ */
+ firstPage: function () {
+ this.set('startIndex', 1);
+ },
+ /**
+ * Onclick handler for last page button on the page
+ */
+ lastPage: function () {
+ var pagesCount = this.get('filteredCount') / parseInt(this.get('displayLength'));
+ var startIndex = (this.get('filteredCount') % parseInt(this.get('displayLength')) === 0) ?
+ (pagesCount - 1) * parseInt(this.get('displayLength')) :
+ Math.floor(pagesCount) * parseInt(this.get('displayLength'));
+ this.set('startIndex', ++startIndex);
+ },
+
+ /**
+ * Calculates default value for startIndex property after applying filter or changing displayLength
+ */
+ updatePaging: function (controller, property) {
+ var displayLength = this.get('displayLength');
+ var filteredContentLength = this.get('filteredCount');
+ if (property == 'displayLength') {
+ this.set('startIndex', Math.min(1, filteredContentLength));
+ }
+ else
+ if (!filteredContentLength) {
+ this.set('startIndex', 0);
+ }
+ else
+ if (this.get('startIndex') > filteredContentLength) {
+ this.set('startIndex', Math.floor((filteredContentLength - 1) / displayLength) * displayLength + 1);
+ }
+ else
+ if (!this.get('startIndex')) {
+ this.set('startIndex', 1);
+ }
+ }.observes('displayLength', 'filteredCount'),
+
+ /**
+ * Apply each filter to each row
+ *
+ * @param {Number} iColumn number of column by which filter
+ * @param {Object} value
+ * @param {String} type
+ */
+ updateFilter: function (iColumn, value, type) {
+ var filterCondition = this.get('filterConditions').findProperty('iColumn', iColumn);
+ if (filterCondition) {
+ filterCondition.value = value;
+ }
+ else {
+ filterCondition = {
+ iColumn: iColumn,
+ value: value,
+ type: type
+ };
+ this.get('filterConditions').push(filterCondition);
+ }
+ this.filtersUsedCalc();
+ this.filter();
+ },
+
+ /**
+ * Contain filter conditions for each column
+ * @type {Array}
+ */
+ filterConditions: [],
+
+ /**
+ * Contains content after implementing filters
+ * @type {Array}
+ */
+ filteredContent: [],
+
+ /**
+ * Determine if filteredContent is empty or not
+ * @type {Boolean}
+ */
+ hasFilteredItems: function() {
+ return !!this.get('filteredCount');
+ }.property('filteredCount'),
+
+ /**
+ * Contains content to show on the current page of data page view
+ * @type {Array}
+ */
+ pageContent: function () {
+ return this.get('filteredContent').slice(this.get('startIndex') - 1, this.get('endIndex'));
+ }.property('filteredCount', 'startIndex', 'endIndex'),
+
+ /**
+ * Filter table by filterConditions
+ */
+ filter: function () {
+ var content = this.get('content');
+ var filterConditions = this.get('filterConditions').filterProperty('value');
+ var result;
+ var assoc = this.get('colPropAssoc');
+ if (filterConditions.length) {
+ result = content.filter(function (item) {
+ var match = true;
+ filterConditions.forEach(function (condition) {
+ var filterFunc = App.Filters.getFilterByType(condition.type, false);
+ if (match) {
+ match = filterFunc(item.get(assoc[condition.iColumn]), condition.value);
+ }
+ });
+ return match;
+ });
+ this.set('filteredContent', result);
+ } else {
+ this.set('filteredContent', content.toArray());
+ }
+ }.observes('content.length'),
+
+ /**
+ * Does any filter is used on the page
+ * @type {Boolean}
+ */
+ filtersUsed: false,
+
+ /**
+ * Determine if some filters are used on the page
+ * Set filtersUsed value
+ */
+ filtersUsedCalc: function() {
+ var filterConditions = this.get('filterConditions');
+ if (!filterConditions.length) {
+ this.set('filtersUsed', false);
+ return;
+ }
+ var filtersUsed = false;
+ filterConditions.forEach(function(filterCondition) {
+ if (filterCondition.value.toString() !== '') {
+ filtersUsed = true;
+ }
+ });
+ this.set('filtersUsed', filtersUsed);
+ },
+
+ /**
+ * Run clearFilter in the each child filterView
+ */
+ clearFilters: function() {
+ this.set('filterConditions', []);
+ this.get('_childViews').forEach(function(childView) {
+ if (childView['clearFilter']) {
+ childView.clearFilter();
+ }
+ });
+ },
+
+ actions: {
+ actionClearFilters: function() {
+ this.clearFilters();
+ }
+ }
+
+});
diff --git contrib/views/jobs/src/main/resources/ui/app/styles/main.less contrib/views/jobs/src/main/resources/ui/app/styles/main.less
index c1997c2..8cd95dc 100644
--- contrib/views/jobs/src/main/resources/ui/app/styles/main.less
+++ contrib/views/jobs/src/main/resources/ui/app/styles/main.less
@@ -16,4 +16,303 @@
* limitations under the License.
*/
-@import '../../app/bower_components/bootstrap/less/bootstrap';
\ No newline at end of file
+@import '../../app/bower_components/bootstrap/less/bootstrap';
+
+#jobs {
+
+ .jobs-type {
+ float: right;
+ margin-top: -24px;
+ }
+
+ .new-jobs-link {
+ float: left;
+ margin-left: 496px;
+ margin-top: -20px;
+ }
+
+ #filtered-jobs{
+ float: left;
+ margin-top: 8px;
+ }
+
+ .jobs_head{
+ height: 30px;
+ }
+
+ .page-bar {
+ border: 1px solid #E4E4E4;
+ color: #7B7B7B;
+ text-align: right;
+ font-size: 12px;
+ label {
+ font-size: 12px;
+ }
+ div {
+ display: inline-block;
+ margin:0 10px;
+ }
+ .items-on-page {
+ label {
+ display:inline;
+ }
+ select {
+ margin-bottom: 4px;
+ margin-top: 4px;
+ width:70px;
+ font-size: 12px;
+ height: 27px;
+ }
+ }
+
+ .paging_two_button {
+ a {
+ padding:0 5px;
+ }
+ a.paginate_disabled_next, a.paginate_disabled_previous {
+ color: gray;
+ &:hover i{
+ color: gray;
+ text-decoration: none;
+ cursor: default;
+ }
+ }
+
+ a.paginate_next, a.paginate_previous {
+ &:hover {
+ text-decoration: none;
+ cursor: pointer;
+ }
+ }
+ }
+ }
+
+ #jobs-table {
+
+ .is-not-link{
+ cursor: default;
+ color: #000000;
+ text-decoration: none;
+ }
+
+ .apply-btn {
+ font-size: 12px;
+ padding: 0px 8px;
+ margin-left: 6px;
+ margin-top: -8px;
+ line-height: 22px;
+ }
+
+ .input-120{
+ width: 120px;
+ }
+
+ .label-row {
+ font-size: 0.9em;
+ th {
+ padding: 4px 4px 4px 8px;
+ }
+ .active-sort {
+ color: #555555;
+ text-decoration: none;
+ background-color: #e5e5e5;
+ -webkit-box-shadow: inset 0 5px 8px rgba(0, 0, 0, 0.100);
+ -moz-box-shadow: inset 0 5px 8px rgba(0, 0, 0, 0.100);
+ box-shadow: inset 0 5px 8px rgba(0, 0, 0, 0.100);
+ }
+ }
+ thead {
+ background: none repeat scroll 0 0 #F8F8F8;
+ }
+ #filter-row {
+ th {
+ padding: 0px;
+ padding-left: 8px;
+ }
+ .active-filter {
+ color: #555555;
+ text-decoration: none;
+ background-color: #e5e5e5;
+ -webkit-box-shadow: inset 0 -5px 8px rgba(0, 0, 0, 0.05);
+ -moz-box-shadow: inset 0 -5px 8px rgba(0, 0, 0, 0.05);
+ box-shadow: inset 0 -5px 8px rgba(0, 0, 0, 0.05);
+ }
+ input {
+ font-size: 12px;
+ height: 14px;
+ }
+ select {
+ height: 27px;
+ font-size: 12px;
+ }
+ .start-time a.ui-icon-circle-close {
+ margin-top: 7px;
+ }
+ .filter-btn {
+ color: #999999;
+ font-size: 12px;
+ line-height: 14px;
+ padding-left: 6px;
+ text-align: left;
+ width: 100px;
+ .icon-filter {
+ color: #999999;
+ }
+ }
+ }
+ th {
+ border-top: none;
+ }
+ th, td {
+ border-left-width: 0;
+ }
+ .no-data{
+ text-align: center;
+ }
+ a.job-link {
+ width: 100%;
+ overflow: auto;
+ word-wrap: break-word;
+ display: inline-block;
+ }
+ .tooltip-inner {
+ text-align: left;
+ max-width: 400px !important;
+ }
+ td:first-child,
+ th:first-child {
+ border-left-width: 1px;
+ width: 14px;
+ }
+ td:first-child + td,
+ th:first-child + th {
+ width: 36%;
+ }
+ td:first-child + td + td,
+ th:first-child + th + th{
+ width: 20%;
+ }
+ td:first-child + td + td + td,
+ th:first-child + th + th + th,
+ td:first-child + td + td + td + td,
+ th:first-child + th + th + th + th{
+ width: 16%;
+ }
+ td:first-child + td + td + td + td + td,
+ th:first-child + th + th + th + th + th{
+ width: 12%;
+ }
+ }
+ .table {
+ table-layout: fixed;
+ th {
+ border-top: none;
+ }
+ ul.filter-components {
+ padding: 5px 0;
+ background: #777777;
+ color: #ffffff;
+ font-weight: normal;
+ font-size: 12px;
+ label {
+ font-size: 12px;
+ }
+ li {
+ display: block;
+ padding: 3px 0 3px 5px;
+ line-height: 20px;
+ label.checkbox {
+ padding-left: 3px;
+ }
+ input[type="checkbox"] {
+ margin: 4px 4px 2px 2px;
+ }
+ }
+ li#title-bar {
+ text-align: left;
+ border-bottom: 1px solid #e4e4e4;
+ a.close {
+ background: #777777;
+ display: inline;
+ color: #ffffff;
+ padding-left: 35px;
+ padding-right: 12px;
+ text-shadow: 0 1px 0 #ffffff;
+ float: none;
+ font-size: 10px;
+ opacity: 0.6;
+ }
+ a.close:hover {
+ background: #777777;
+ opacity: 1.0;
+ }
+ }
+ li#selector-bar {
+ text-align: left;
+ border-bottom: 1px solid #e4e4e4;
+ font-size: 6px;
+ }
+ li#list-area {
+ font-weight: normal;
+ text-align: left;
+ }
+ li#button-bar {
+ text-align: center;
+ button {
+ font-size: 12px;
+ }
+ }
+ ul {
+ margin-left: 10px;
+ }
+ &>li {
+ &>ul {
+ height: 150px;
+ margin-left: 0;
+ overflow-y: scroll;
+ }
+ }
+ }
+
+ .sorting_asc {
+ background:
+ url(data:image/png;base64,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) no-repeat right 50%;
+ }
+ .sorting_desc {
+ background: url(data:image/png;base64,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) no-repeat right 50%;
+ }
+ .sorting {
+ background: url( data:image/png;base64,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) no-repeat right 50%;
+ }
+ div.view-wrapper {
+ input[type="checkbox"], .btn-group {
+ margin-bottom: 9px;
+ }
+ }
+
+ a.ui-icon-circle-close {
+ float: right;
+ opacity: 0.2;
+ padding: 1px 0;
+ position: relative;
+ right: 0px;
+ margin-top: 3px;
+ z-index: 10;
+ &:hover {
+ opacity: 0.7;
+ }
+ }
+ .notActive {
+ a.ui-icon-circle-close {
+ visibility: hidden;
+ }
+ }
+ }
+}
+
+.sort-wrapper {
+ .column-name {
+ cursor: pointer;
+ padding-right: 18px;
+ }
+}
\ No newline at end of file
diff --git contrib/views/jobs/src/main/resources/ui/app/templates/jobs.hbs contrib/views/jobs/src/main/resources/ui/app/templates/jobs.hbs
new file mode 100644
index 0000000..d43f5f0
--- /dev/null
+++ contrib/views/jobs/src/main/resources/ui/app/templates/jobs.hbs
@@ -0,0 +1,90 @@
+{{!
+* Licensed to the Apache Software Foundation (ASF) under one
+* or more contributor license agreements. See the NOTICE file
+* distributed with this work for additional information
+* regarding copyright ownership. The ASF licenses this file
+* to you under the Apache License, Version 2.0 (the
+* "License"); you may not use this file except in compliance
+* with the License. You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+}}
+
+
+| + {{view view.parentView.idSort}} + {{view view.parentView.userSort}} + {{view view.parentView.startTimeSort}} + {{view view.parentView.endTimeSort}} + {{view view.parentView.durationSort}} + {{/view}} + + | |||||
|---|---|---|---|---|---|
| + | {{view view.jobsIdFilterView}} | +{{view view.userFilterView}} | +{{view view.startTimeFilterView}} | ++ | + |
| + {{controller.jobsMessage}} + | +|||||
| + {{#if job.failed}} + + {{/if}} + | +{{view view.jobNameView jobBinding="job"}} | +{{job.user}} | +{{job.startTimeDisplay}} | +{{job.endTimeDisplay}} | +{{job.durationDisplay}} | +