Repository: ambari Updated Branches: refs/heads/trunk 5cd8c291a -> d2e2a7914
AMBARI-10268 Draw Gauge widget from the relevant retrieved widget data from the API. (atkach) Project: http://git-wip-us.apache.org/repos/asf/ambari/repo Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/d2e2a791 Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/d2e2a791 Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/d2e2a791 Branch: refs/heads/trunk Commit: d2e2a79141afe440f713533f99ab98722c3084dd Parents: 5cd8c29 Author: Andrii Tkach <[email protected]> Authored: Mon Mar 30 13:52:35 2015 +0300 Committer: Andrii Tkach <[email protected]> Committed: Mon Mar 30 13:52:35 2015 +0300 ---------------------------------------------------------------------- .../HBASE/Append_num_ops_&_Delete_num_ops.json | 6 +- .../data/widget_layouts/HBASE/stack_layout.json | 32 +++++ .../controllers/main/service/info/summary.js | 3 +- ambari-web/app/mixins/common/widget_mixin.js | 58 ++++++++- ambari-web/app/models/widget.js | 2 + ambari-web/app/styles/widget_layout.less | 29 +++-- .../templates/common/widget/gauge_widget.hbs | 22 ++++ ambari-web/app/views.js | 3 +- .../views/common/widget/gauge_widget_view.js | 120 +++++++++++++++++++ .../views/common/widget/graph_widget_view.js | 4 +- .../views/common/widget/template_widget_view.js | 51 +------- .../test/mixins/common/widget_mixin_test.js | 80 +++++++++++-- 12 files changed, 328 insertions(+), 82 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/ambari/blob/d2e2a791/ambari-web/app/assets/data/metrics/HBASE/Append_num_ops_&_Delete_num_ops.json ---------------------------------------------------------------------- diff --git a/ambari-web/app/assets/data/metrics/HBASE/Append_num_ops_&_Delete_num_ops.json b/ambari-web/app/assets/data/metrics/HBASE/Append_num_ops_&_Delete_num_ops.json index 2db12a0..87e0294 100644 --- a/ambari-web/app/assets/data/metrics/HBASE/Append_num_ops_&_Delete_num_ops.json +++ b/ambari-web/app/assets/data/metrics/HBASE/Append_num_ops_&_Delete_num_ops.json @@ -6,6 +6,10 @@ "service_name" : "HBASE" }, "metrics" : { + "jvm": { + "HeapMemoryUsed": 61, + "HeapMemoryMax": 100 + }, "hbase" : { "ipc" : { "IPC" : { @@ -13,8 +17,8 @@ } }, "regionserver" : { + "percentFilesLocal" : 99, "Server" : { - "percentFilesLocal" : 99, "Append_num_ops" : [ [ 2.0, http://git-wip-us.apache.org/repos/asf/ambari/blob/d2e2a791/ambari-web/app/assets/data/widget_layouts/HBASE/stack_layout.json ---------------------------------------------------------------------- diff --git a/ambari-web/app/assets/data/widget_layouts/HBASE/stack_layout.json b/ambari-web/app/assets/data/widget_layouts/HBASE/stack_layout.json index 4c8d3ee..6285138 100644 --- a/ambari-web/app/assets/data/widget_layouts/HBASE/stack_layout.json +++ b/ambari-web/app/assets/data/widget_layouts/HBASE/stack_layout.json @@ -72,6 +72,38 @@ "properties": { "display_unit": "%" } + }, + { + "widget_name": "NAMENODE_HEAP", + "display_name": "NameNode Heap", + "widget_type": "GAUGE", + "description": "", + "metrics":[ + { + "name": "java.lang:type=Memory.HeapMemoryUsage[used]", + "widget_id": "metrics/jvm/HeapMemoryUsed", + "category": "", + "service_name": "HBASE", + "component_name": "HBASE_REGIONSERVER" + }, + { + "name": "java.lang:type=Memory.HeapMemoryUsage[max]", + "widget_id": "metrics/jvm/HeapMemoryMax", + "category": "", + "service_name": "HBASE", + "component_name": "HBASE_REGIONSERVER" + } + ], + "values": [ + { + "name": "NameNode heap", + "value": "${java.lang:type=Memory.HeapMemoryUsage[used]/java.lang:type=Memory.HeapMemoryUsage[max]}" + } + ], + "properties": { + "warning_threshold": 0.9, + "error_threshold": 0.7 + } } ] } http://git-wip-us.apache.org/repos/asf/ambari/blob/d2e2a791/ambari-web/app/controllers/main/service/info/summary.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/controllers/main/service/info/summary.js b/ambari-web/app/controllers/main/service/info/summary.js index ed5dbb9..d1d1be8 100644 --- a/ambari-web/app/controllers/main/service/info/summary.js +++ b/ambari-web/app/controllers/main/service/info/summary.js @@ -346,8 +346,7 @@ App.MainServiceInfoSummaryController = Em.Controller.extend({ stackVersionURL: App.get('stackVersionURL'), serviceName: this.get('content.serviceName') }, - success: 'loadStackWidgetsLayoutSuccessCallback', - error: 'loadStackWidgetsLayoutErrorCallback' + success: 'loadStackWidgetsLayoutSuccessCallback' }); }, http://git-wip-us.apache.org/repos/asf/ambari/blob/d2e2a791/ambari-web/app/mixins/common/widget_mixin.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/mixins/common/widget_mixin.js b/ambari-web/app/mixins/common/widget_mixin.js index 1f90500..7d1abfe 100644 --- a/ambari-web/app/mixins/common/widget_mixin.js +++ b/ambari-web/app/mixins/common/widget_mixin.js @@ -24,7 +24,7 @@ App.WidgetMixin = Ember.Mixin.create({ * @type {RegExp} * @const */ - EXPRESSION_REGEX: /\$\{([\w\.\+\-\*\/\(\)]*)\}/g, + EXPRESSION_REGEX: /\$\{([\w\.\+\-\*\/\(\)\:\=\[\]]*)\}/g, /** * @type {RegExp} @@ -36,7 +36,7 @@ App.WidgetMixin = Ember.Mixin.create({ * @type {RegExp} * @const */ - VALUE_NAME_REGEX: /[\w\.]+/g, + VALUE_NAME_REGEX: /[\w\.\:\=\[\]]+/g, /** * common metrics container @@ -102,6 +102,55 @@ App.WidgetMixin = Ember.Mixin.create({ }, /** + * calculate series datasets for graph widgets + */ + calculateValues: function () { + var metrics = this.get('metrics'); + var displayUnit = this.get('content.properties.display_unit'); + + this.get('content.values').forEach(function (value) { + var computeExpression = this.computeExpression(this.extractExpressions(value), metrics); + value.computedValue = value.value.replace(this.get('EXPRESSION_REGEX'), function (match) { + return (computeExpression[match]) ? computeExpression[match] + (displayUnit || "") : Em.I18n.t('common.na'); + }); + }, this); + }, + + /** + * compute expression + * @param expressions + * @param metrics + * @returns {object} + */ + computeExpression: function (expressions, metrics) { + var result = {}; + + expressions.forEach(function (_expression) { + var validExpression = true; + var value = ""; + + //replace values with metrics data + var beforeCompute = _expression.replace(this.get('VALUE_NAME_REGEX'), function (match) { + if (metrics.someProperty('name', match)) { + return metrics.findProperty('name', match).data; + } else { + validExpression = false; + console.warn('Metrics not found to compute expression'); + } + }); + + if (validExpression) { + //check for correct math expression + validExpression = this.get('MATH_EXPRESSION_REGEX').test(beforeCompute); + !validExpression && console.warn('Value is not correct mathematical expression'); + } + + result['${' + _expression + '}'] = (validExpression) ? Number(window.eval(beforeCompute)).toString() : value; + }, this); + return result; + }, + + /** * get data formatted for request * @param {Array} metrics */ @@ -143,11 +192,10 @@ App.WidgetMixin = Ember.Mixin.create({ getServiceComponentMetricsSuccessCallback: function (data, opt, params) { var metrics = []; - var metricsData = data.metrics[params.serviceName.toLowerCase()]; this.get('content.metrics').forEach(function (_metric) { - if (Em.get(metricsData, _metric.name)) { - _metric.data = Em.get(metricsData, _metric.name); + if (Em.get(data, _metric.widget_id.replace(/\//g, '.'))) { + _metric.data = Em.get(data, _metric.widget_id.replace(/\//g, '.')); this.get('metrics').pushObject(_metric); } }, this); http://git-wip-us.apache.org/repos/asf/ambari/blob/d2e2a791/ambari-web/app/models/widget.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/models/widget.js b/ambari-web/app/models/widget.js index bdbb527..b093a53 100644 --- a/ambari-web/app/models/widget.js +++ b/ambari-web/app/models/widget.js @@ -59,6 +59,8 @@ App.Widget = DS.Model.extend({ return App.GraphWidgetView; case 'NUMBER': return App.TemplateWidgetView; + case 'GAUGE': + return App.GaugeWidgetView; default: return Em.View; } http://git-wip-us.apache.org/repos/asf/ambari/blob/d2e2a791/ambari-web/app/styles/widget_layout.less ---------------------------------------------------------------------- diff --git a/ambari-web/app/styles/widget_layout.less b/ambari-web/app/styles/widget_layout.less index 265e666..8b4b94c 100644 --- a/ambari-web/app/styles/widget_layout.less +++ b/ambari-web/app/styles/widget_layout.less @@ -18,21 +18,28 @@ #widget_layout { .widget { + .title { + padding: 5px 0 0 5px; + height: 25px; + font-weight: bold; + text-align: left; + } + .content { + text-align: center; + color: #5ab400; + padding-top: 35px; + font-weight: bold; + font-size: 35px; + } .template-widget { height: 150px; width: 90%; - .title { - padding: 5px 0 0 5px; - height: 25px; - font-weight: bold; - text-align: left; - } + } + .gauge-widget { + height: 150px; + width: 90%; .content { - text-align: center; - color: #5ab400; - padding-top: 35px; - font-weight: bold; - font-size: 35px; + padding-top: 5px; } } } http://git-wip-us.apache.org/repos/asf/ambari/blob/d2e2a791/ambari-web/app/templates/common/widget/gauge_widget.hbs ---------------------------------------------------------------------- diff --git a/ambari-web/app/templates/common/widget/gauge_widget.hbs b/ambari-web/app/templates/common/widget/gauge_widget.hbs new file mode 100644 index 0000000..03a975f --- /dev/null +++ b/ambari-web/app/templates/common/widget/gauge_widget.hbs @@ -0,0 +1,22 @@ +{{! +* 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. +}} + +<div class="gauge-widget thumbnail"> + <div class="caption title">{{view.title}}</div> + <div class="content"> {{view view.chartView}}</div> +</div> http://git-wip-us.apache.org/repos/asf/ambari/blob/d2e2a791/ambari-web/app/views.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/views.js b/ambari-web/app/views.js index 472d2a5..b074010 100644 --- a/ambari-web/app/views.js +++ b/ambari-web/app/views.js @@ -65,13 +65,14 @@ require('views/common/configs/widgets/time_interval_spinner_view'); require('views/common/configs/widgets/toggle_config_widget_view'); require('views/common/configs/widgets/overrides/slider_config_widget_override_view'); require('views/common/configs/service_config_layout_tab_view'); -require('views/common/widget/template_widget_view'); require('views/common/filter_combobox'); require('views/common/filter_combo_cleanable'); require('views/common/table_view'); require('views/common/progress_bar_view'); require('views/common/controls_view'); require('views/common/widget/graph_widget_view'); +require('views/common/widget/template_widget_view'); +require('views/common/widget/gauge_widget_view'); require('views/login'); require('views/main'); require('views/main/menu'); http://git-wip-us.apache.org/repos/asf/ambari/blob/d2e2a791/ambari-web/app/views/common/widget/gauge_widget_view.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/views/common/widget/gauge_widget_view.js b/ambari-web/app/views/common/widget/gauge_widget_view.js new file mode 100644 index 0000000..0964e11 --- /dev/null +++ b/ambari-web/app/views/common/widget/gauge_widget_view.js @@ -0,0 +1,120 @@ +/** + * 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. + */ + +var App = require('app'); + +App.GaugeWidgetView = Em.View.extend(App.WidgetMixin, { + templateName: require('templates/common/widget/gauge_widget'), + + /** + * @type {string} + */ + title: '', + + /** + * @type {string} + */ + value: '', + + /** + * common metrics container + * @type {Array} + */ + metrics: [], + + drawWidget: function () { + if (this.get('isLoaded')) { + this.calculateValues(); + this.set('title', this.get('content.values')[0].name); + this.set('value', this.get('content.values')[0].computedValue); + } + }.observes('isLoaded'), + + chartView: App.ChartPieView.extend({ + stroke: '#D6DDDF', //light grey + innerR: 25, + + FACTOR: 100, + + MAX_VALUE: 100, + + warningThreshold: function(){ + return this.get('parentView.content.properties.warning_threshold'); + }.property('parentView.content.properties.warning_threshold'), + + errorThreshold: function(){ + return this.get('parentView.content.properties.error_threshold'); + }.property('parentView.content.properties.error_threshold'), + + id: function() { + return this.get('parentView.content.widgetName'); + }.property('parentView.content.widgetName'), + + existCenterText: true, + centerTextColor: function () { + return this.get('contentColor'); + }.property('contentColor'), + + palette: new Rickshaw.Color.Palette({ + scheme: [ '#FFFFFF', '#D6DDDF'].reverse() + }), + + data: function () { + var data = parseFloat(this.get('parentView.value')) * this.get('FACTOR'); + if (isNaN(data)) return [this.get('MAX_VALUE'), 0]; + return [data, this.get('MAX_VALUE') - data]; + }.property('parentView.value'), + + contentColor: function () { + var used = parseFloat(this.get('parentView.value')) * this.get('FACTOR'); + var thresh1 = parseFloat(this.get('warningThreshold')) * this.get('FACTOR'); + var thresh2 = parseFloat(this.get('errorThreshold')) * this.get('FACTOR'); + var color_green = App.healthStatusGreen; + var color_red = App.healthStatusRed; + var color_orange = App.healthStatusOrange; + if (used <= thresh1) { + this.set('palette', new Rickshaw.Color.Palette({ + scheme: [ '#FFFFFF', color_green ].reverse() + })); + return color_green; + } else if (used <= thresh2) { + this.set('palette', new Rickshaw.Color.Palette({ + scheme: [ '#FFFFFF', color_orange ].reverse() + })); + return color_orange; + } else { + this.set('palette', new Rickshaw.Color.Palette({ + scheme: [ '#FFFFFF', color_red ].reverse() + })); + return color_red; + } + }.property('parentView.value', 'warningThreshold', 'errorThreshold'), + + // refresh text and color when data in model changed + refreshSvg: function () { + // remove old svg + var old_svg = $("#" + this.get('id')); + if(old_svg){ + old_svg.remove(); + } + + // draw new svg + this.appendSvg(); + }.observes('parentView.value', 'warningThreshold', 'errorThreshold') + }) +}); \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ambari/blob/d2e2a791/ambari-web/app/views/common/widget/graph_widget_view.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/views/common/widget/graph_widget_view.js b/ambari-web/app/views/common/widget/graph_widget_view.js index c245622..2e4f0d9 100644 --- a/ambari-web/app/views/common/widget/graph_widget_view.js +++ b/ambari-web/app/views/common/widget/graph_widget_view.js @@ -67,14 +67,14 @@ App.GraphWidgetView = App.ChartLinearTimeView.extend(App.WidgetMixin, { drawWidget: function () { if (this.get('isLoaded')) { - this._refreshGraph(this.calculateSeries()) + this._refreshGraph(this.calculateValues()) } }.observes('isLoaded'), /** * calculate series datasets for graph widgets */ - calculateSeries: function () { + calculateValues: function () { var metrics = this.get('metrics'); var seriesData = []; http://git-wip-us.apache.org/repos/asf/ambari/blob/d2e2a791/ambari-web/app/views/common/widget/template_widget_view.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/views/common/widget/template_widget_view.js b/ambari-web/app/views/common/widget/template_widget_view.js index 580b92a..9b9d844 100644 --- a/ambari-web/app/views/common/widget/template_widget_view.js +++ b/ambari-web/app/views/common/widget/template_widget_view.js @@ -43,54 +43,5 @@ App.TemplateWidgetView = Em.View.extend(App.WidgetMixin, { this.set('value', this.get('content.values')[0].computedValue); this.set('title', this.get('content.values')[0].name); } - }.observes('isLoaded'), - - /** - * calculate series datasets for graph widgets - */ - calculateValues: function () { - var metrics = this.get('metrics'); - var displayUnit = this.get('content.properties.display_unit'); - - this.get('content.values').forEach(function (value) { - var computeExpression = this.computeExpression(this.extractExpressions(value), metrics); - value.computedValue = value.value.replace(this.get('EXPRESSION_REGEX'), function (match) { - return (computeExpression[match]) ? computeExpression[match] + displayUnit : Em.I18n.t('common.na'); - }); - }, this); - }, - - /** - * compute expression - * @param expressions - * @param metrics - * @returns {object} - */ - computeExpression: function (expressions, metrics) { - var result = {}; - - expressions.forEach(function (_expression) { - var validExpression = true; - var value = ""; - - //replace values with metrics data - var beforeCompute = _expression.replace(this.get('VALUE_NAME_REGEX'), function (match) { - if (metrics.someProperty('name', match)) { - return metrics.findProperty('name', match).data; - } else { - validExpression = false; - console.warn('Metrics not found to compute expression'); - } - }); - - if (validExpression) { - //check for correct math expression - validExpression = this.get('MATH_EXPRESSION_REGEX').test(beforeCompute); - !validExpression && console.warn('Value is not correct mathematical expression'); - } - - result['${' + _expression + '}'] = (validExpression) ? Number(window.eval(beforeCompute)).toString() : value; - }, this); - return result; - } + }.observes('isLoaded') }); \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ambari/blob/d2e2a791/ambari-web/test/mixins/common/widget_mixin_test.js ---------------------------------------------------------------------- diff --git a/ambari-web/test/mixins/common/widget_mixin_test.js b/ambari-web/test/mixins/common/widget_mixin_test.js index 6b78769..2ee9edd 100644 --- a/ambari-web/test/mixins/common/widget_mixin_test.js +++ b/ambari-web/test/mixins/common/widget_mixin_test.js @@ -179,16 +179,15 @@ describe('App.WidgetMixin', function() { }); }); - describe("#getServiceComponentMetricsSuccessCallback()", function() { + describe("#getServiceComponentMetricsSuccessCallback()", function () { var mixinObject = mixinClass.create(); - - it("", function() { + it("", function () { var data = { metrics: { - "hbase" : { - "ipc" : { - "IPC" : { - "numOpenConnections" : 11.5 + "hbase": { + "ipc": { + "IPC": { + "numOpenConnections": 11.5 } } } @@ -196,11 +195,11 @@ describe('App.WidgetMixin', function() { }; mixinObject.set('content.metrics', [ { - name: 'ipc.IPC.numOpenConnections' + widget_id: 'metrics/hbase/ipc/IPC/numOpenConnections' } ]); - mixinObject.getServiceComponentMetricsSuccessCallback(data, {}, {serviceName: 'hbase'}); - expect(mixinObject.get('metrics').findProperty('name', 'ipc.IPC.numOpenConnections').data).to.equal(11.5); + mixinObject.getServiceComponentMetricsSuccessCallback(data); + expect(mixinObject.get('metrics').findProperty('widget_id', 'metrics/hbase/ipc/IPC/numOpenConnections').data).to.equal(11.5); }); }); @@ -234,5 +233,66 @@ describe('App.WidgetMixin', function() { }); }); + describe("#calculateValues()", function() { + var mixinObject = mixinClass.create(); + + beforeEach(function () { + sinon.stub(mixinObject, 'extractExpressions'); + this.mock = sinon.stub(mixinObject, 'computeExpression'); + }); + afterEach(function () { + mixinObject.extractExpressions.restore(); + this.mock.restore(); + }); + it("value compute correctly", function() { + this.mock.returns({'${a}': 1}); + mixinObject.set('content.values', [{ + value: '${a}' + }]); + mixinObject.calculateValues(); + expect(mixinObject.get('content.values')[0].computedValue).to.equal('1'); + }); + it("value not available", function() { + this.mock.returns({}); + mixinObject.set('content.values', [{ + value: '${a}' + }]); + mixinObject.calculateValues(); + expect(mixinObject.get('content.values')[0].computedValue).to.equal(Em.I18n.t('common.na')); + }); + }); + + describe("#computeExpression()", function() { + var mixinObject = mixinClass.create(); + + it("expression missing metrics", function() { + var expressions = ['e.m1']; + var metrics = []; + expect(mixinObject.computeExpression(expressions, metrics)).to.eql({ + "${e.m1}": "" + }); + }); + it("Value is not correct mathematical expression", function() { + var expressions = ['e.m1']; + var metrics = [{ + name: 'e.m1', + data: 'a+1' + }]; + expect(mixinObject.computeExpression(expressions, metrics)).to.eql({ + "${e.m1}": "" + }); + }); + it("correct expression", function() { + var expressions = ['e.m1+e.m1']; + var metrics = [{ + name: 'e.m1', + data: 1 + }]; + expect(mixinObject.computeExpression(expressions, metrics)).to.eql({ + "${e.m1+e.m1}": "2" + }); + }); + }); + });
