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,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAKQWlDQ1BJQ0MgUHJvZmlsZQAASA2dlndUU9kWh8+9N73QEiIgJfQaegkg0jtIFQRRiUmAUAKGhCZ2RAVGFBEpVmRUwAFHhyJjRRQLg4Ji1wnyEFDGwVFEReXdjGsJ7601896a/cdZ39nnt9fZZ+9917oAUPyCBMJ0WAGANKFYFO7rwVwSE8vE9wIYEAEOWAHA4WZmBEf4RALU/L09mZmoSMaz9u4ugGS72yy/UCZz1v9/kSI3QyQGAApF1TY8fiYX5QKUU7PFGTL/BMr0lSkyhjEyFqEJoqwi48SvbPan5iu7yZiXJuShGlnOGbw0noy7UN6aJeGjjAShXJgl4GejfAdlvVRJmgDl9yjT0/icTAAwFJlfzOcmoWyJMkUUGe6J8gIACJTEObxyDov5OWieAHimZ+SKBIlJYqYR15hp5ejIZvrxs1P5YjErlMNN4Yh4TM/0tAyOMBeAr2+WRQElWW2ZaJHtrRzt7VnW5mj5v9nfHn5T/T3IevtV8Sbsz55BjJ5Z32zsrC+9FgD2JFqbHbO+lVUAtG0GQOXhrE/vIADyBQC03pzzHoZsXpLE4gwnC4vs7GxzAZ9rLivoN/ufgm/Kv4Y595nL7vtWO6YXP4EjSRUzZUXlpqemS0TMzAwOl89k/fcQ/+PAOWnNycMsnJ/AF/GF6FVR6JQJhIlou4U8gViQLmQKhH/V4X8YNicHGX6daxRodV8AfYU5ULhJB8hvPQBDIwMkbj96An3rWxAxCsi+vGitka9zjzJ6/uf6Hwtcim7hTEEiU+b2DI9kciWiLBmj34RswQISkAd0oAo0gS4wAixgDRyAM3AD3iAAhIBIEAOWAy5IAmlABLJBPtgACkEx2AF2g2pwANSBetAEToI2cAZcBFfADXALDIBHQAqGwUswAd6BaQiC8BAVokGqkBakD5lC1hAbWgh5Q0FQOBQDxUOJkBCSQPnQJqgYKoOqoUNQPfQjdBq6CF2D+qAH0CA0Bv0BfYQRmALTYQ3YALaA2bA7HAhHwsvgRHgVnAcXwNvhSrgWPg63whfhG/AALIVfwpMIQMgIA9FGWAgb8URCkFgkAREha5EipAKpRZqQDqQbuY1IkXHkAwaHoWGYGBbGGeOHWYzhYlZh1mJKMNWYY5hWTBfmNmYQM4H5gqVi1bGmWCesP3YJNhGbjS3EVmCPYFuwl7ED2GHsOxwOx8AZ4hxwfrgYXDJuNa4Etw/XjLuA68MN4SbxeLwq3hTvgg/Bc/BifCG+Cn8cfx7fjx/GvyeQCVoEa4IPIZYgJGwkVBAaCOcI/YQRwjRRgahPdCKGEHnEXGIpsY7YQbxJHCZOkxRJhiQXUiQpmbSBVElqIl0mPSa9IZPJOmRHchhZQF5PriSfIF8lD5I/UJQoJhRPShxFQtlOOUq5QHlAeUOlUg2obtRYqpi6nVpPvUR9Sn0vR5Mzl/OX48mtk6uRa5Xrl3slT5TXl3eXXy6fJ18hf0r+pvy4AlHBQMFTgaOwVqFG4bTCPYVJRZqilWKIYppiiWKD4jXFUSW8koGStxJPqUDpsNIlpSEaQtOledK4tE20Otpl2jAdRzek+9OT6cX0H+i99AllJWVb5SjlHOUa5bPKUgbCMGD4M1IZpYyTjLuMj/M05rnP48/bNq9pXv+8KZX5Km4qfJUilWaVAZWPqkxVb9UU1Z2qbapP1DBqJmphatlq+9Uuq43Pp893ns+dXzT/5PyH6rC6iXq4+mr1w+o96pMamhq+GhkaVRqXNMY1GZpumsma5ZrnNMe0aFoLtQRa5VrntV4wlZnuzFRmJbOLOaGtru2nLdE+pN2rPa1jqLNYZ6NOs84TXZIuWzdBt1y3U3dCT0svWC9fr1HvoT5Rn62fpL9Hv1t/ysDQINpgi0GbwaihiqG/YZ5ho+FjI6qRq9Eqo1qjO8Y4Y7ZxivE+41smsImdSZJJjclNU9jU3lRgus+0zwxr5mgmNKs1u8eisNxZWaxG1qA5wzzIfKN5m/krCz2LWIudFt0WXyztLFMt6ywfWSlZBVhttOqw+sPaxJprXWN9x4Zq42Ozzqbd5rWtqS3fdr/tfTuaXbDdFrtOu8/2DvYi+yb7MQc9h3iHvQ732HR2KLuEfdUR6+jhuM7xjOMHJ3snsdNJp9+dWc4pzg3OowsMF/AX1C0YctFx4bgccpEuZC6MX3hwodRV25XjWuv6zE3Xjed2xG3E3dg92f24+ysPSw+RR4vHlKeT5xrPC16Il69XkVevt5L3Yu9q76c+Oj6JPo0+E752vqt9L/hh/QL9dvrd89fw5/rX+08EOASsCegKpARGBFYHPgsyCRIFdQTDwQHBu4IfL9JfJFzUFgJC/EN2hTwJNQxdFfpzGC4sNKwm7Hm4VXh+eHcELWJFREPEu0iPyNLIR4uNFksWd0bJR8VF1UdNRXtFl0VLl1gsWbPkRoxajCCmPRYfGxV7JHZyqffS3UuH4+ziCuPuLjNclrPs2nK15anLz66QX8FZcSoeGx8d3xD/iRPCqeVMrvRfuXflBNeTu4f7kufGK+eN8V34ZfyRBJeEsoTRRJfEXYljSa5JFUnjAk9BteB1sl/ygeSplJCUoykzqdGpzWmEtPi000IlYYqwK10zPSe9L8M0ozBDuspp1e5VE6JA0ZFMKHNZZruYjv5M9UiMJJslg1kLs2qy3mdHZZ/KUcwR5vTkmuRuyx3J88n7fjVmNXd1Z752/ob8wTXuaw6thdauXNu5Tnddwbrh9b7rj20gbUjZ8MtGy41lG99uit7UUaBRsL5gaLPv5sZCuUJR4b0tzlsObMVsFWzt3WazrWrblyJe0fViy+KK4k8l3JLr31l9V/ndzPaE7b2l9qX7d+B2CHfc3em681iZYlle2dCu4F2t5czyovK3u1fsvlZhW3FgD2mPZI+0MqiyvUqvakfVp+qk6oEaj5rmvep7t+2d2sfb17/fbX/TAY0DxQc+HhQcvH/I91BrrUFtxWHc4azDz+ui6rq/Z39ff0TtSPGRz0eFR6XHwo911TvU1zeoN5Q2wo2SxrHjccdv/eD1Q3sTq+lQM6O5+AQ4ITnx4sf4H++eDDzZeYp9qukn/Z/2ttBailqh1tzWibakNml7THvf6YDTnR3OHS0/m/989Iz2mZqzymdLz5HOFZybOZ93fvJCxoXxi4kXhzpXdD66tOTSna6wrt7LgZevXvG5cqnbvfv8VZerZ645XTt9nX297Yb9jdYeu56WX+x+aem172296XCz/ZbjrY6+BX3n+l37L972un3ljv+dGwOLBvruLr57/17cPel93v3RB6kPXj/Mejj9aP1j7OOiJwpPKp6qP6391fjXZqm99Oyg12DPs4hnj4a4Qy//lfmvT8MFz6nPK0a0RupHrUfPjPmM3Xqx9MXwy4yX0+OFvyn+tveV0auffnf7vWdiycTwa9HrmT9K3qi+OfrW9m3nZOjk03dp76anit6rvj/2gf2h+2P0x5Hp7E/4T5WfjT93fAn88ngmbWbm3/eE8/syOll+AAAACXBIWXMAAAsTAAALEwEAmpwYAAAA4klEQVQ4Ee2RPw8BQRDF3x4dCokL0SqUKqVSr/ZRruWTaEnUWgkShwji3yWCwoXQOCKCHXPq24hSmGJ3srvz5vdmga8NIhK1GhW2B8q+M+F/96DRRHE0hUEagegUEyK4VdVoqgv3fL2h3HAMQ3I+sQDLCpRdUlWNUux8prjZltXTRUIQ4X4T6HSRcRwkPxLj7r7ZHPXFSgO7A3xgwQfsncRghJKKzpPMPiBv9pBwDQmhgaTgnRU5zD7S86U3necH2CtQJIyKHkWKyXTGCrFZh4XtxxWt4x6eda9u/+U/gZ+dwBODrVwv7HA8iwAAAABJRU5ErkJggg==) 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,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAKQWlDQ1BJQ0MgUHJvZmlsZQAASA2dlndUU9kWh8+9N73QEiIgJfQaegkg0jtIFQRRiUmAUAKGhCZ2RAVGFBEpVmRUwAFHhyJjRRQLg4Ji1wnyEFDGwVFEReXdjGsJ7601896a/cdZ39nnt9fZZ+9917oAUPyCBMJ0WAGANKFYFO7rwVwSE8vE9wIYEAEOWAHA4WZmBEf4RALU/L09mZmoSMaz9u4ugGS72yy/UCZz1v9/kSI3QyQGAApF1TY8fiYX5QKUU7PFGTL/BMr0lSkyhjEyFqEJoqwi48SvbPan5iu7yZiXJuShGlnOGbw0noy7UN6aJeGjjAShXJgl4GejfAdlvVRJmgDl9yjT0/icTAAwFJlfzOcmoWyJMkUUGe6J8gIACJTEObxyDov5OWieAHimZ+SKBIlJYqYR15hp5ejIZvrxs1P5YjErlMNN4Yh4TM/0tAyOMBeAr2+WRQElWW2ZaJHtrRzt7VnW5mj5v9nfHn5T/T3IevtV8Sbsz55BjJ5Z32zsrC+9FgD2JFqbHbO+lVUAtG0GQOXhrE/vIADyBQC03pzzHoZsXpLE4gwnC4vs7GxzAZ9rLivoN/ufgm/Kv4Y595nL7vtWO6YXP4EjSRUzZUXlpqemS0TMzAwOl89k/fcQ/+PAOWnNycMsnJ/AF/GF6FVR6JQJhIlou4U8gViQLmQKhH/V4X8YNicHGX6daxRodV8AfYU5ULhJB8hvPQBDIwMkbj96An3rWxAxCsi+vGitka9zjzJ6/uf6Hwtcim7hTEEiU+b2DI9kciWiLBmj34RswQISkAd0oAo0gS4wAixgDRyAM3AD3iAAhIBIEAOWAy5IAmlABLJBPtgACkEx2AF2g2pwANSBetAEToI2cAZcBFfADXALDIBHQAqGwUswAd6BaQiC8BAVokGqkBakD5lC1hAbWgh5Q0FQOBQDxUOJkBCSQPnQJqgYKoOqoUNQPfQjdBq6CF2D+qAH0CA0Bv0BfYQRmALTYQ3YALaA2bA7HAhHwsvgRHgVnAcXwNvhSrgWPg63whfhG/AALIVfwpMIQMgIA9FGWAgb8URCkFgkAREha5EipAKpRZqQDqQbuY1IkXHkAwaHoWGYGBbGGeOHWYzhYlZh1mJKMNWYY5hWTBfmNmYQM4H5gqVi1bGmWCesP3YJNhGbjS3EVmCPYFuwl7ED2GHsOxwOx8AZ4hxwfrgYXDJuNa4Etw/XjLuA68MN4SbxeLwq3hTvgg/Bc/BifCG+Cn8cfx7fjx/GvyeQCVoEa4IPIZYgJGwkVBAaCOcI/YQRwjRRgahPdCKGEHnEXGIpsY7YQbxJHCZOkxRJhiQXUiQpmbSBVElqIl0mPSa9IZPJOmRHchhZQF5PriSfIF8lD5I/UJQoJhRPShxFQtlOOUq5QHlAeUOlUg2obtRYqpi6nVpPvUR9Sn0vR5Mzl/OX48mtk6uRa5Xrl3slT5TXl3eXXy6fJ18hf0r+pvy4AlHBQMFTgaOwVqFG4bTCPYVJRZqilWKIYppiiWKD4jXFUSW8koGStxJPqUDpsNIlpSEaQtOledK4tE20Otpl2jAdRzek+9OT6cX0H+i99AllJWVb5SjlHOUa5bPKUgbCMGD4M1IZpYyTjLuMj/M05rnP48/bNq9pXv+8KZX5Km4qfJUilWaVAZWPqkxVb9UU1Z2qbapP1DBqJmphatlq+9Uuq43Pp893ns+dXzT/5PyH6rC6iXq4+mr1w+o96pMamhq+GhkaVRqXNMY1GZpumsma5ZrnNMe0aFoLtQRa5VrntV4wlZnuzFRmJbOLOaGtru2nLdE+pN2rPa1jqLNYZ6NOs84TXZIuWzdBt1y3U3dCT0svWC9fr1HvoT5Rn62fpL9Hv1t/ysDQINpgi0GbwaihiqG/YZ5ho+FjI6qRq9Eqo1qjO8Y4Y7ZxivE+41smsImdSZJJjclNU9jU3lRgus+0zwxr5mgmNKs1u8eisNxZWaxG1qA5wzzIfKN5m/krCz2LWIudFt0WXyztLFMt6ywfWSlZBVhttOqw+sPaxJprXWN9x4Zq42Ozzqbd5rWtqS3fdr/tfTuaXbDdFrtOu8/2DvYi+yb7MQc9h3iHvQ732HR2KLuEfdUR6+jhuM7xjOMHJ3snsdNJp9+dWc4pzg3OowsMF/AX1C0YctFx4bgccpEuZC6MX3hwodRV25XjWuv6zE3Xjed2xG3E3dg92f24+ysPSw+RR4vHlKeT5xrPC16Il69XkVevt5L3Yu9q76c+Oj6JPo0+E752vqt9L/hh/QL9dvrd89fw5/rX+08EOASsCegKpARGBFYHPgsyCRIFdQTDwQHBu4IfL9JfJFzUFgJC/EN2hTwJNQxdFfpzGC4sNKwm7Hm4VXh+eHcELWJFREPEu0iPyNLIR4uNFksWd0bJR8VF1UdNRXtFl0VLl1gsWbPkRoxajCCmPRYfGxV7JHZyqffS3UuH4+ziCuPuLjNclrPs2nK15anLz66QX8FZcSoeGx8d3xD/iRPCqeVMrvRfuXflBNeTu4f7kufGK+eN8V34ZfyRBJeEsoTRRJfEXYljSa5JFUnjAk9BteB1sl/ygeSplJCUoykzqdGpzWmEtPi000IlYYqwK10zPSe9L8M0ozBDuspp1e5VE6JA0ZFMKHNZZruYjv5M9UiMJJslg1kLs2qy3mdHZZ/KUcwR5vTkmuRuyx3J88n7fjVmNXd1Z752/ob8wTXuaw6thdauXNu5Tnddwbrh9b7rj20gbUjZ8MtGy41lG99uit7UUaBRsL5gaLPv5sZCuUJR4b0tzlsObMVsFWzt3WazrWrblyJe0fViy+KK4k8l3JLr31l9V/ndzPaE7b2l9qX7d+B2CHfc3em681iZYlle2dCu4F2t5czyovK3u1fsvlZhW3FgD2mPZI+0MqiyvUqvakfVp+qk6oEaj5rmvep7t+2d2sfb17/fbX/TAY0DxQc+HhQcvH/I91BrrUFtxWHc4azDz+ui6rq/Z39ff0TtSPGRz0eFR6XHwo911TvU1zeoN5Q2wo2SxrHjccdv/eD1Q3sTq+lQM6O5+AQ4ITnx4sf4H++eDDzZeYp9qukn/Z/2ttBailqh1tzWibakNml7THvf6YDTnR3OHS0/m/989Iz2mZqzymdLz5HOFZybOZ93fvJCxoXxi4kXhzpXdD66tOTSna6wrt7LgZevXvG5cqnbvfv8VZerZ645XTt9nX297Yb9jdYeu56WX+x+aem172296XCz/ZbjrY6+BX3n+l37L972un3ljv+dGwOLBvruLr57/17cPel93v3RB6kPXj/Mejj9aP1j7OOiJwpPKp6qP6391fjXZqm99Oyg12DPs4hnj4a4Qy//lfmvT8MFz6nPK0a0RupHrUfPjPmM3Xqx9MXwy4yX0+OFvyn+tveV0auffnf7vWdiycTwa9HrmT9K3qi+OfrW9m3nZOjk03dp76anit6rvj/2gf2h+2P0x5Hp7E/4T5WfjT93fAn88ngmbWbm3/eE8/syOll+AAAACXBIWXMAAAsTAAALEwEAmpwYAAABmElEQVQ4EdWSv0vDQBTH7y4ZUkKhTdtYHArOUvwPdHAVpeBY3PwH/BfEycF/wclR6NzBxUFxKrgokRLaSkmhTZr+ADWJ32s5DeXaSkHBW97du/c+73vvHiF/vaIooj+pyZYFAaTbtn0DuzR2YQBX1G63K57n7TQajfNlhRfCfN8/6na7u4AS13VPOp3O/iLgXBgAa0i+/Hh7J5RSEoYh6fV6FfjX5wGlMCQwgKpQNs0Lo4kdjUYEz77FvSIDSmGA7DmOU+SKxGJkukeRDfTwWPjjVo0fxH48Hic1TbtmjBX5c2F1WA/3rSAI7obDoSVif81+vyNWAmNQHgwGB6qqbqHxOUVRklDkQ2ELCu+h+qJQKDzGUiZb6TPT6TTt9/uHABLeK947QFKE0RSyNg3DkM6c9AN0Xb9CwguUCNDXeKDQQyaTeZpVxc9SZVASQMk2frWFzyCTwUBDElqCmKZZxv10VmaIUmU8Bgmv+Xy+JNRxXzabraJfz3y/0mo2m2e1Wi2q1+sQG+VWgogkAKhlWaeY/pLw/T/7CTBQv9a27vsbAAAAAElFTkSuQmCC) 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. +}} + + +
+
+ {{#if controller.hasNewJobs}} + + {{/if}} +
+ {{t jobs.type}} : {{t jobs.type.hive}} +
+
+ + + {{#view view.sortView classNames="label-row" contentBinding="view.content"}} + + {{view view.parentView.idSort}} + {{view view.parentView.userSort}} + {{view view.parentView.startTimeSort}} + {{view view.parentView.endTimeSort}} + {{view view.parentView.durationSort}} + {{/view}} + + + + + + + + + + + + {{#if view.noDataToShow}} + + + + {{else}} + {{#each job in controller.sortedContent}} + + + + + + + + + {{/each}} + {{/if}} + +
{{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}}
+ +
+
+ {{view.filteredJobs}} - {{t jobs.filtered.clear}} +
+
+ +
+
+ {{view view.jobsPaginationLeft}} + {{view view.jobsPaginationRight}} +
+
+
+ diff --git contrib/views/jobs/src/main/resources/ui/app/templates/jobs/jobs_name.hbs contrib/views/jobs/src/main/resources/ui/app/templates/jobs/jobs_name.hbs new file mode 100644 index 0000000..37690ef --- /dev/null +++ contrib/views/jobs/src/main/resources/ui/app/templates/jobs/jobs_name.hbs @@ -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. +}} + +{{job.name}} diff --git contrib/views/jobs/src/main/resources/ui/app/templates/sort_field_template.hbs contrib/views/jobs/src/main/resources/ui/app/templates/sort_field_template.hbs new file mode 100644 index 0000000..6fc49fe --- /dev/null +++ contrib/views/jobs/src/main/resources/ui/app/templates/sort_field_template.hbs @@ -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. +}} + +{{view.displayName}} diff --git contrib/views/jobs/src/main/resources/ui/app/templates/table/navigation/pagination_first.hbs contrib/views/jobs/src/main/resources/ui/app/templates/table/navigation/pagination_first.hbs new file mode 100644 index 0000000..d538654 --- /dev/null +++ contrib/views/jobs/src/main/resources/ui/app/templates/table/navigation/pagination_first.hbs @@ -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. +}} + + diff --git contrib/views/jobs/src/main/resources/ui/app/templates/table/navigation/pagination_last.hbs contrib/views/jobs/src/main/resources/ui/app/templates/table/navigation/pagination_last.hbs new file mode 100644 index 0000000..99dbd68 --- /dev/null +++ contrib/views/jobs/src/main/resources/ui/app/templates/table/navigation/pagination_last.hbs @@ -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. +}} + + diff --git contrib/views/jobs/src/main/resources/ui/app/templates/table/navigation/pagination_left.hbs contrib/views/jobs/src/main/resources/ui/app/templates/table/navigation/pagination_left.hbs new file mode 100644 index 0000000..683a180 --- /dev/null +++ contrib/views/jobs/src/main/resources/ui/app/templates/table/navigation/pagination_left.hbs @@ -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. +}} + + diff --git contrib/views/jobs/src/main/resources/ui/app/templates/table/navigation/pagination_right.hbs contrib/views/jobs/src/main/resources/ui/app/templates/table/navigation/pagination_right.hbs new file mode 100644 index 0000000..a6b67cd --- /dev/null +++ contrib/views/jobs/src/main/resources/ui/app/templates/table/navigation/pagination_right.hbs @@ -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. +}} + + diff --git contrib/views/jobs/src/main/resources/ui/app/templates/wrapper_layout.hbs contrib/views/jobs/src/main/resources/ui/app/templates/wrapper_layout.hbs new file mode 100644 index 0000000..c2904de --- /dev/null +++ contrib/views/jobs/src/main/resources/ui/app/templates/wrapper_layout.hbs @@ -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. +}} + + {{yield}} diff --git contrib/views/jobs/src/main/resources/ui/app/templates/wrapper_template.hbs contrib/views/jobs/src/main/resources/ui/app/templates/wrapper_template.hbs new file mode 100644 index 0000000..0529100 --- /dev/null +++ contrib/views/jobs/src/main/resources/ui/app/templates/wrapper_template.hbs @@ -0,0 +1,25 @@ +{{! +* 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. +}} + +{{#if view.fieldId}} + +{{/if}} +{{view view.filterView}} +{{#if view.showApply}} + +{{/if}} diff --git contrib/views/jobs/src/main/resources/ui/bower.json contrib/views/jobs/src/main/resources/ui/bower.json index 0619e51..9b123b2 100644 --- contrib/views/jobs/src/main/resources/ui/bower.json +++ contrib/views/jobs/src/main/resources/ui/bower.json @@ -3,9 +3,11 @@ "version": "0.0.1", "dependencies": { "ember": "1.5.0", + "moment": ">=2.7.0", "handlebars": "1.2.1", "ember-data": "1.0.0-beta.7", - "bootstrap": ">3.0", + "ember-i18n": "1.6.*", + "bootstrap": "2.3.*", "ember-addons.bs_for_ember": ">=0.7", "ember-json-mapper": "master" },