AMBARI-14993. Log Search: Add Host Log Metrics to Host Details -> Summary (alexantonenko)
Project: http://git-wip-us.apache.org/repos/asf/ambari/repo Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/96bdecf8 Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/96bdecf8 Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/96bdecf8 Branch: refs/heads/branch-dev-patch-upgrade Commit: 96bdecf8474064465b2ecc1261152d9ba68730e2 Parents: 209ec33 Author: Alex Antonenko <hiv...@gmail.com> Authored: Wed Feb 10 16:31:52 2016 +0200 Committer: Alex Antonenko <hiv...@gmail.com> Committed: Thu Feb 11 11:48:18 2016 +0200 ---------------------------------------------------------------------- ambari-web/app/messages.js | 1 + ambari-web/app/routes/main.js | 5 +- .../app/templates/main/host/log_metrics.hbs | 26 ++++ ambari-web/app/templates/main/host/summary.hbs | 24 +++- ambari-web/app/utils/ember_reopen.js | 46 ++++++ ambari-web/app/views.js | 1 + ambari-web/app/views/common/chart/pie.js | 11 +- ambari-web/app/views/main/host/log_metrics.js | 141 +++++++++++++++++++ ambari-web/app/views/main/host/logs_view.js | 12 ++ ambari-web/test/utils/ember_reopen_test.js | 57 ++++++++ 10 files changed, 311 insertions(+), 13 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/ambari/blob/96bdecf8/ambari-web/app/messages.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/messages.js b/ambari-web/app/messages.js index 00fb4b9..59877a5 100644 --- a/ambari-web/app/messages.js +++ b/ambari-web/app/messages.js @@ -2307,6 +2307,7 @@ Em.I18n.translations = { 'hosts.host.summary.hostname':'Hostname', 'hosts.host.summary.agentHeartbeat':'Heartbeat', 'hosts.host.summary.hostMetrics':'Host Metrics', + 'hosts.host.summary.hostLogMetrics':'Host Log Metrics', 'hosts.host.summary.addComponent':'Add Component', 'hosts.host.summary.currentVersion':'Current Version', http://git-wip-us.apache.org/repos/asf/ambari/blob/96bdecf8/ambari-web/app/routes/main.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/routes/main.js b/ambari-web/app/routes/main.js index 891bf34..419f845 100644 --- a/ambari-web/app/routes/main.js +++ b/ambari-web/app/routes/main.js @@ -265,13 +265,16 @@ module.exports = Em.Route.extend(App.RouterRedirections, { }), logs: Em.Route.extend({ - route: '/logs', + route: '/logs:query', connectOutlets: function (router, context) { if (App.get('supports.logSearch')) { router.get('mainHostDetailsController').connectOutlet('mainHostLogs') } else { router.transitionTo('summary'); } + }, + serialize: function(router, params) { + return this.serializeQueryParams(router, params, 'mainHostDetailsController'); } }), http://git-wip-us.apache.org/repos/asf/ambari/blob/96bdecf8/ambari-web/app/templates/main/host/log_metrics.hbs ---------------------------------------------------------------------- diff --git a/ambari-web/app/templates/main/host/log_metrics.hbs b/ambari-web/app/templates/main/host/log_metrics.hbs new file mode 100644 index 0000000..22a39be --- /dev/null +++ b/ambari-web/app/templates/main/host/log_metrics.hbs @@ -0,0 +1,26 @@ +{{! +* 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="row-fluid log-metrics-charts mtl"> + {{#each item in view.logsData}} + <div class="span6 text-center mtl"> + {{view view.chartView contentBinding="item"}} + <a href="#" {{action transitionByService item target="view"}}>{{item.service.displayName}}</a> + </div> + {{/each}} +</div> http://git-wip-us.apache.org/repos/asf/ambari/blob/96bdecf8/ambari-web/app/templates/main/host/summary.hbs ---------------------------------------------------------------------- diff --git a/ambari-web/app/templates/main/host/summary.hbs b/ambari-web/app/templates/main/host/summary.hbs index 6b4c7a5..17a0b69 100644 --- a/ambari-web/app/templates/main/host/summary.hbs +++ b/ambari-web/app/templates/main/host/summary.hbs @@ -168,19 +168,31 @@ </div> </div> </div> - {{!metrics}} - {{#unless view.isNoHostMetricsService}} - <div class="span6"> + <div class="span6"> + {{!metrics}} + {{#unless view.isNoHostMetricsService}} <div class="box"> <div class="box-header"> <h4>{{t hosts.host.summary.hostMetrics}}</h4> {{view view.timeRangeListView}} </div> <div> - {{view App.MainHostMetricsView contentBinding="view.content"}} + {{view App.MainHostMetricsView contentBinding="view.content"}} </div> </div> - </div> + {{/unless}} + + {{!logs metrics}} + {{#if App.supports.logSearch}} + <div class="box"> + <div class="box-header"> + <h4>{{t hosts.host.summary.hostLogMetrics}}</h4> + </div> + <div> + {{view App.MainHostLogMetrics contentBinding="view.content"}} + </div> + </div> + {{/if}} </div> - {{/unless}} + </div> </div> http://git-wip-us.apache.org/repos/asf/ambari/blob/96bdecf8/ambari-web/app/utils/ember_reopen.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/utils/ember_reopen.js b/ambari-web/app/utils/ember_reopen.js index 512b3da..bf091da 100644 --- a/ambari-web/app/utils/ember_reopen.js +++ b/ambari-web/app/utils/ember_reopen.js @@ -242,6 +242,23 @@ Ember.TextArea.reopen({ attributeBindings: ['readonly'] }); +/** + * Simply converts query string to object. + * + * @param {string} queryString query string e.g. '?param1=value1¶m2=value2' + * @return {object} converted object + */ +function parseQueryParams(queryString) { + if (!queryString) { + return {}; + } + return queryString.replace(/^\?/, '').split('&').map(decodeURIComponent) + .reduce(function(p, c) { + var keyVal = c.split('='); + p[keyVal[0]] = keyVal[1]; + return p; + }, {}); +}; Ember.Route.reopen({ /** @@ -257,6 +274,35 @@ Ember.Route.reopen({ */ exitRoute: function (router, context, callback) { callback(); + }, + + /** + * Query Params serializer. This method should be used inside <code>serialize</code> method. + * You need to specify `:query` dynamic sygment in your route's <code>route</code> attribute + * e.g. Em.Route.extend({ route: '/login:query'}) and return result of this method. + * This method will set <code>serializedQuery</code> property to specified controller by name. + * For concrete example see `app/routes/main.js`. + * + * @example + * queryParams: Em.Route.extend({ + * route: '/queryDemo:query', + * serialize: function(route, params) { + * return this.serializeQueryParams(route, params, 'controllerNameToSetQueryObject'); + * } + * }); + * // now when navigated to http://example.com/#/queryDemo?param1=value1¶m2=value2 + * // App.router.get('controllerNameToSetQueryObject').get('serializedQuery') + * // will return { param1: 'value1', param2: 'value2' } + * + * @param {Em.Router} router router instance passed to <code>serialize</code> method + * @param {object} params dynamic segment passed to <code>seriazlie</code> + * @param {string} controllerName name of the controller to set `serializedQuery` as result + * @return {object} + */ + serializeQueryParams: function(router, params, controllerName) { + var controller = router.get(controllerName); + controller.set('serializedQuery', parseQueryParams(params ? params.query : '')); + return params || { query: ''}; } }); http://git-wip-us.apache.org/repos/asf/ambari/blob/96bdecf8/ambari-web/app/views.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/views.js b/ambari-web/app/views.js index a4008f7..2440086 100644 --- a/ambari-web/app/views.js +++ b/ambari-web/app/views.js @@ -128,6 +128,7 @@ require('views/main/host/summary'); require('views/main/host/configs'); require('views/main/host/configs_service'); require('views/main/host/configs_service_menu'); +require('views/main/host/log_metrics'); require('views/main/host/metrics'); require('views/main/host/stack_versions_view'); require('views/main/host/add_view'); http://git-wip-us.apache.org/repos/asf/ambari/blob/96bdecf8/ambari-web/app/views/common/chart/pie.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/views/common/chart/pie.js b/ambari-web/app/views/common/chart/pie.js index 0280f87..ce9bda4 100644 --- a/ambari-web/app/views/common/chart/pie.js +++ b/ambari-web/app/views/common/chart/pie.js @@ -82,16 +82,15 @@ App.ChartPieView = Em.View.extend({ .append("svg:g") .attr("transform", "translate(" + thisChart.get('w') / 2 + "," + thisChart.get('h') / 2 + ")")); - this.set('arcs', thisChart.get('svg').selectAll("path") + this.set('arcs', thisChart.get('svg').selectAll(".arc") .data(thisChart.donut(thisChart.get('data'))) - .enter().append("svg:path") + .enter() + .append("svg:g").attr('class', 'arc') + .append('svg:path') .attr("fill", function (d, i) { return thisChart.palette.color(i); }) .attr("d", thisChart.get('arc')) - ); - } - -}); \ No newline at end of file +}); http://git-wip-us.apache.org/repos/asf/ambari/blob/96bdecf8/ambari-web/app/views/main/host/log_metrics.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/views/main/host/log_metrics.js b/ambari-web/app/views/main/host/log_metrics.js new file mode 100644 index 0000000..20f5ec6 --- /dev/null +++ b/ambari-web/app/views/main/host/log_metrics.js @@ -0,0 +1,141 @@ +/** + * 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'); + +/** + * @typedef {Ember.Object} LogLevelItemObject + * @property {string} level level name + * @property {number} counter + */ +/** + * @typedef {Object} ServiceLogMetricsObject + * @property {App.Service} service model instance + * @property {LogLevelItemObject[]} logs + */ +App.MainHostLogMetrics = Em.View.extend({ + templateName: require('templates/main/host/log_metrics'), + classNames: ['host-log-metrics'], + + /** + * @type {ServiceLogMetricsObject[]} + */ + logsData: function() { + var services = this.get('content').get('hostComponents').mapProperty('service').uniq(); + var logLevels = ['fatal', 'critical', 'error', 'warning', 'info', 'debug']; + return services.map(function(service) { + var levels = logLevels.map(function(level) { + return Em.Object.create({ + level: level, + counter: Math.ceil(Math.random()*10) + }); + }); + return Em.Object.create({ + service: service, + logs: levels + }); + }); + }.property('content'), + + /** + * @type {Ember.View} Pie Chart view + * @extends App.PieChartView + */ + chartView: App.ChartPieView.extend({ + classNames: ['log-metrics-chart'], + w: 150, + h: 150, + stroke: '#fff', + strokeWidth: 1, + levelColors: { + FATAL: '#B10202', + CRITICAL: '#E00505', + ERROR: App.healthStatusRed, + INFO: App.healthStatusGreen, + WARNING: App.healthStatusOrange, + DEBUG: '#1e61f7' + }, + innerR: 36, + donut: d3.layout.pie().sort(null).value(function(d) { return d.get('counter'); }), + + prepareChartData: function(content) { + this.set('data', content.get('logs')); + }, + + didInsertElement: function() { + this.prepareChartData(this.get('content')); + this._super(); + this.appendLabels(); + this.formatCenterText(); + this.attachArcEvents(); + this.colorizeArcs(); + }, + + attachArcEvents: function() { + var self = this; + this.get('svg').selectAll('.arc') + .on('mouseover', function(d) { + self.get('svg').select('g.center-text').select('text') + .text(d.data.get('level').capitalize() + ": " + d.data.get('counter')); + }) + .on('mouseout', function() { + self.get('svg').select('g.center-text').select('text').text(''); + }); + }, + + formatCenterText: function() { + this.get('svg') + .append('svg:g') + .attr('class', 'center-text') + .attr('render-order', 1) + .append('svg:text') + .attr('transform', "translate(0,0)") + .attr('text-anchor', 'middle') + .attr('stroke', '#000') + .attr('stroke-width', 0) + }, + + appendLabels: function() { + var labelArc = d3.svg.arc() + .outerRadius(this.get('outerR') - 15) + .innerRadius(this.get('outerR') - 15); + this.get('svg').selectAll('.arc') + .append('text') + .attr('transform', function(d) { return "translate(" + labelArc.centroid(d) + ")"; }) + .attr('stroke', '#000') + .attr('stroke-width', 0) + .attr('font-size', '12px') + .attr('dy', '.50em') + .text(function(d) { return d.data.get('counter'); }); + }, + + colorizeArcs: function() { + var self = this; + this.get('svg').selectAll('.arc path') + .attr('fill', function(d) { + return self.get('levelColors')[d.data.get('level').toUpperCase()]; + }); + } + }), + + + transitionByService: function(e) { + var service = e.context; + App.router.transitionTo('logs', {query: '?service_name=' + service.get('service.serviceName')}); + } +}); http://git-wip-us.apache.org/repos/asf/ambari/blob/96bdecf8/ambari-web/app/views/main/host/logs_view.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/views/main/host/logs_view.js b/ambari-web/app/views/main/host/logs_view.js index dfe00e4..b51f955 100644 --- a/ambari-web/app/views/main/host/logs_view.js +++ b/ambari-web/app/views/main/host/logs_view.js @@ -67,6 +67,10 @@ App.MainHostLogsView = App.TableView.extend({ serviceNameFilterView: filters.createSelectView({ column: 1, fieldType: 'filter-input-width', + didInsertElement: function() { + this.setValue(Em.getWithDefault(this, 'controller.serializedQuery.service_name', '')); + this._super(); + }, content: function() { return [{ value: '', @@ -86,6 +90,10 @@ App.MainHostLogsView = App.TableView.extend({ componentNameFilterView: filters.createSelectView({ column: 2, fieldType: 'filter-input-width', + didInsertElement: function() { + this.setValue(Em.getWithDefault(this, 'controller.serializedQuery.component_name', '')); + this._super(); + }, content: function() { var hostName = this.get('parentView').get('host.hostName'), hostComponents = App.HostComponent.find().filterProperty('hostName', hostName), @@ -108,6 +116,10 @@ App.MainHostLogsView = App.TableView.extend({ fileExtensionsFilter: filters.createSelectView({ column: 3, fieldType: 'filter-input-width', + didInsertElement: function() { + this.setValue(Em.getWithDefault(this, 'controller.serializedQuery.file_extension', '')); + this._super(); + }, content: function() { return [{ value: '', http://git-wip-us.apache.org/repos/asf/ambari/blob/96bdecf8/ambari-web/test/utils/ember_reopen_test.js ---------------------------------------------------------------------- diff --git a/ambari-web/test/utils/ember_reopen_test.js b/ambari-web/test/utils/ember_reopen_test.js index eda5e81..aa50a50 100644 --- a/ambari-web/test/utils/ember_reopen_test.js +++ b/ambari-web/test/utils/ember_reopen_test.js @@ -78,4 +78,61 @@ describe('Ember functionality extension', function () { }); + describe('#Em.Route', function() { + describe('#serializeQueryParams', function() { + var route, + cases = [ + { + m: 'No query params', + params: undefined, + e: { + result: {query: ''}, + serializedQuery: {} + } + }, + { + m: 'Query params ?param1=value1¶m2=value2', + params: { query: '?param1=value1¶m2=value2'}, + e: { + result: {query: '?param1=value1¶m2=value2'}, + serializedQuery: {param1: 'value1', param2: 'value2'} + } + }, + { + m: 'Query params with encodedComponent ?param1=value1%30¶m2=value2', + params: { query: '?param1=value1%30¶m2=value2'}, + e: { + result: {query: '?param1=value1%30¶m2=value2'}, + serializedQuery: {param1: 'value10', param2: 'value2'} + } + } + ]; + + beforeEach(function() { + route = Ember.Route.create({ + route: 'demo:query', + serialize: function(router, params) { + return this.serializeQueryParams(router, params, 'testController'); + } + }); + }); + + afterEach(function() { + route.destroy(); + route = null; + }); + + cases.forEach(function(test) { + it(test.m, function() { + var ctrl = Em.Object.create({}); + var router = Em.Object.create({ + testController: ctrl + }); + var ret = route.serialize(router, test.params); + expect(ret).to.be.eql(test.e.result); + expect(ctrl.get('serializedQuery')).to.be.eql(test.e.serializedQuery); + }); + }); + }); + }); });