Return-Path: X-Original-To: apmail-ambari-commits-archive@www.apache.org Delivered-To: apmail-ambari-commits-archive@www.apache.org Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by minotaur.apache.org (Postfix) with SMTP id CA67517810 for ; Sat, 14 Mar 2015 20:56:59 +0000 (UTC) Received: (qmail 7636 invoked by uid 500); 14 Mar 2015 20:56:59 -0000 Delivered-To: apmail-ambari-commits-archive@ambari.apache.org Received: (qmail 7545 invoked by uid 500); 14 Mar 2015 20:56:59 -0000 Mailing-List: contact commits-help@ambari.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: ambari-dev@ambari.apache.org Delivered-To: mailing list commits@ambari.apache.org Received: (qmail 7453 invoked by uid 99); 14 Mar 2015 20:56:59 -0000 Received: from git1-us-west.apache.org (HELO git1-us-west.apache.org) (140.211.11.23) by apache.org (qpsmtpd/0.29) with ESMTP; Sat, 14 Mar 2015 20:56:59 +0000 Received: by git1-us-west.apache.org (ASF Mail Server at git1-us-west.apache.org, from userid 33) id 5ECD4E17E3; Sat, 14 Mar 2015 20:56:59 +0000 (UTC) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit From: alexantonenko@apache.org To: commits@ambari.apache.org Date: Sat, 14 Mar 2015 20:57:01 -0000 Message-Id: <8fdd0dcf029641d388399913e616926b@git.apache.org> In-Reply-To: <56458af06c224036b731236aa02885d9@git.apache.org> References: <56458af06c224036b731236aa02885d9@git.apache.org> X-Mailer: ASF-Git Admin Mailer Subject: [3/3] ambari git commit: AMBARI-10063. CapSched View: Add Support for Node labels and versions (alexantonenko) AMBARI-10063. CapSched View: Add Support for Node labels and versions (alexantonenko) Project: http://git-wip-us.apache.org/repos/asf/ambari/repo Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/95103a90 Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/95103a90 Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/95103a90 Branch: refs/heads/trunk Commit: 95103a905df36745b8e9f01768a612193626c221 Parents: a5f99f4 Author: Alex Antonenko Authored: Sat Mar 14 22:05:21 2015 +0200 Committer: Alex Antonenko Committed: Sat Mar 14 22:56:52 2015 +0200 ---------------------------------------------------------------------- .../capacityscheduler/ConfigurationService.java | 115 +- .../src/main/resources/ui/app/adapters.js | 439 +++---- .../src/main/resources/ui/app/assets/index.html | 2 - .../resources/ui/app/components/capacityBar.js | 22 +- .../ui/app/components/capacityInput.js | 45 +- .../ui/app/components/confirmDelete.js | 6 +- .../ui/app/components/dropdownConfirmation.js | 11 +- .../resources/ui/app/components/escapeAcl.js | 11 +- .../resources/ui/app/components/pathInput.js | 6 +- .../ui/app/components/queueListItem.js | 67 +- .../resources/ui/app/components/radioButton.js | 6 +- .../ui/app/components/totalCapacity.js | 411 ++++++- .../main/resources/ui/app/controllers/queue.js | 284 +++-- .../main/resources/ui/app/controllers/queues.js | 287 ++++- .../src/main/resources/ui/app/initialize.js | 6 + .../src/main/resources/ui/app/models/queue.js | 148 ++- .../src/main/resources/ui/app/router.js | 34 +- .../src/main/resources/ui/app/serializers.js | 285 +++++ .../src/main/resources/ui/app/store.js | 164 +++ .../resources/ui/app/styles/application.less | 1069 +++++++++++------- .../src/main/resources/ui/app/templates.js | 2 + .../ui/app/templates/capacityEditForm.hbs | 87 +- .../ui/app/templates/components/capacityBar.hbs | 4 +- .../components/dropdownConfirmation.hbs | 29 + .../ui/app/templates/components/pathInput.hbs | 4 +- .../app/templates/components/queueContainer.hbs | 67 ++ .../app/templates/components/queueListItem.hbs | 42 +- .../app/templates/components/totalCapacity.hbs | 67 +- .../main/resources/ui/app/templates/queue.hbs | 136 ++- .../main/resources/ui/app/templates/queues.hbs | 47 +- .../ui/app/templates/schedulerPanel.hbs | 84 +- .../src/main/resources/ui/app/views/queues.js | 3 +- .../src/main/resources/ui/bower.json | 14 +- .../src/main/resources/ui/config.coffee | 14 +- 34 files changed, 2869 insertions(+), 1149 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/ambari/blob/95103a90/contrib/views/capacity-scheduler/src/main/java/org/apache/ambari/view/capacityscheduler/ConfigurationService.java ---------------------------------------------------------------------- diff --git a/contrib/views/capacity-scheduler/src/main/java/org/apache/ambari/view/capacityscheduler/ConfigurationService.java b/contrib/views/capacity-scheduler/src/main/java/org/apache/ambari/view/capacityscheduler/ConfigurationService.java index e6d81d0..f703f65 100644 --- a/contrib/views/capacity-scheduler/src/main/java/org/apache/ambari/view/capacityscheduler/ConfigurationService.java +++ b/contrib/views/capacity-scheduler/src/main/java/org/apache/ambari/view/capacityscheduler/ConfigurationService.java @@ -114,9 +114,12 @@ public class ConfigurationService { // ================================================================================ private final String versionTagUrl = "%s?fields=Clusters/desired_configs/capacity-scheduler"; - private final String configurationUrl = "%%s/configurations?type=capacity-scheduler&tag=%s"; + private final String configurationUrl = "%s/configurations?type=capacity-scheduler"; + private final String configurationUrlByTag = "%%s/configurations?type=capacity-scheduler&tag=%s"; private final String rmHostUrl = "%s/services/YARN/components/RESOURCEMANAGER?fields=host_components/host_name"; + private final String rmGetNodeLabelUrl = "http://%s:8088/ws/v1/cluster/get-node-labels"; + // ================================================================================ // Privilege Reading // ================================================================================ @@ -131,7 +134,7 @@ public class ConfigurationService { */ @GET @Produces(MediaType.APPLICATION_JSON) - public Response readConfiguration() { + public Response readLatestConfiguration() { Response response = null; try { validateViewConfiguration(); @@ -149,6 +152,55 @@ public class ConfigurationService { } /** + * Gets capacity scheduler configuration by all tags. + * + * @return scheduler configuration + */ + @GET + @Path("all") + @Produces(MediaType.APPLICATION_JSON) + public Response readAllConfigurations() { + Response response = null; + try { + validateViewConfiguration(); + + String url = String.format(configurationUrl, baseUrl); + JSONObject configurations = proxy.request(url).get().asJSON(); + response = Response.ok(configurations).build(); + } catch (WebApplicationException ex) { + throw ex; + } catch (Exception ex) { + throw new ServiceFormattedException(ex.getMessage(), ex); + } + + return response; + } + + /** + * Gets capacity scheduler configuration by specific tag. + * + * @return scheduler configuration + */ + @GET + @Path("byTag/{tag}") + @Produces(MediaType.APPLICATION_JSON) + public Response readConfigurationByTag(@PathParam("tag") String tag) { + Response response = null; + try { + validateViewConfiguration(); + + JSONObject configurations = getConfigurationFromAmbari(tag); + response = Response.ok(configurations).build(); + } catch (WebApplicationException ex) { + throw ex; + } catch (Exception ex) { + throw new ServiceFormattedException(ex.getMessage(), ex); + } + + return response; + } + + /** * Gets the privilege for this user. * * @return scheduler configuration @@ -173,6 +225,31 @@ public class ConfigurationService { } /** + * Gets node labels from RM + * + * @return node labels + */ + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("/nodeLabels") + public Response getNodeLabels() { + Response response; + + try { + String nodeLabels = proxy.request(String.format(rmGetNodeLabelUrl, getRMHost())).get().asString(); + + response = Response.ok(nodeLabels).build(); + } catch (WebApplicationException ex) { + throw ex; + } catch (Exception ex) { + throw new ServiceFormattedException(ex.getMessage(), ex); + } + + return response; + } + + + /** * Checks if the user is an operator. * * @return if true, the user is an operator; otherwise false @@ -183,7 +260,7 @@ public class ConfigurationService { // first check if the user is an CLUSTER.OPERATOR String url = String.format(clusterOperatorPrivilegeUrl, baseUrl, context.getUsername()); JSONObject json = proxy.request(url).get().asJSON(); - + if (json == null || json.size() <= 0) { // user is not a CLUSTER.OPERATOR but might be an AMBARI.ADMIN url = String.format(ambariAdminPrivilegeUrl, serverUrl, context.getUsername()); @@ -215,7 +292,7 @@ public class ConfigurationService { } private JSONObject getConfigurationFromAmbari(String versionTag) { - String urlTemplate = String.format(configurationUrl, versionTag); + String urlTemplate = String.format(configurationUrlByTag, versionTag); String url = String.format(urlTemplate, baseUrl); return proxy.request(url).get().asJSON(); } @@ -267,6 +344,7 @@ public class ConfigurationService { @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public Response writeConfiguration(JSONObject request) { + JSONObject response; try { validateViewConfiguration(); @@ -274,9 +352,9 @@ public class ConfigurationService { return Response.status(401).build(); } - proxy.request(baseUrl). - setData(makeConfigUpdateData(request)). - put(); + response = proxy.request(baseUrl). + setData(request). + put().asJSON(); } catch (WebApplicationException ex) { throw ex; @@ -284,7 +362,7 @@ public class ConfigurationService { throw new ServiceFormattedException(ex.getMessage(), ex); } - return readConfiguration(); + return Response.ok(response).build(); } /** @@ -302,8 +380,6 @@ public class ConfigurationService { if (isOperator() == false) { return Response.status(401).build(); } - - writeConfiguration(request); String rmHost = getRMHost(); JSONObject data = (JSONObject) JSONValue.parse(String.format(refreshRMRequestData, rmHost)); @@ -316,7 +392,7 @@ public class ConfigurationService { } catch (Exception ex) { throw new ServiceFormattedException(ex.getMessage(), ex); } - return readConfiguration(); + return readLatestConfiguration(); } /** @@ -335,8 +411,6 @@ public class ConfigurationService { return Response.status(401).build(); } - writeConfiguration(request); - String rmHost = getRMHost(); JSONObject data = (JSONObject) JSONValue.parse(String.format(restartRMRequestData, rmHost, rmHost)); proxy.request(baseUrl + "/requests/"). @@ -348,7 +422,7 @@ public class ConfigurationService { } catch (Exception ex) { throw new ServiceFormattedException(ex.getMessage(), ex); } - return readConfiguration(); + return readLatestConfiguration(); } private String getRMHost() { @@ -367,17 +441,4 @@ public class ConfigurationService { return rmHost; } - private JSONObject makeConfigUpdateData(JSONObject request) { - JSONObject desiredConfigs = (JSONObject) request.clone(); - desiredConfigs.put("type", "capacity-scheduler"); - desiredConfigs.put("tag", "version" + String.valueOf(System.currentTimeMillis())); - - JSONObject clusters = new JSONObject(); - clusters.put("desired_configs", desiredConfigs); - - JSONObject data = new JSONObject(); - data.put("Clusters", clusters); - return data; - } - } http://git-wip-us.apache.org/repos/asf/ambari/blob/95103a90/contrib/views/capacity-scheduler/src/main/resources/ui/app/adapters.js ---------------------------------------------------------------------- diff --git a/contrib/views/capacity-scheduler/src/main/resources/ui/app/adapters.js b/contrib/views/capacity-scheduler/src/main/resources/ui/app/adapters.js index 3c47a90..07d6fbd 100644 --- a/contrib/views/capacity-scheduler/src/main/resources/ui/app/adapters.js +++ b/contrib/views/capacity-scheduler/src/main/resources/ui/app/adapters.js @@ -18,13 +18,36 @@ var App = require('app'); +/** + * Gets the view backend URI + * + * @return view backend URI + */ +function _getCapacitySchedulerViewUri(adapter) { + if (App.testMode) + return "/data"; + + var parts = window.location.pathname.match(/[^\/]*/g).filterBy('').removeAt(0), + view = parts[0], + version = parts[1], + instance = parts[2]; + if (parts.length == 2) { // version is not present + instance = parts[1]; + version = ''; + } + + return '/' + [adapter.namespace,'views',view,'versions',version,'instances',instance,'resources','scheduler','configuration'].join('/'); +} + + App.QueueAdapter = DS.Adapter.extend({ + defaultSerializer:'queue', PREFIX: "yarn.scheduler.capacity", + namespace: 'api/v1', queues: [], - clusterName: '', - tag: '', + createRecord: function(store, type, record) { - var data = this.serialize(record, { includeId: true }); + var data = record.toJSON({ includeId: true }); return new Ember.RSVP.Promise(function(resolve, reject) { return store.filter('queue',function (q) { return q.id === record.id; @@ -34,50 +57,68 @@ App.QueueAdapter = DS.Adapter.extend({ message = "Field can not be empty"; } else if (queues.get('length') > 1) { message = "Queue already exists"; - }; + } if (message) { var error = new DS.InvalidError({path:[message]}); store.recordWasInvalid(record, error.errors); return; - }; - - Ember.run(record, resolve, {'queue':data}) + } + Ember.run(record, resolve, {'queue':data,'label':[]}); }); }); }, + deleteRecord:function (store, type, record) { + return new Ember.RSVP.Promise(function(resolve, reject) { + Ember.run(null, resolve, {'queue':record.serialize({ includeId: true , clone: true })}); + }); + }, + saveMark:'', updateRecord:function (store,type,record) { - var adapter = this, - saveMark = this.get('saveMark'), - uri = _getCapacitySchedulerUri(adapter.clusterName, adapter.tag), + var adapter = this; + var saveMark = this.get('saveMark'), + uri = _getCapacitySchedulerViewUri(this), serializer = store.serializerFor('queue'), - props = serializer.serializeConfig(record); + props = serializer.serializeConfig(record), + new_tag = 'version' + Math.floor(+moment()), + postSaveUri,data; if (saveMark) { - uri = [uri,saveMark].join('/'); + postSaveUri = [_getCapacitySchedulerViewUri(this),saveMark].join('/'); this.set('saveMark',''); - }; + } + + data = JSON.stringify({'Clusters': + {'desired_config': + [{ + 'type': 'capacity-scheduler', + 'tag': new_tag, + 'service_config_version_note': props.service_config_version_note, + 'properties': props.properties + }] + } + }); return new Ember.RSVP.Promise(function(resolve, reject) { - adapter.ajax(uri,'PUT',{data:props}).then(function(data) { - Ember.run(null, resolve, data); + adapter.ajax(uri,'PUT',{contentType:'application/json; charset=utf-8',data:data}).then(function(data) { + store.setProperties({'current_tag':new_tag,'tag':new_tag}); + Ember.run(null, resolve, data.resources.objectAt(0).configurations.objectAt(0).configs); }, function(jqXHR) { jqXHR.then = null; Ember.run(null, reject, jqXHR); + }).then(function () { + if (postSaveUri) { + adapter.postSave(postSaveUri); + } }); }); }, - - /** - * Finds queue by id. - * - */ - findQueue: function(id) { - var result = $.grep(App.Adapter.queues, function(e){ return e.get('id') == id; }); - return result[0]; + + postSave:function(uri){ + this.ajax(uri,'PUT',{contentType:'application/json; charset=utf-8',data:JSON.stringify({save:true})}); }, /** @@ -85,9 +126,18 @@ App.QueueAdapter = DS.Adapter.extend({ * */ find: function(store, type, id) { - return store.findAll('queue').then(function (queues) { - return {"queue":queues.findBy('id',id).toJSON({includeId:true})}; - }); + id = id.toLowerCase(); + var record = store.getById(type,id); + var key = type.typeKey; + if (record) { + return new Ember.RSVP.Promise(function(resolve, reject) { + resolve({key:record.toJSON({includeId:true})}); + }); + } else { + return store.findAll('queue').then(function (queues) { + resolve({key:store.getById(type,id).toJSON({includeId:true})}); + }); + } }, /** @@ -96,23 +146,62 @@ App.QueueAdapter = DS.Adapter.extend({ */ findAll: function(store, type) { var adapter = this; - var uri = _getCapacitySchedulerUri(adapter.clusterName, adapter.tag); + var uri = _getCapacitySchedulerViewUri(this); if (App.testMode) uri = uri + "/scheduler-configuration.json"; + return new Ember.RSVP.Promise(function(resolve, reject) { - adapter.ajax(uri).then(function(data) { + adapter.ajax(uri,'GET').then(function(data) { + var config = data.items.objectAt(0); + if (!store.get('isInitialized')) { + store.set('current_tag',config.tag); + } + store.setProperties({'clusterName':config.Config.cluster_name,'tag':config.tag}); + Ember.run(null, resolve, data.items.objectAt(0).properties); + }, function(jqXHR) { + jqXHR.then = null; + Ember.run(null, reject, jqXHR); + }); + }); + }, + + findAllTagged: function(store, type) { + var adapter = this, + uri = [_getCapacitySchedulerViewUri(this),'byTag',store.get('tag')].join('/'); + + return new Ember.RSVP.Promise(function(resolve, reject) { + adapter.ajax(uri,'GET').then(function(data) { Ember.run(null, resolve, data); }, function(jqXHR) { jqXHR.then = null; Ember.run(null, reject, jqXHR); }); }); + }, + + getNodeLabels:function () { + var uri = [_getCapacitySchedulerViewUri(this),'nodeLabels'].join('/'); + return new Ember.RSVP.Promise(function(resolve, reject) { + this.ajax(uri,'GET').then(function(data) { + var parsedData = JSON.parse(data), labels; + if (parsedData && Em.isArray(parsedData.nodeLabels)) { + labels = parsedData.nodeLabels; + } else { + labels = (parsedData && parsedData.nodeLabels)?[parsedData.nodeLabels]:[]; + } + Ember.run(null, resolve, labels.map(function (label) { + return {name:label}; + })); + }, function(jqXHR) { + jqXHR.then = null; + Ember.run(null, reject, jqXHR); + }); + }.bind(this)); }, getPrivilege:function () { - var uri = _getCapacitySchedulerUri(this.clusterName, this.tag); - uri = [uri,'privilege'].join('/'); + var uri = [_getCapacitySchedulerViewUri(this),'privilege'].join('/'); if (App.testMode) uri = uri + ".json"; return new Ember.RSVP.Promise(function(resolve, reject) { @@ -124,7 +213,7 @@ App.QueueAdapter = DS.Adapter.extend({ }); }.bind(this)); }, - + ajax: function(url, type, hash) { var adapter = this; @@ -150,10 +239,6 @@ App.QueueAdapter = DS.Adapter.extend({ hash.dataType = 'json'; hash.context = this; - if (hash.data && type !== 'GET') { - hash.contentType = 'application/json; charset=utf-8'; - hash.data = JSON.stringify(hash.data); - } hash.beforeSend = function (xhr) { xhr.setRequestHeader('X-Requested-By', 'view-capacity-scheduler'); }; @@ -177,277 +262,23 @@ App.SchedulerAdapter = App.QueueAdapter.extend({ } }); -App.ApplicationStore = DS.Store.extend({ - - adapter: App.QueueAdapter, - - configNote:'', - - markForRefresh:function (mark) { - this.set('defaultAdapter.saveMark','saveAndRefresh'); - }, - - markForRestart:function (mark) { - this.set('defaultAdapter.saveMark','saveAndRestart'); - }, - - flushPendingSave: function() { - var pending = this._pendingSave.slice(), - newPending = [[]],scheduler; - - if (pending.length == 1) { - this._super(); - return; - }; - - pending.forEach(function (tuple) { - var record = tuple[0], resolver = tuple[1], - operation; - newPending[0].push(record) - newPending[1] = resolver; - }); - - this._pendingSave = [newPending]; - this._super(); - }, - didSaveRecord: function(record, data) { - if (Em.isArray(record)) { - for (var i = 0; i < record.length; i++) { - this._super(record[i],data.findBy('id',record[i].id)); - }; - } else { - this._super(record, data); - }; - }, - recordWasError: function(record) { - if (Em.isArray(record)) { - for (var i = 0; i < record.length; i++) { - record[i].adapterDidError(); - }; - } else { - record.adapterDidError(); - } - }, - checkOperator:function () { - return this.get('defaultAdapter').getPrivilege(); - } -}); - -App.ApplicationSerializer = DS.RESTSerializer.extend({ - - PREFIX:"yarn.scheduler.capacity", - - serializeConfig:function (records) { - var config = {}, - note = this.get('store.configNote'), - prefix = this.PREFIX; - records.forEach(function (record) { - if (record.id == 'scheduler') { - config[prefix + ".maximum-am-resource-percent"] = record.get('maximum_am_resource_percent')/100; // convert back to decimal - config[prefix + ".maximum-applications"] = record.get('maximum_applications'); - config[prefix + ".node-locality-delay"] = record.get('node_locality_delay'); - config[prefix + ".resource-calculator"] = record.get('resource_calculator'); - } else { - config[prefix + "." + record.get('path') + ".unfunded.capacity"] = record.get('unfunded_capacity'); - config[prefix + "." + record.get('path') + ".acl_administer_queue"] = record.get('acl_administer_queue'); - config[prefix + "." + record.get('path') + ".acl_submit_applications"] = record.get('acl_submit_applications'); - config[prefix + "." + record.get('path') + ".minimum-user-limit-percent"] = record.get('minimum_user_limit_percent'); - config[prefix + "." + record.get('path') + ".maximum-capacity"] = record.get('maximum_capacity'); - config[prefix + "." + record.get('path') + ".user-limit-factor"] = record.get('user_limit_factor'); - config[prefix + "." + record.get('path') + ".state"] = record.get('state'); - config[prefix + "." + record.get('path') + ".capacity"] = record.get('capacity'); - config[prefix + "." + record.get('path') + ".queues"] = record.get('queueNames')||''; - - // do not set property if not set - var ma = record.get('maximum_applications')||''; - if (ma) { - config[prefix + "." + record.get('path') + ".maximum-applications"] = ma; - } +App.TagAdapter = App.QueueAdapter.extend({ + /** + * Finds all tags. + * + */ + findAll: function(store, type) { + var adapter = this; + var uri = [_getCapacitySchedulerViewUri(this),'all'].join('/'); - // do not set property if not set - var marp = record.get('maximum_am_resource_percent')||''; - if (marp) { - marp = marp/100; // convert back to decimal - config[prefix + "." + record.get('path') + ".maximum-am-resource-percent"] = marp; - } - }; + return new Ember.RSVP.Promise(function(resolve, reject) { + adapter.ajax(uri ,'GET').then(function(data) { + Ember.run(null, resolve, data); + }, function(jqXHR) { + jqXHR.then = null; + Ember.run(null, reject, jqXHR); + }); }); - - for (var i in config) { - if (config[i] === null || config[i] === undefined) { - delete config[i]; - } - } - - this.set('store.configNote',''); - - return {properties : config, service_config_version_note: note}; - - }, - normalizeConfig:function (store, payload) { - var queues = []; - var props = payload.items[0].properties; - - var scheduler = [{ - id:'scheduler', - maximum_am_resource_percent:props[this.PREFIX + ".maximum-am-resource-percent"]*100, // convert to percent - maximum_applications:props[this.PREFIX + ".maximum-applications"], - node_locality_delay:props[this.PREFIX + ".node-locality-delay"], - resource_calculator:props[this.PREFIX + ".resource-calculator"] - }]; - queues = _recurseQueues(null, "root", 0, props, queues, store); - - return {'queue':queues,'scheduler':scheduler}; - }, - - extractFindAll: function(store, type, payload){ - var queues = []; - var props = payload.items[0].properties; - - var scheduler = { - id:'scheduler', - maximum_am_resource_percent:props[this.PREFIX + ".maximum-am-resource-percent"]*100, // convert to percent - maximum_applications:props[this.PREFIX + ".maximum-applications"], - node_locality_delay:props[this.PREFIX + ".node-locality-delay"], - resource_calculator:props[this.PREFIX + ".resource-calculator"] - }; - queues = _recurseQueues(null, "root", 0, props, queues, store); - - var config = this.normalizeConfig(store, payload); - - return this.extractArray(store, type, config); - }, - - extractUpdateRecord:function (store, type, payload, id, requestType) { - var queues = []; - var props = payload.items[0].properties; - - var scheduler = { - id:'scheduler', - maximum_am_resource_percent:props[this.PREFIX + ".maximum-am-resource-percent"]*100, // convert to percent - maximum_applications:props[this.PREFIX + ".maximum-applications"], - node_locality_delay:props[this.PREFIX + ".node-locality-delay"], - resource_calculator:props[this.PREFIX + ".resource-calculator"] - }; - queues = _recurseQueues(null, "root", 0, props, queues, store); - - var config = this.normalizeConfig(store, payload); - - return this.extractConfig(store, App.Queue, {'queue':queues,'scheduler':[scheduler]}); - }, - - extractConfig: function(store, primaryType, payload) { - payload = this.normalizePayload(payload); - - var primaryTypeName = primaryType.typeKey, - primaryArray = [], - scheduler,queues; - - for (var prop in payload) { - var typeKey = prop, - forcedSecondary = false; - - if (prop.charAt(0) === '_') { - forcedSecondary = true; - typeKey = prop.substr(1); - } - - var typeName = this.typeForRoot(typeKey), - type = store.modelFor(typeName), - typeSerializer = store.serializerFor(type), - isPrimary = (!forcedSecondary && (type.typeKey === primaryTypeName)); - - var normalizedArray = Ember.ArrayPolyfills.map.call(payload[prop], function(hash) { - return typeSerializer.normalize(type, hash, prop); - }, this); - - if (typeKey == App.Scheduler.typeKey) { - scheduler = normalizedArray; - } else { - queues = normalizedArray; - } - } - - return scheduler.concat(queues); - }, - - extractQueue: function(data, props) { - var q = { name: data.name, parentPath: data.parentPath, depth: data.depth }; - var prefix = this.PREFIX; - - if (q.parentPath == null || q.parentPath.length == 0){ - q.path = q.name; - } else { - q.path = q.parentPath + '.' + q.name; - } - q.id = q.path.dasherize(); - - q.unfunded_capacity = props[prefix + "." + q.path + ".unfunded.capacity"]; - - q.state = props[prefix + "." + q.path + ".state"]; - q.acl_administer_queue = props[prefix + "." + q.path + ".acl_administer_queue"]; - q.acl_submit_applications = props[prefix + "." + q.path + ".acl_submit_applications"]; - - q.capacity = props[prefix + "." + q.path + ".capacity"]; - q.maximum_capacity = props[prefix + "." + q.path + ".maximum-capacity"]; - - q.user_limit_factor = props[prefix + "." + q.path + ".user-limit-factor"]; - q.minimum_user_limit_percent = props[prefix + "." + q.path + ".minimum-user-limit-percent"]; - q.maximum_applications = props[prefix + "." + q.path + ".maximum-applications"]; - q.maximum_am_resource_percent = props[prefix + "." + q.path + ".maximum-am-resource-percent"] - - if (q.maximum_am_resource_percent) - q.maximum_am_resource_percent = q.maximum_am_resource_percent*100; // convert to percent - - q.queueNames = props[prefix + "." + q.path + ".queues"]; - - return q; } }); -/** - * Recursively builds the list of queues. - * - */ -function _recurseQueues(parentQueue, queueName, depth, props, queues, store) { - var serializer = store.serializerFor('queue'); - var prefix = serializer.PREFIX; - var parentPath = ''; - if (parentQueue != null) { - parentPath = parentQueue.path; - prefix += "."; - } - - var queue = serializer.extractQueue({ name: queueName, parentPath: parentPath, depth: depth}, props); - queues.push(queue); - - var queueProp = prefix + parentPath + "." + queueName + ".queues"; - if (props[queueProp]) { - var qs = props[queueProp].split(','); - for (var i=0; i < qs.length; i++) { - queues = _recurseQueues(queue, qs[i], depth+1, props, queues, store) - } - } - - return queues; -} - -/** - * Gets the cluster name URI based on test mode - * - * @return cluster name URI - */ -function _getCapacitySchedulerUri() { - if (App.testMode) - return "/data"; - - var parts = window.location.pathname.match(/\/[^\/]*/g); - var view = parts[1]; - var version = '/versions' + parts[2]; - var instance = parts[3]; - if (parts.length == 4) { // version is not present - instance = parts[2]; - version = ''; - } - return '/api/v1/views' + view + version + '/instances' + instance+'/resources/scheduler/configuration'; -} http://git-wip-us.apache.org/repos/asf/ambari/blob/95103a90/contrib/views/capacity-scheduler/src/main/resources/ui/app/assets/index.html ---------------------------------------------------------------------- diff --git a/contrib/views/capacity-scheduler/src/main/resources/ui/app/assets/index.html b/contrib/views/capacity-scheduler/src/main/resources/ui/app/assets/index.html index ce0972c..2cdfe39 100644 --- a/contrib/views/capacity-scheduler/src/main/resources/ui/app/assets/index.html +++ b/contrib/views/capacity-scheduler/src/main/resources/ui/app/assets/index.html @@ -34,13 +34,11 @@ http://git-wip-us.apache.org/repos/asf/ambari/blob/95103a90/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/capacityBar.js ---------------------------------------------------------------------- diff --git a/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/capacityBar.js b/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/capacityBar.js index 22d511b..6f35e28 100644 --- a/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/capacityBar.js +++ b/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/capacityBar.js @@ -20,22 +20,36 @@ var App = require('app'); App.CapacityBarComponent = Em.Component.extend({ layoutName:'components/capacityBar', + classNames:['capacity-bar'], + + capacityValue:null, + + maxCapacityValue:null, + + warn:false, + + pattern:'width: %@%;', + mark:function () { if (this.$().parent().hasClass('active')) { Ember.run.next(this, 'addInClass'); - }; + } }.on('didInsertElement'), + addInClass:function () { this.$().children().addClass('in'); }, + capacityWidth: function() { - return (Number(this.get('capacityValue'))<=100)?('width: '+this.get('capacityValue')+'%'):'width: 100%'; + return this.get('pattern').fmt(( +this.get('capacityValue') <= 100 ) ? +this.get('capacityValue') : 100); }.property('capacityValue'), + maxCapacityWidth: function() { - var val = Number(this.get('maxCapacityValue')) - Number(this.get('capacityValue')); - return (val<=100)?('width: '+val+'%'):'width: 100%'; + var val = this.get('maxCapacityValue') - this.get('capacityValue'); + return this.get('pattern').fmt((val<=100)?val:100); }.property('maxCapacityValue','capacityValue'), + showMaxCapacity:function () { return this.get('maxCapacityValue') > this.get('capacityValue'); }.property('maxCapacityValue','capacityValue') http://git-wip-us.apache.org/repos/asf/ambari/blob/95103a90/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/capacityInput.js ---------------------------------------------------------------------- diff --git a/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/capacityInput.js b/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/capacityInput.js index 0bf4b85..8ac766d 100644 --- a/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/capacityInput.js +++ b/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/capacityInput.js @@ -24,8 +24,7 @@ App.InputRangeComponent = Em.TextField.extend({ action: 'mouseUp', mouseUp: function () { - var value = this.get('value'); - this.sendAction('action', value); + this.sendAction('action', this.get('value')); } }); @@ -42,35 +41,29 @@ App.IntInputComponent = Ember.TextField.extend({ classNames:['form-control'], maxVal:null, intVal:function () { - var val = parseFloat(this.get('value'))||0; + var val = (!Em.isBlank(this.get('value'))) ? parseFloat(this.get('value')) : null; var maxVal = this.get('maxVal'); - val = (maxVal && maxVal < val)?maxVal:val; - this.set('value', val); + this.set('value', (maxVal && maxVal < val)?maxVal:val); }.on('change'), checkNumber:function () { - var val = this.get('value'), - num = Number(val), - str = String(val); - if (typeof val !== "number" && !isNaN(num) && str == num.toString()) { - this.set('value', Number(val)); - }; + this.set('value', (!Em.isBlank(this.get('value')) && !isNaN(parseFloat(this.get('value')))) ? parseFloat(this.get('value')): null); }.observes('value') }); App.CapacityInputComponent = App.IntInputComponent.extend({ - + totalCapacity:null, queue:null, keyDown: function(evt) { var newChar, val = this.get('value')||0; val = val.toString(); - - if ((evt.keyCode > 64 && evt.keyCode < 91) || - (evt.keyCode > 185 && evt.keyCode < 193) || + + if ((evt.keyCode > 64 && evt.keyCode < 91) || + (evt.keyCode > 185 && evt.keyCode < 193) || (evt.keyCode > 218 && evt.keyCode < 223)) { return false; - }; + } if (evt.keyCode > 95 && evt.keyCode < 106) { newChar = (evt.keyCode - 96).toString(); @@ -80,25 +73,31 @@ App.CapacityInputComponent = App.IntInputComponent.extend({ if (newChar.match(/[0-9]/)) { val = val.substring(0, evt.target.selectionStart) + newChar + val.substring(evt.target.selectionEnd); - }; + } - return parseFloat(val)<=100; - }, + return parseFloat(val) <= 100; + } }); App.MaxCapacityInputComponent = App.CapacityInputComponent.extend({ isInvalid:false, invalid:function (c,o) { - var queue = this.get('queue'), - max_capacity = Number(queue.get('maximum_capacity')), - capacity = Number(queue.get('capacity')); + var queue = this.get('queue'), max_capacity, capacity; + + if (queue.get('maximum_capacity') === null) return; + + max_capacity = +queue.get('maximum_capacity'); + capacity = +queue.get('capacity'); + if (o == 'queue.capacity' && max_capacity < capacity) { return queue.set('maximum_capacity',capacity); - }; + } + if (max_capacity < capacity && queue.get('isDirty')) { queue.get('errors').add('maximum_capacity', 'Maximum must be equal or greater than capacity'); } else { queue.get('errors').remove('maximum_capacity'); } + }.observes('queue.maximum_capacity','queue.capacity') }); http://git-wip-us.apache.org/repos/asf/ambari/blob/95103a90/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/confirmDelete.js ---------------------------------------------------------------------- diff --git a/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/confirmDelete.js b/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/confirmDelete.js index 8b61ca3..5ada051 100644 --- a/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/confirmDelete.js +++ b/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/confirmDelete.js @@ -36,16 +36,18 @@ App.ConfirmDeleteComponent = Em.Component.extend(App.ClickElsewhereMixin,{ tagName:'a', tooltip:function () { var element = this.$(); + if (this.get('confirm')) { element.tooltip({ placement:'left', title:'Click again to confirm' }).tooltip('show'); } else { - element.tooltip('destroy') + element.tooltip('destroy'); } + }.observes('confirm'), - click:function (e) { + click:function () { this.send('delete'); }, didChange:function () { http://git-wip-us.apache.org/repos/asf/ambari/blob/95103a90/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/dropdownConfirmation.js ---------------------------------------------------------------------- diff --git a/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/dropdownConfirmation.js b/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/dropdownConfirmation.js index 105d960..d86e3a0 100644 --- a/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/dropdownConfirmation.js +++ b/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/dropdownConfirmation.js @@ -20,6 +20,7 @@ var App = require('app'); App.DropdownConfirmationComponent = Em.Component.extend(App.ClickElsewhereMixin,{ + layoutName:'components/dropdownConfirmation', tagName:'li', restartConfirming:false, actions:{ @@ -36,20 +37,20 @@ App.DropdownConfirmationComponent = Em.Component.extend(App.ClickElsewhereMixin, this.$().parents('.dropdown-menu').parent().removeClass('open'); }, dropdownHideControl:function () { - var _this = this; this.$().parents('.dropdown-menu').parent().on( "hide.bs.dropdown", function() { - return !_this.get('restartConfirming'); - }); + return !this.get('restartConfirming'); + }.bind(this)); }.on('didInsertElement'), button:Em.Component.extend({ tagName:'a', - click:function (e) { + click:function () { this.triggerAction({ action: 'showRestartConfirmation', target: this.get('parentView'), actionContext: this.get('context') }); } - }) + }), + needRestart:false }); http://git-wip-us.apache.org/repos/asf/ambari/blob/95103a90/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/escapeAcl.js ---------------------------------------------------------------------- diff --git a/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/escapeAcl.js b/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/escapeAcl.js index b5a3e07..125b037 100644 --- a/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/escapeAcl.js +++ b/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/escapeAcl.js @@ -19,7 +19,7 @@ Ember.Handlebars.helper('escapeACL', function(value) { var output = ''; - + value = value || ''; if (value.trim() == '') { @@ -28,21 +28,22 @@ Ember.Handlebars.helper('escapeACL', function(value) { output = ''; } else { var ug = value.split(' '); - var users = ug[0].split(',')||[]; + var users = ug[0].split(',')||[]; var groups = (ug.length == 2)?ug[1].split(',')||[]:[]; - output += ' ' + output += ' '; users.forEach(function (user) { output += (user)?' '+ user +' ':''; }); - output += ' ' + output += ' '; groups.forEach(function (group) { output += (group)?' '+ group +' ':''; }); - output += ' ' + + output += ' '; } return new Ember.Handlebars.SafeString(output); }); http://git-wip-us.apache.org/repos/asf/ambari/blob/95103a90/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/pathInput.js ---------------------------------------------------------------------- diff --git a/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/pathInput.js b/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/pathInput.js index 92bfb31..fc8f77c 100644 --- a/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/pathInput.js +++ b/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/pathInput.js @@ -39,7 +39,7 @@ App.PathInputComponent = Em.Component.extend({ cancel:function () { this.set('activeFlag',false); } - }, + }, queues:[], activeFlag:false, pathMap:Em.computed.mapBy('queues','path'), @@ -60,7 +60,7 @@ App.PathInputComponent = Em.Component.extend({ this.$().typeahead({ source: this.get('pathSource'), matcher: function (item) { - return ~item.toLowerCase().indexOf(this.query.toLowerCase()) + return ~item.toLowerCase().indexOf(this.query.toLowerCase()); }, minLength:2, items:100, @@ -82,4 +82,4 @@ App.PathInputComponent = Em.Component.extend({ this.$().tooltip('destroy'); } }) -}); \ No newline at end of file +}); http://git-wip-us.apache.org/repos/asf/ambari/blob/95103a90/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/queueListItem.js ---------------------------------------------------------------------- diff --git a/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/queueListItem.js b/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/queueListItem.js index c9bf813..fc52eb9 100644 --- a/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/queueListItem.js +++ b/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/queueListItem.js @@ -18,7 +18,7 @@ var App = require('app'); -App.RecurceQueuesComponent = Ember.View.extend({ +App.RecurceQueuesComponent = Em.View.extend({ templateName: "components/queueListItem", depth:0, parent:'', @@ -40,3 +40,68 @@ App.RecurceQueuesComponent = Ember.View.extend({ } }); + +App.DiffTooltipComponent = Em.Component.extend({ + classNames:'fa fa-fw fa-lg blue fa-pencil'.w(), + tagName:'i', + queue:null, + initTooltip:function () { + var queue = this.get('queue'); + this.$().tooltip({ + title:function () { + var caption = '', + fmtString = '%@: %@ -> %@\n', + emptyValue = 'not set', + changes = queue.changedAttributes(), + idsToNames = function (l) { + return l.split('.').get('lastObject'); + }, + formatChangedAttributes = function (prefix,item) { + // don't show this to user. + if (item == '_accessAllLabels') return; + + var oldV = this[item].objectAt(0), + newV = this[item].objectAt(1); + + caption += fmtString.fmt( + [prefix,item].compact().join('.'), + (oldV != null && '\'%@\''.fmt(oldV)) || emptyValue, + (newV != null && '\'%@\''.fmt(newV)) || emptyValue + ); + }, + initialLabels, + currentLabels, + isAllChanged, + oldV, + newV; + + Em.keys(changes).forEach(Em.run.bind(changes,formatChangedAttributes,null)); + + if (queue.constructor.typeKey === 'queue') { + //cpmpare labels + isAllChanged = changes.hasOwnProperty('_accessAllLabels'); + initialLabels = queue.get('initialLabels').sort(); + currentLabels = queue.get('labels').mapBy('id').sort(); + + if (queue.get('isLabelsDirty') || isAllChanged) { + + oldV = ((isAllChanged && changes._accessAllLabels.objectAt(0)) || (!isAllChanged && queue.get('_accessAllLabels')))?'*':initialLabels.map(idsToNames).join(',') || emptyValue; + newV = ((isAllChanged && changes._accessAllLabels.objectAt(1)) || (!isAllChanged && queue.get('_accessAllLabels')))?'*':currentLabels.map(idsToNames).join(',') || emptyValue; + + caption += fmtString.fmt('accessible-node-labels', oldV, newV); + } + + queue.get('labels').forEach(function (label) { + var labelsChanges = label.changedAttributes(), + prefix = ['accessible-node-labels',label.get('name')].join('.'); + Em.keys(labelsChanges).forEach(Em.run.bind(labelsChanges,formatChangedAttributes,prefix)); + }); + } + + return caption; + }, + html:true, + placement:'bottom' + }); + }.on('didInsertElement') +}); http://git-wip-us.apache.org/repos/asf/ambari/blob/95103a90/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/radioButton.js ---------------------------------------------------------------------- diff --git a/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/radioButton.js b/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/radioButton.js index 30fd531..8be8919 100644 --- a/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/radioButton.js +++ b/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/radioButton.js @@ -23,7 +23,7 @@ App.RadioButtonInputComponent = Ember.View.extend({ type : "radio", attributeBindings : [ "type", "value", "checked:checked" ], click : function() { - this.set("selection", this.get('value')) + this.set("selection", this.get('value')); } }); @@ -35,12 +35,12 @@ App.RadioButtonComponent = Em.Component.extend({ //arguments selection:null, label:null, - value:null, + value:null, click : function() { this.set("selection", this.get('value')) }, isActive : function() { - return this.get("value") == this.get("selection"); + return this.get("value") == this.get("selection"); }.property("selection"), layout:Em.Handlebars.compile('{{label}} {{radio-button-input selection=selection value=value checked=isActive}}') }); http://git-wip-us.apache.org/repos/asf/ambari/blob/95103a90/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/totalCapacity.js ---------------------------------------------------------------------- diff --git a/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/totalCapacity.js b/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/totalCapacity.js index 806cf3b..d4fc80b 100644 --- a/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/totalCapacity.js +++ b/contrib/views/capacity-scheduler/src/main/resources/ui/app/components/totalCapacity.js @@ -21,55 +21,149 @@ var App = require('app'); App.TotalCapacityComponent = Ember.Component.extend({ layoutName:'components/totalCapacity', + // TAKEN VALUES + + /** + * Speaks for itself. + * @type {App.Queue} + */ + currentQueue:null, + + /** + * All queues in store. + * @type {DS.RecordArray} + */ + allQueues:[], + + /** + * All queues in store sorted by App.QueuesController + * @type {Array} + */ + allQueuesArranged:[], + + /** + * User admin status. + * @type {Boolean} + */ + isOperator:false, + + /** + * Target for rollbackProp action. + * @type {String} + */ + rollbackProp:'', + + // ACTIONS + actions:{ - toggleEdit:function () { - this.toggleProperty('isEdit'); - }, - addQueue:function (path) { - this.sendAction('addQueue',path); + toggleProperty:function (property,target) { + target = target || this; + target.toggleProperty(property); }, - createQueue:function (queue) { - this.sendAction('createQueue',queue); + rollbackProp:function (prop, item) { + this.sendAction('rollbackProp', prop, item); }, - deleteQueue:function (queue) { - this.sendAction('deleteQueue',queue); + toggleLabel:function (labelName, queue) { + var q = queue || this.get('currentQueue'), + labelRecord = q.store.getById('label',[q.get('path'),labelName].join('.').toLowerCase()); + + if (q.get('labels').contains(labelRecord)) { + this.recurseRemoveLabel(q,labelRecord); + } else { + q.get('labels').pushObject(labelRecord); + } + q.notifyPropertyChange('labels'); } }, /** - * passed params + * @param {App.Queue} target queue + * @param {App.Label} label ralated to queue. All labels with it's name will be removed from child queues. + * @method recurseRemoveLabel */ - currentQueue:null, - allQueues:[], - allQueuesArranged:[], + recurseRemoveLabel:function(queue,label) { + label = queue.get('labels').findBy('name',label.get('name')); + if (label) { + queue.get('labels').removeObject(label); + this.get('allQueues').filterBy('parentPath',queue.get('path')).forEach(function (child) { + this.recurseRemoveLabel(child,label); + }.bind(this)); + } + }, - isEdit: true, - - disableEdit:function () { - this.set('isEdit',false); - }.observes('allQueues'), + // COMPUTED PROPERTIES + /** + * Path for current queue parent. + * @type {String} + */ currentPrPath:Em.computed.alias('currentQueue.parentPath'), - leafQueuesCapacity: Ember.computed.map('leafQueues.@each.capacity', function (queue) { - return Number(queue.get('capacity')); - }), + /** + * Current queue parent. + * @type {App.Queue} + */ + parentQueue:function () { + return this.allQueues.findBy('path',this.get('currentPrPath')); + }.property('allQueues','currentPrPath'), - totalCapacity: Ember.computed.sum('leafQueuesCapacity'), - + /** + * Leaf queues. + * @type {Array} + */ leafQueues:function () { return this.allQueuesArranged.filterBy('parentPath',this.get('currentPrPath')).filterBy('isNew',false); }.property('allQueuesArranged.length','currentPrPath'), - newLeafQueues:function () { - return this.allQueues.filterBy('parentPath',this.get('currentPrPath')).filterBy('isNew',true); - }.property('allQueues.length','currentPrPath'), - - parentQueue:function () { - return this.allQueues.findBy('path',this.get('currentPrPath')); - }.property('allQueues','currentPrPath'), + /** + * Array of leaf queues capacities. + * @type {Array} + */ + leafQueuesCapacity: Ember.computed.map('leafQueues.@each.capacity', function (queue) { + return +queue.get('capacity'); + }), + + /** + * Sum of leaf queues capacities. + * @type {Number} + */ + totalCapacity: Ember.computed.sum('leafQueuesCapacity'), + + /** + * Node labels stored in store. + * @type {Array} + */ + nodeLabels:Ember.computed.alias('currentQueue.store.nodeLabels.content'), - currentInLeaf:function (argument) { + /** + * Array of arrays of node labels. + * Made to split node labels in several rows. + * + * @return {Array} + */ + arrangedNodeLabels:function () { + var llength = this.get('nodeLabels.length'), + cols = (llength % 4 === 1)?3:4, + arranged = []; + + this.get('nodeLabels').forEach(function (l, idx, all) { + if (idx%cols === 0) { + arranged.pushObject([l]); + } else { + arranged.get('lastObject').pushObject(l) + } + }); + + return arranged; + }.property('nodeLabels.length'), + + // OBSERVABLES + + /** + * Marks currentQueue with 'isCurrent' flag. + * @method currentInLeaf + */ + currentInLeaf:function () { var queues = this.get('allQueues'); queues.setEach('isCurrent',false); if(!this.get('currentQueue.currentState.stateName').match(/root.deleted/)) { @@ -77,22 +171,251 @@ App.TotalCapacityComponent = Ember.Component.extend({ } }.observes('leafQueues','currentQueue').on('init'), - newQueueNameField: Em.TextField.extend({ + // CHILD VIEWS + + /** + * Toggle buttons for node labels. + * @type {Em.View} + */ + nodeLabelsToggles: Em.View.extend(Ember.ViewTargetActionSupport,{ + + // TAKEN VALUES + + /** + * Name of node label. + * @type {[type]} + */ + labelName:null, + /** + * Node label's existence flag. + * @type {Boolean} + */ + notExist:false, + + /** + * Queue related to label. + * @type {App.Queue} + */ queue:null, - classNames:['form-control'], - classNameBindings:['isValid::input-error'], - isValid:Em.computed.bool('queue.isValid') + + /** + * Queue's leaf. + * @type {Array} + */ + leaf:null, + + // VIEW'S ATTRIBUTES + + tagName:'label', + + classNames:['btn','btn-default'], + + classNameBindings:['isActive:active','warning','lockedByParent:disabled','notExist:not-exist'], + + action:'toggleLabel', + + click:function () { + if (this.get('parentView.isOperator')) { + this.triggerAction({ + actionContext: [this.get('labelName'),this.get('queue')] + }); + Em.run.next(function() { + this.notifyPropertyChange('currentLabel'); + }.bind(this)); + } + }, + + // COMPUTED PROPERTIES + + /** + * Returns true if parent queue has no access to label with 'labelName'. + * @return {Boolean} + */ + lockedByParent:function () { + var parent = this.get('controller.parentQueue'); + return (Em.isEmpty(parent))?false:!parent.get('labels').findBy('name',this.get('labelName')); + }.property('controller.parentQueue'), + + /** + * Returns true if current queue has access to label with 'labelName'. + * @return {Boolean} + */ + isActive:function () { + return this.get('queue.labels').mapBy('name').contains(this.get('labelName')); + }.property('queue.labels.[]'), + + /** + * Label record of type App.Label related to current queue. + * @return {App.Label} + */ + currentLabel:function () { + return this.get('queue.labels').findBy('name',this.get('labelName')); + }.property('labelName','queue.labels.length'), + + /** + * Label records from leaf queues, that has label with 'labelName'. + * @return {Array} + */ + labels:function () { + return this.get('leaf').map(function (item) { + return item.get('labels').findBy('name',this.get('labelName')); + }.bind(this)).compact(); + }.property('leaf.@each.labels.[]'), + + /** + * Sum of label records from leaf queues, that has label with 'labelName'. + * @return {Number} + */ + capacityValue:function () { + return this.get('labels').reduce(function (prev, label) { + return prev + ((label && label.get('capacity'))?+label.get('capacity'):0); + }, 0); + }.property('labels.@each.capacity','labels.[]'), + + /** + * Returns true if total capacity of node labels in leaf are greater than 100. + * @type {Boolean} + */ + warning:Em.computed.gt('capacityValue',100), + + // OBSERVABLES + + /** + * Marks labels with 'overCapacity' mark if they can't have such capacity value. + * @method capacityWatcher + */ + capacityWatcher:function () { + this.get('labels').setEach('overCapacity',this.get('warning')); + }.observes('warning').on('didInsertElement'), + + /** + * Initializes tooltip on button when inserted to DOM. + * @method capacityTooltip + */ + capacityTooltip:function () { + this.$().tooltip({ + title: function () { + return this.get('capacityValue') + ''; + }.bind(this), + container:"#"+this.elementId, + animation:false + }); + }.on('didInsertElement'), + + /** + * Marks buttons with 'first' and 'last' classes when inserted to DOM. + * @method setFirstAndLast + */ + setFirstAndLast:function (item) { + var items = this.$().parents('.labels-toggle').find('label'); + items.first().addClass('first'); + items.last().addClass('last'); + }.on('didInsertElement'), + + /** + * Timer information with callback that hides tooltip. + * @type {Array} + */ + isShownTimer:null, + + /** + * Shows tooltip when label's capacity value changes. + * @method showCapacityTooltip + */ + showCapacityTooltip: function() { + Em.run.next(this,function () { + if (Em.isNone(this.$())) return; + + this.$().tooltip({ + title: function () { + return this.get('capacityValue') + ''; + }.bind(this), + container:"#"+this.elementId, + animation:false + }).tooltip('show'); + + Em.run.cancel(this.get('isShownTimer')); + this.set('isShownTimer',Em.run.debounce(this,function() { + if (this.$()) this.$().tooltip('hide'); + },500)); + }) + }.observes('currentLabel.capacity'), + + /** + * Destrioy tooltips when element destroys. + * @method willClearRender + */ + willClearRender:function() { + this.$().tooltip('destroy'); + }, + + // CHILD VIEWS + + /** + * Bar that shows total capcity value on node label. + * @type {Em.View} + */ + nodeLabelsBar:Em.View.extend({ + + classNameBindings:[':progress-bar'], + + attributeBindings:['capacityWidth:style'], + + /** + * Formatting pattern. + * @type {String} + */ + pattern:'width: %@%;', + + /** + * Alias for parentView.capacityValue. + * @type {String} + */ + value:Em.computed.alias('parentView.capacityValue'), + + /** + * Formats pattern whit value. + * @return {String} + */ + capacityWidth: function(c,o) { + return this.get('pattern').fmt((+this.get('value')<=100)?this.get('value'):100); + }.property('value') + }), + + /** + * Bar that shows current label capcity on node label. + * @type {Em.View} + */ + currentQueueBar:Em.View.extend({ + + classNameBindings:[':progress-bar',':ghost'], + + attributeBindings:['capacityWidth:style'], + + /** + * Formatting pattern. + * @type {String} + */ + pattern:'width: %@%;', + + /** + * Alias for parentView.currentLabel.capacity. + * @type {String} + */ + value:Em.computed.alias('parentView.currentLabel.capacity'), + + capacityWidth: function(c,o) { + return this.get('pattern').fmt((+this.get('value')<=100)?this.get('value'):0); + }.property('parentView.currentLabel.capacity','parentView.queue.labels.length') + }) }) }); App.CapacityEditFormView = Em.View.extend({ - mark:function () { - this.addObserver('controller.target.isEdit',this,'slide'); - if (!this.get('controller.target.isEdit')) { - this.$('.capacity-edit-form').hide(); - } - }.on('didInsertElement'), - slide:function () { - this.$('.capacity-edit-form').slideToggle(100); - } + dirty_capacity:function () { + return this.get('controller.content').changedAttributes().hasOwnProperty('capacity'); + }.property('controller.content.capacity'), + dirty_maxcapacity:function () { + return this.get('controller.content').changedAttributes().hasOwnProperty('maximum_capacity'); + }.property('controller.content.maximum_capacity') }); http://git-wip-us.apache.org/repos/asf/ambari/blob/95103a90/contrib/views/capacity-scheduler/src/main/resources/ui/app/controllers/queue.js ---------------------------------------------------------------------- diff --git a/contrib/views/capacity-scheduler/src/main/resources/ui/app/controllers/queue.js b/contrib/views/capacity-scheduler/src/main/resources/ui/app/controllers/queue.js index 1bffbea..41a4dfd 100644 --- a/contrib/views/capacity-scheduler/src/main/resources/ui/app/controllers/queue.js +++ b/contrib/views/capacity-scheduler/src/main/resources/ui/app/controllers/queue.js @@ -23,129 +23,265 @@ var _stopState = 'STOPPED'; App.QueueController = Ember.ObjectController.extend({ needs:['queues'], - isOperator:Em.computed.alias('controllers.queues.isOperator'), - isNotOperator:Em.computed.alias('controllers.queues.isNotOperator'), actions:{ setState:function (state) { - this.content.set('state', (state === "running") ? _runState : _stopState ); + this.set('content.state', (state === "running") ? (this.get('content.state') == null) ? null : _runState : _stopState ); }, - createQ:function (record) { + createQ:function () { this.get('controllers.queues').send('createQ',this.get('content')); }, addQ:function (path) { this.get('controllers.queues').send('addQ',path); }, delQ:function (record) { - this.get('controllers.queues').send('delQ',this.get('content')); + this.get('controllers.queues').send('delQ',record); }, renameQ:function (opt) { if (opt == 'ask') { - this.content.addObserver('name',this,this.setQueuePath); + this.set('tmpName',{name:this.get('content.name'),path:this.get('content.path')}); + this.get('content').addObserver('name',this,this.setQueuePath); this.toggleProperty('isRenaming'); return; } if (opt == 'cancel') { - this.send('rollbackProp','name'); - this.send('rollbackProp','id'); - this.send('rollbackProp','path'); - this.content.removeObserver('name',this,this.setQueuePath); + this.get('content').removeObserver('name',this,this.setQueuePath); + this.get('content').setProperties({ + name:this.get('tmpName.name'), + id:this.get('tmpName.path'), + path:this.get('tmpName.path') + }); this.toggleProperty('isRenaming'); return; } - if (opt) { - var self = this; - this.store.filter('queue',function (q) { - return q.id === self.content.id; - }).then(function(queues){ - if (queues.get('length') > 1) { - return self.content.get('errors').add('path', 'Queue already exists'); - } - self.toggleProperty('isRenaming'); - self.content.removeObserver('name',self,self.setQueuePath); - self.transitionToRoute('queue',self.content.id); - }) + if (opt && !this.get('content').get('errors.path')) { + + this.store.filter('label',function (label){ + return label.get('forQueue') == this.get('tmpName.path'); + }.bind(this)).then(function (labels) { + labels.forEach(function (label) { + label.materializeId([this.get('id'),label.get('name')].join('.')); + label.store.updateId(label,label); + }.bind(this)) + }.bind(this)); + this.toggleProperty('isRenaming'); + this.get('content').removeObserver('name',this,this.setQueuePath); + this.store.updateId(this.get('content'),this.get('content')); + this.transitionToRoute('queue',this.get('content.id')); + } }, - rollbackProp:function(prop){ - attributes = this.content.changedAttributes(); + // TODO bubble to route + rollbackProp:function(prop, queue){ + queue = queue || this.get('content'); + attributes = queue.changedAttributes(); if (attributes.hasOwnProperty(prop)) { - this.content.set(prop,attributes[prop][0]); + queue.set(prop,attributes[prop][0]); } } }, - setQueuePath:function (queue,o) { - var name = queue.get(o); - var parentPath = queue.get('parentPath'); - - queue.setProperties({ - name:name.replace(/\s|\./g, ''), - path:parentPath+'.'+name, - id:(parentPath+'.'+name).dasherize() - }); - if (name == '') { - queue.get('errors').add('path', 'This field is required'); - } - }, + /** + * Collection of modified fields in queue. + * @type {Object} - { [fileldName] : {Boolean} } + */ + queueDirtyFilelds:{}, + /** + * Represents renaming status. + * @type {Boolean} + */ isRenaming:false, - unsetRenaming:function () { - this.set('isRenaming',false); - }.observes('content'), + /** + * Object contains temporary name and path while renaming the queue. + * @type {Object} - { name : {String}, path : {String} } + */ + tmpName:{}, + + + + // COMPUTED PROPERTIES - isRoot:Ember.computed.equal('content.id', 'root'), - isRunning:Ember.computed.equal('content.state', _runState), + /** + * Alias for user admin status. + * @type {Boolean} + */ + isOperator:Em.computed.alias('controllers.queues.isOperator'), + + /** + * Inverted user admin status. + * @type {Boolean} + */ + isNotOperator:Em.computed.alias('controllers.queues.isNotOperator'), + + /** + * All queues in store. + * @type {DS.RecordArray} + */ allQueues:Em.computed.alias('controllers.queues.content'), - allQueuesArranged:Em.computed.alias('controllers.queues.arrangedContent'), - handleAcl:function (key,value) { - if (value) { - this.set(key,(value == '*')?'*':' '); - } - return (this.get(key) == '*')? '*':'custom'; - }, + /** + * Array of leaf queues. + * @return {Array} + */ + leafQueues:function () { + return this.get('allQueues').filterBy('parentPath',this.get('content.parentPath')); + }.property('allQueues.length','content.parentPath'), + + /** + * Parent of current queue. + * @return {App.Queue} + */ + parentQueue: function () { + return this.store.getById('queue',this.get('content.parentPath')); + }.property('content.parentPath'), - acl_administer_queue: function (key, value, previousValue) { + /** + * Returns true if queue is root. + * @type {Boolean} + */ + isRoot:Ember.computed.match('content.id', /^(root|root.default)$/), + + /** + * Represents queue run state. Returns true if state is null. + * @return {Boolean} + */ + isRunning: function() { + return this.get('content.state') == _runState || this.get('content.state') == null; + }.property('content.state'), + + /** + * Queue's acl_administer_queue property can be set to '*' (everyone) or ' ' (nobody) thru this property. + * + * @param {String} key + * @param {String} value + * @return {String} - '*' if equal to '*' or 'custom' in other case. + */ + acl_administer_queue: function (key, value) { return this.handleAcl('content.acl_administer_queue',value); }.property('content.acl_administer_queue'), + + /** + * Returns true if acl_administer_queue is set to '*' + * @type {Boolean} + */ aaq_anyone:Ember.computed.equal('acl_administer_queue', '*'), - aaq_dirty:function () { - var attributes = this.content.changedAttributes(); - return attributes.hasOwnProperty('acl_administer_queue'); - }.property('content.acl_administer_queue'), - acl_submit_applications: function (key, value, previousValue) { + /** + * Queue's acl_submit_applications property can be set to '*' (everyone) or ' ' (nobody) thru this property. + * + * @param {String} key + * @param {String} value + * @return {String} - '*' if equal to '*' or 'custom' in other case. + */ + acl_submit_applications: function (key, value) { return this.handleAcl('content.acl_submit_applications',value); }.property('content.acl_submit_applications'), + + /** + * Returns true if acl_submit_applications is set to '*' + * @type {Boolean} + */ asa_anyone:Ember.computed.equal('acl_submit_applications', '*'), - asa_dirty:function () { - var attributes = this.content.changedAttributes(); - return attributes.hasOwnProperty('acl_submit_applications'); - }.property('content.acl_submit_applications'), + /** + * Error messages for queue path. + * @type {[type]} + */ + pathErrors:Ember.computed.mapBy('content.errors.path','message'), + + + + // OBSERVABLES + + /** + * Marks each queue in leaf with 'overCapacity' if sum if their capacity values is greater then 100. + * @method capacityControl + */ capacityControl:function () { - var leafQueues = this.get('leafQueues'); - var total = 0; + var leafQueues = this.get('leafQueues'), + total = leafQueues.reduce(function (prev, queue) { + return +queue.get('capacity') + prev; + },0); - leafQueues.forEach(function (queue) { - total+=Number(queue.get('capacity')); - }); leafQueues.setEach('overCapacity',total>100); }.observes('content.capacity','leafQueues.@each.capacity'), - leafQueues:function () { - return this.get('allQueues').filterBy('parentPath',this.get('content.parentPath')); - }.property('allQueues.length','content.parentPath'), - queueNamesControl:function (c,o) { - var leaf = c.get('leafQueues'); - var parent = c.get('allQueues').filterBy('path',c.get('content.parentPath')).get('firstObject'); - if (parent) parent.set('queueNames',leaf.mapBy('name').join() || null); + /** + * Keeps track of leaf queues and sets 'queues' value of parent to list of their names. + * @method queueNamesControl + */ + queueNamesControl:function () { + if (this.get('parentQueue')) this.set('parentQueue.queuesArray',this.get('leafQueues').mapBy('name')); }.observes('allQueues.length','allQueues.@each.name','content'), - pathErrors:Ember.computed.mapBy('content.errors.path','message') - + /** + * Adds observers for each queue attribute. + * @method dirtyObserver + */ + dirtyObserver:function () { + this.get('content.constructor.transformedAttributes.keys.list').forEach(function(item) { + this.addObserver('content.' + item,this,'propertyBecomeDirty'); + }.bind(this)); + }.observes('content'), + + + + // METHODS + + /** + * Sets ACL value to '*' or ' ' and returns '*' and 'custom' respectively. + * @param {String} key - ACL attribute + * @param {String} value - ACL value + * @return {String} + */ + handleAcl:function (key,value) { + if (value) { + this.set(key,(value == '*')?'*':' '); + } + return (this.get(key) == '*' || this.get(key) == null) ? '*' : 'custom'; + }, + + /** + * Sets queue path and id in accordance with the name. + * Also adds errors to queue if name is incorrect. + * + * @method setQueuePath + */ + setQueuePath:function (queue) { + var name = queue.get('name').replace(/\s|\./g, ''), + parentPath = queue.get('parentPath'); + + queue.setProperties({ + name:name, + path:parentPath+'.'+name, + id:(parentPath+'.'+name).dasherize() + }); + + if (name == '') { + queue.get('errors').add('path', 'This field is required'); + } + + this.store.filter('queue',function (q) { + return q.get('id') === queue.get('id'); + }.bind(this)).then(function (queues){ + if (queues.get('length') > 1) { + return this.get('content').get('errors').add('path', 'Queue already exists'); + } + }.bind(this)); + }, + + /** + * Adds modified queue fileds to q queueDirtyFilelds collection. + * @param {String} controller + * @param {String} property + * @method propertyBecomeDirty + */ + propertyBecomeDirty:function (controller, property) { + var queueProp = property.split('.').objectAt(1); + this.set('queueDirtyFilelds.' + queueProp, this.get('content').changedAttributes().hasOwnProperty(queueProp)); + } }); App.ErrorController = Ember.ObjectController.extend(); http://git-wip-us.apache.org/repos/asf/ambari/blob/95103a90/contrib/views/capacity-scheduler/src/main/resources/ui/app/controllers/queues.js ---------------------------------------------------------------------- diff --git a/contrib/views/capacity-scheduler/src/main/resources/ui/app/controllers/queues.js b/contrib/views/capacity-scheduler/src/main/resources/ui/app/controllers/queues.js index b03e633..aecd265 100644 --- a/contrib/views/capacity-scheduler/src/main/resources/ui/app/controllers/queues.js +++ b/contrib/views/capacity-scheduler/src/main/resources/ui/app/controllers/queues.js @@ -24,8 +24,13 @@ App.QueuesController = Ember.ArrayController.extend({ sortProperties: ['name'], sortAscending: true, actions:{ + loadTagged:function (tag) { + this.transitionToRoute('queue','root').then(function() { + this.store.fetchTagged(App.Queue,tag); + }.bind(this)); + }, goToQueue:function (queue) { - this.transitionToRoute('queue',queue) + this.transitionToRoute('queue',queue); }, askPath:function () { this.set('isWaitingPath',true); @@ -53,6 +58,9 @@ App.QueuesController = Ember.ArrayController.extend({ record.save().then(Em.run.bind(this,this.set,'newQueue',null)); }, delQ:function (record) { + var queues = this.get('content'), + parentPath = record.get('parentPath'), + name = record.get('name'); if (record.get('isNew')) { this.set('newQueue',null); } @@ -60,9 +68,15 @@ App.QueuesController = Ember.ArrayController.extend({ this.set('hasDeletedQueues',true); } if (record.isCurrent) { - this.transitionToRoute('queue',record.get('parentPath')); + this.transitionToRoute('queue',parentPath.toLowerCase()) + .then(Em.run.schedule('afterRender', function () { + record.destroyRecord().then(function() { + queues.findBy('path',parentPath).set('queuesArray',{'exclude':name}); + }); + })); + } else { + record.destroyRecord(); } - this.store.deleteRecord(record); }, saveConfig:function (mark) { if (mark == 'restart') { @@ -70,11 +84,15 @@ App.QueuesController = Ember.ArrayController.extend({ } else if (mark == 'refresh') { this.get('store').markForRefresh(); } + var collectedLabels = this.get('model').reduce(function (prev,q) { + return prev.pushObjects(q.get('labels.content')); + },[]); var hadDeletedQueues = this.get('hasDeletedQueues'), scheduler = this.get('scheduler').save(), model = this.get('model').save(), - all = Em.RSVP.Promise.all([model,scheduler]); + labels = DS.ManyArray.create({content:collectedLabels}).save(), + all = Em.RSVP.Promise.all([labels,model,scheduler]); all.catch(Em.run.bind(this,this.saveError,hadDeletedQueues)); @@ -85,18 +103,147 @@ App.QueuesController = Ember.ArrayController.extend({ } }, + /** + * User admin status. + * @type {Boolean} + */ + isOperator:false, + + /** + * Inverted isOperator value. + * @type {Boolean} + */ + isNotOperator:cmp.not('isOperator'), + + /** + * Flag to show input for adding queue. + * @type {Boolean} + */ + isWaitingPath:false, + + /** + * Property for error message which may appear when saving queue. + * @type {Object} + */ alertMessage:null, + + /** + * Temporary filed for new queue + * @type {App.Queue} + */ + newQueue:null, + + /** + * True if newQueue is not empty. + * @type {Boolean} + */ + hasNewQueue: cmp.bool('newQueue'), + + /** + * Current configuration version tag. + * @type {[type]} + */ + current_tag: cmp.alias('store.current_tag'), + + /** + * Scheduler record + * @type {App.Scheduler} + */ + scheduler:null, + + /** + * Collection of modified fields in Scheduler. + * @type {Object} - { [fileldName] : {Boolean} } + */ + schedulerDirtyFilelds:{}, + + + configNote: cmp.alias('store.configNote'), + + /*configNote:function (arg,val) { + if (arguments.length > 1) { + this.set('store.configNote',val); + } + return this.get('store.configNote'); + }.property('store.configNote'),*/ + + tags:function () { + return this.store.find('tag'); + }.property('store.current_tag'), + + sortedTags: cmp.sort('tags', function(a, b){ + return (+a.id > +b.id)?(+a.id < +b.id)?0:-1:1; + }), + + saveError:function (hadDeletedQueues,error) { this.set('hasDeletedQueues',hadDeletedQueues); var response = JSON.parse(error.responseText); this.set('alertMessage',response); }, - isOperator:false, - isNotOperator:cmp.not('isOperator'), + propertyBecomeDirty:function (controller,property) { + var schedProp = property.split('.').objectAt(1); + this.set('schedulerDirtyFilelds.' + schedProp, this.get('scheduler').changedAttributes().hasOwnProperty(schedProp)); + }, + + dirtyObserver:function () { + this.get('scheduler.constructor.transformedAttributes.keys.list').forEach(function(item) { + this.addObserver('scheduler.' + item,this,'propertyBecomeDirty'); + }.bind(this)); + }.observes('scheduler'), + + + trackNewQueue:function () { + var newQueue = this.get('newQueue'); + if (Em.isEmpty(newQueue)) { + return; + } + var name = newQueue.get('name'); + var parentPath = newQueue.get('parentPath'); + + this.get('newQueue').setProperties({ + name:name.replace(/\s/g, ''), + path:parentPath+'.'+name, + id:(parentPath+'.'+name).dasherize() + }); + + }.observes('newQueue.name'), + + + + // TRACKING OF RESTART REQUIREMENT + + /** + * check if RM needs restart + * @type {bool} + */ + needRestart: cmp.any('hasDeletedQueues', 'hasRenamedQueues'), + + /** + * True if some queue of desired configs was removed. + * @type {Boolean} + */ + hasDeletedQueues:false, + + /** + * List of queues with modified name. + * @type {Array} + */ + renamedQueues:cmp.filter('content.@each.name',function (queue){ + return queue.changedAttributes().hasOwnProperty('name') && !queue.get('isNewQueue'); + }), + + /** + * True if renamedQueues is not empty. + * @type {Boolean} + */ + hasRenamedQueues: cmp.notEmpty('renamedQueues.[]'), + + + + // TRACKING OF REFRESH REQUIREMENT - isWaitingPath:false, - /** * check if RM needs refresh * @type {bool} @@ -104,34 +251,59 @@ App.QueuesController = Ember.ArrayController.extend({ needRefresh: cmp.and('needRefreshProps','noNeedRestart'), /** - * props for 'needRefresh' + * Inverted needRestart value. + * @type {Boolean} + */ + noNeedRestart: cmp.not('needRestart'), + + /** + * Check properties for refresh requirement + * @type {Boolean} + */ + needRefreshProps: cmp.any('hasChanges', 'hasNewQueues','dirtyScheduler'), + + /** + * List of modified queues. + * @type {Array} + */ + dirtyQueues:function () { + return this.get('content').filter(function (q) { + return q.get('isAnyDirty'); + }); + }.property('content.@each.isAnyDirty'), + + /** + * True if dirtyQueues is not empty. + * @type {Boolean} */ - dirtyQueues: cmp.filterBy('content', 'isDirty', true), - dirtyScheduler: cmp.bool('scheduler.isDirty'), - newQueues: cmp.filterBy('content', 'isNewQueue', true), hasChanges: cmp.notEmpty('dirtyQueues.[]'), + + /** + * List of new queues. + * @type {Array} + */ + newQueues: cmp.filterBy('content', 'isNewQueue', true), + + /** + * True if newQueues is not empty. + * @type {Boolean} + */ hasNewQueues: cmp.notEmpty('newQueues.[]'), - needRefreshProps: cmp.any('hasChanges', 'hasNewQueues','dirtyScheduler'), - noNeedRestart: cmp.not('needRestart'), - /** - * check if RM needs restart - * @type {bool} + * True if scheduler is modified. + * @type {[type]} */ - needRestart: cmp.any('hasDeletedQueues', 'hasRenamedQueues'), - + dirtyScheduler: cmp.bool('scheduler.isDirty'), + + + // TRACKING OF PRESERVATION POSSIBILITY + /** - * props for 'needRestart' + * check there is some changes for save + * @type {bool} */ - hasDeletedQueues:false, - hasRenamedQueues: cmp.notEmpty('renamedQueues.[]'), - renamedQueues:function () { - return this.content.filter(function(queue){ - var attr = queue.changedAttributes(); - return attr.hasOwnProperty('name') && !queue.get('isNewQueue'); - }); - }.property('content.@each.name'), + needSave: cmp.any('needRestart', 'needRefresh'), /** * check if can save configs @@ -140,43 +312,42 @@ App.QueuesController = Ember.ArrayController.extend({ canNotSave: cmp.any('hasOverCapacity', 'hasUncompetedAddings','hasNotValid','isNotOperator'), /** - * props for canNotSave + * List of not valid queues. + * @type {Array} */ notValid:cmp.filterBy('content','isValid',false), - overCapacityQ:cmp.filterBy('content','overCapacity',true), - uncompetedAddings:cmp.filterBy('content', 'isNew', true), - hasNotValid:cmp.notEmpty('notValid.[]'), - hasOverCapacity:cmp.notEmpty('overCapacityQ.[]'), - hasUncompetedAddings:cmp.notEmpty('uncompetedAddings.[]'), /** - * check there is some changes for save - * @type {bool} + * True if notValid is not empty. + * @type {Boolean} */ - needSave: cmp.any('needRestart', 'needRefresh'), - - newQueue:null, - hasNewQueue: cmp.bool('newQueue'), - trackNewQueue:function () { - var newQueue = this.get('newQueue'); - if (Em.isEmpty(newQueue)){ - return; - } - var name = newQueue.get('name'); - var parentPath = newQueue.get('parentPath'); + hasNotValid:cmp.notEmpty('notValid.[]'), - this.get('newQueue').setProperties({ - name:name.replace(/\s/g, ''), - path:parentPath+'.'+name, - id:(parentPath+'.'+name).dasherize() + /** + * List of queues with excess of capacity + * @type {Array} + */ + overCapacityQ:function () { + return this.get('content').filter(function (q) { + return q.get('overCapacity'); }); + }.property('content.@each.overCapacity'), - }.observes('newQueue.name'), + /** + * True if overCapacityQ is not empty. + * @type {Boolean} + */ + hasOverCapacity:cmp.notEmpty('overCapacityQ.[]'), - configNote:function (arg,val) { - if (arguments.length > 1) { - this.set('store.configNote',val); - } - return this.get('store.configNote'); - }.property('store.configNote') + /** + * List of queues with incompete adding process + * @type {[type]} + */ + uncompetedAddings:cmp.filterBy('content', 'isNew', true), + + /** + * True if uncompetedAddings is not empty. + * @type {Boolean} + */ + hasUncompetedAddings:cmp.notEmpty('uncompetedAddings.[]') }); http://git-wip-us.apache.org/repos/asf/ambari/blob/95103a90/contrib/views/capacity-scheduler/src/main/resources/ui/app/initialize.js ---------------------------------------------------------------------- diff --git a/contrib/views/capacity-scheduler/src/main/resources/ui/app/initialize.js b/contrib/views/capacity-scheduler/src/main/resources/ui/app/initialize.js index dda4c83..57f57a0 100644 --- a/contrib/views/capacity-scheduler/src/main/resources/ui/app/initialize.js +++ b/contrib/views/capacity-scheduler/src/main/resources/ui/app/initialize.js @@ -24,6 +24,12 @@ App.testMode = false; // adapters require('adapters'); +//serializers +require('serializers'); + +//store +require('store'); + //components require('components'); http://git-wip-us.apache.org/repos/asf/ambari/blob/95103a90/contrib/views/capacity-scheduler/src/main/resources/ui/app/models/queue.js ---------------------------------------------------------------------- diff --git a/contrib/views/capacity-scheduler/src/main/resources/ui/app/models/queue.js b/contrib/views/capacity-scheduler/src/main/resources/ui/app/models/queue.js index cf22437..2044ff1 100644 --- a/contrib/views/capacity-scheduler/src/main/resources/ui/app/models/queue.js +++ b/contrib/views/capacity-scheduler/src/main/resources/ui/app/models/queue.js @@ -18,11 +18,49 @@ var App = require('app'); +App.Label = DS.Model.extend({ + queue: DS.belongsTo('queue'), + capacity: DS.attr('number', { defaultValue: 0 }), + maximum_capacity: DS.attr('number', { defaultValue: 0 }), + forQueue:function() { + return this.get('id').substring(0,this.get('id').lastIndexOf('.')); + }.property('id'), + name:function() { + return this.get('id').substr(this.get('id').lastIndexOf('.')+1); + }.property('id'), + + overCapacity:false, + isNotExist:function () { + return this.get('store.nodeLabels.content').findBy('name',this.get('name')).notExist; + }.property('store.nodeLabels.content.@each.notExist') +}); + App.Scheduler = DS.Model.extend({ maximum_am_resource_percent: DS.attr('number', { defaultValue: 0 }), maximum_applications: DS.attr('number', { defaultValue: 0 }), node_locality_delay: DS.attr('number', { defaultValue: 0 }), - resource_calculator: DS.attr('string', { defaultValue: '' }), + resource_calculator: DS.attr('string', { defaultValue: '' }) +}); + + +/** + * Represents tagged of configuraion vresion. + * + */ +App.Tag = DS.Model.extend({ + tag:DS.attr('string'), + isCurrent:function () { + return this.get('tag') === this.get('store.current_tag'); + }.property('store.current_tag'), + changed:function () { + return (this.get('tag').match(/version[1-9]+/))?moment(+this.get('tag').replace('version','')).fromNow():''; + }.property('tag'), + updateTime:function () { + Em.run.later(this,function () { + this.trigger('tick'); + this.notifyPropertyChange('tag'); + },5000); + }.on('init','tick') }); /** @@ -30,6 +68,55 @@ App.Scheduler = DS.Model.extend({ * */ App.Queue = DS.Model.extend({ + labels: DS.hasMany('label'), + sortBy:['name'], + sortedLabels:Em.computed.sort('labels','sortBy'), + + _accessAllLabels:DS.attr('boolean'), + accessAllLabels:function (key,val) { + var labels = this.get('store.nodeLabels').map(function(label) { + return this.store.getById('label',[this.get('id'),label.name].join('.')); + }.bind(this)); + + if (arguments.length > 1) { + this.set('_accessAllLabels',val); + + if (this.get('_accessAllLabels')) { + labels.forEach(function(lb) { + + var containsByParent = (Em.isEmpty(this.get('parentPath')))?true:this.store.getById('queue',this.get('parentPath')).get('labels').findBy('name',lb.get('name')); + if (!this.get('labels').contains(lb) && !!containsByParent) { + this.get('labels').pushObject(lb); + this.notifyPropertyChange('labels'); + } + }.bind(this)); + } + } + + if (this.get('labels.length') != labels.get('length')) { + this.set('_accessAllLabels',false); + } + + return this.get('_accessAllLabels'); + }.property('_accessAllLabels','labels.[]'), + + isAnyDirty: function () { + return this.get('isDirty') || !Em.isEmpty(this.get('labels').findBy('isDirty',true)) || this.get('isLabelsDirty'); + }.property('isDirty','labels.@each.isDirty','initialLabels','isLabelsDirty'), + + initialLabels:[], + labelsLoad:function() { + this.set('initialLabels',this.get('labels').mapBy('id')); + }.on('didLoad','didUpdate','didCreate'), + + isLabelsDirty:function () { + var il = this.get('initialLabels').sort(); + var cl = this.get('labels').mapBy('id').sort(); + return !((il.length == cl.length) && il.every(function(element, index) { + return element === cl[index]; + })); + }.property('initialLabels', 'labels.[]', '_accessAllLabels'), + name: DS.attr('string'), parentPath: DS.attr('string'), depth: DS.attr('number'), @@ -42,20 +129,59 @@ App.Queue = DS.Model.extend({ capacity: DS.attr('number', { defaultValue: 0 }), maximum_capacity: DS.attr('number', { defaultValue: 0 }), - unfunded_capacity: DS.attr('number', { defaultValue: 0 }), - + //unfunded_capacity: DS.attr('number', { defaultValue: 0 }), + user_limit_factor: DS.attr('number', { defaultValue: 1 }), minimum_user_limit_percent: DS.attr('number', { defaultValue: 100 }), - maximum_applications: DS.attr('number', { defaultValue: '' }), - maximum_am_resource_percent: DS.attr('number', { defaultValue: '' }), + maximum_applications: DS.attr('number', { defaultValue: null }), + maximum_am_resource_percent: DS.attr('number', { defaultValue: null }), - queueNames: DS.attr('string'), - queueNamesArray:function () { - return (this.get('queueNames.length')>0)?this.get('queueNames').split(','):[]; - }.property('queueNames'), + queues: DS.attr('string'), + queuesArray:function (key,val) { + var qrray; + if (arguments.length > 1) { + if (typeof val === 'object' && val.hasOwnProperty('exclude')) { + qrray = (this.get('queues'))?this.get('queues').split(','):[]; + this.set('queues',qrray.removeObject(val.exclude).join(',') || null); + } else { + this.set('queues',val.join(',') || null); + } + } + return (this.get('queues'))?this.get('queues').split(','):[]; + }.property('queues'), - overCapacity:false, + _overCapacity:false, + overCapacity:function(key,val) { + if (arguments.length > 1) { + this.set('_overCapacity',val); + } + + return this.get('_overCapacity') || !Em.isEmpty(this.get('labels').filterBy('overCapacity')); + }.property('_overCapacity','labels.@each.overCapacity'), //new queue flag - isNewQueue:DS.attr('boolean', {defaultValue: false}) + isNewQueue:DS.attr('boolean', {defaultValue: false}), + + version:null, + clearTag:function () { + this.set('version', null); + }.observes( + 'name', + 'parentPath', + 'depth', + 'path', + 'state', + 'acl_administer_queue', + 'acl_submit_applications', + 'capacity', + 'maximum_capacity', + 'unfunded_capacity', + 'user_limit_factor', + 'minimum_user_limit_percent', + 'maximum_applications', + 'maximum_am_resource_percent', + 'queues', + 'labels.@each.capacity', + 'labels.@each.maximum_capacity' + ) });