Repository: ambari Updated Branches: refs/heads/trunk f9ee4a0b8 -> 7cfcf6579
AMBARI-16151. Finalize LogSearch integration (alexantonenko) Project: http://git-wip-us.apache.org/repos/asf/ambari/repo Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/7cfcf657 Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/7cfcf657 Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/7cfcf657 Branch: refs/heads/trunk Commit: 7cfcf657951be5ca824e37ecb2f3c7ed54ca8a56 Parents: f3cb35c Author: Alex Antonenko <[email protected]> Authored: Thu Apr 28 14:22:19 2016 +0300 Committer: Alex Antonenko <[email protected]> Committed: Thu Apr 28 19:53:48 2016 +0300 ---------------------------------------------------------------------- ambari-web/app/config.js | 5 +- .../app/controllers/global/update_controller.js | 4 + ambari-web/app/mappers/hosts_mapper.js | 20 ++ ambari-web/app/messages.js | 5 + .../app/mixins/common/infinite_scroll_mixin.js | 16 +- ambari-web/app/models.js | 3 +- ambari-web/app/models/host_component.js | 1 + ambari-web/app/models/host_component_log.js | 29 ++ ambari-web/app/styles/application.less | 14 +- ambari-web/app/styles/common.less | 33 +++ ambari-web/app/styles/log_file_search.less | 75 +++++ ambari-web/app/styles/modal_popups.less | 104 ++++++- .../templates/common/host_progress_popup.hbs | 92 ++++--- ambari-web/app/templates/common/log_tail.hbs | 34 +++ ambari-web/app/templates/common/modal_popup.hbs | 2 +- .../common/modal_popups/log_tail_popup.hbs | 49 ++++ ambari-web/app/templates/main/host/logs.hbs | 22 +- ambari-web/app/templates/main/host/summary.hbs | 2 +- ambari-web/app/utils/ajax/ajax.js | 16 ++ ambari-web/app/utils/file_utils.js | 4 + ambari-web/app/utils/host_progress_popup.js | 3 +- ambari-web/app/views.js | 2 + ambari-web/app/views/common/filter_view.js | 4 + .../common/host_progress_popup_body_view.js | 272 ++++++++++++++++++- ambari-web/app/views/common/log_tail_view.js | 232 ++++++++++++++++ ambari-web/app/views/common/modal_popup.js | 12 +- .../views/common/modal_popups/log_tail_popup.js | 115 ++++++++ ambari-web/app/views/main/host/logs_view.js | 85 ++++-- ambari-web/app/views/main/host/menu.js | 7 +- ambari-web/test/views/main/host/menu_test.js | 19 +- 30 files changed, 1193 insertions(+), 88 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/ambari/blob/7cfcf657/ambari-web/app/config.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/config.js b/ambari-web/app/config.js index 98aa380..fe970b1 100644 --- a/ambari-web/app/config.js +++ b/ambari-web/app/config.js @@ -79,11 +79,12 @@ App.supports = { preInstallChecks: false, hostComboSearchBox: true, serviceAutoStart: false, - logSearch: false, + logSearch: true, redhatSatellite: false, enableIpa: false, addingNewRepository: false, - kerberosStackAdvisor: true + kerberosStackAdvisor: true, + logCountVizualization: false }; if (App.enableExperimental) { http://git-wip-us.apache.org/repos/asf/ambari/blob/7cfcf657/ambari-web/app/controllers/global/update_controller.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/controllers/global/update_controller.js b/ambari-web/app/controllers/global/update_controller.js index 77341b1..223376b 100644 --- a/ambari-web/app/controllers/global/update_controller.js +++ b/ambari-web/app/controllers/global/update_controller.js @@ -231,6 +231,7 @@ App.UpdateController = Em.Controller.extend({ 'stack_versions/repository_versions/RepositoryVersions/display_name', mainHostController = App.router.get('mainHostController'), sortProperties = mainHostController.getSortProps(), + loggingResource = ',host_components/logging', isHostsLoaded = false; this.get('queryParams').set('Hosts', mainHostController.getQueryParameters(true)); if (App.router.get('currentState.parentState.name') === 'hosts') { @@ -264,6 +265,9 @@ App.UpdateController = Em.Controller.extend({ realUrl = realUrl.replace("<stackVersions>", stackVersionInfo); realUrl = realUrl.replace("<metrics>", lazyLoadMetrics ? "" : "metrics/disk,metrics/load/load_one,"); realUrl = realUrl.replace('<hostDetailsParams>', hostDetailsParams); + if (App.get('supports.logSearch')) { + realUrl += loggingResource; + } var clientCallback = function (skipCall, queryParams) { var completeCallback = function () { http://git-wip-us.apache.org/repos/asf/ambari/blob/7cfcf657/ambari-web/app/mappers/hosts_mapper.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/mappers/hosts_mapper.js b/ambari-web/app/mappers/hosts_mapper.js index 10a9e61..919ce77 100644 --- a/ambari-web/app/mappers/hosts_mapper.js +++ b/ambari-web/app/mappers/hosts_mapper.js @@ -79,6 +79,16 @@ App.hostsMapper = App.QuickDataMapper.create({ host_id: 'host_name', is_visible: 'is_visible' }, + hostComponentLogsConfig: { + name: 'logging.name', + service_name: 'HostRoles.service_name', + host_name: 'HostRoles.host_name', + log_file_names_type: 'array', + log_file_names_key: 'logging.logs', + log_file_names: { + item: 'name' + } + }, map: function (json, returnMapped) { returnMapped = !!returnMapped; console.time('App.hostsMapper execution time'); @@ -94,6 +104,7 @@ App.hostsMapper = App.QuickDataMapper.create({ var selectedHosts = App.db.getSelectedHosts('mainHostController'); var clusterName = App.get('clusterName'); var advancedHostComponents = []; + var hostComponentLogs = []; // Create a map for quick access on existing hosts var hosts = App.Host.find().toArray(); @@ -143,6 +154,13 @@ App.hostsMapper = App.QuickDataMapper.create({ if (component.passive_state !== 'OFF') { componentsInPassiveState.push(id); } + if (host_component.hasOwnProperty('logging')) { + var logParsed = this.parseIt(host_component, this.hostComponentLogsConfig); + logParsed.id = logParsed.host_name + '_' + logParsed.name; + logParsed.host_component_id = host_component.id; + component.component_logs_id = logParsed.id; + hostComponentLogs.push(logParsed); + } } var currentVersion = item.stack_versions.findProperty('HostStackVersions.state', 'CURRENT'); @@ -195,6 +213,7 @@ App.hostsMapper = App.QuickDataMapper.create({ App.store.commit(); App.store.loadMany(App.HostStackVersion, stackVersions); + App.store.loadMany(App.HostComponentLog, hostComponentLogs); App.store.loadMany(App.HostComponent, components); //"itemTotal" present only for Hosts page request if (!Em.isNone(json.itemTotal)) { @@ -212,6 +231,7 @@ App.hostsMapper = App.QuickDataMapper.create({ }, /** + * set metric fields of hosts * @param {object} data */ http://git-wip-us.apache.org/repos/asf/ambari/blob/7cfcf657/ambari-web/app/messages.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/messages.js b/ambari-web/app/messages.js index eea0e2a..a1ed7be 100644 --- a/ambari-web/app/messages.js +++ b/ambari-web/app/messages.js @@ -97,6 +97,7 @@ Em.I18n.translations = { 'common.progress':'Progress', 'common.status':'Status', 'common.action':'Action', + 'common.refresh':'Refresh', 'common.remove':'Remove', 'common.retry':'Retry', 'common.skip':'Skip', @@ -144,6 +145,7 @@ Em.I18n.translations = { 'common.disableAll':'Disable All', 'common.disk':'Disk', 'common.diskUsage':'Disk Usage', + 'common.last':'Last', 'common.loadAvg':'Load Avg', 'common.components':'Components', 'common.component':'Component', @@ -281,6 +283,7 @@ Em.I18n.translations = { 'common.stderr': "stderr", 'common.structuredOut': "structured_out", 'common.fileName': 'File Name', + 'common.file': 'File', 'common.days': "Days", 'common.hours': "Hours", 'common.minutes': "Minutes", @@ -446,6 +449,8 @@ Em.I18n.translations = { 'popup.jdkValidation.header': 'Unsupported JDK', 'popup.jdkValidation.body': 'The {0} Stack requires JDK {1} but Ambari is configured for JDK {2}. This could result in error or problems with running your cluster.', + 'popup.logTail.header': 'File Name', + 'popup.logTail.openInLogSearch': 'Open In Log Search', 'login.header':'Sign in', 'login.message.title':'Login Message', http://git-wip-us.apache.org/repos/asf/ambari/blob/7cfcf657/ambari-web/app/mixins/common/infinite_scroll_mixin.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/mixins/common/infinite_scroll_mixin.js b/ambari-web/app/mixins/common/infinite_scroll_mixin.js index 70c424e..a42b41b 100644 --- a/ambari-web/app/mixins/common/infinite_scroll_mixin.js +++ b/ambari-web/app/mixins/common/infinite_scroll_mixin.js @@ -74,6 +74,12 @@ App.InfiniteScrollMixin = Ember.Mixin.create({ }, /** + * Determines that there is no data to load on next callback call. + * + */ + _infiniteScrollMoreData: true, + + /** * Initialize infinite scroll on specified HTMLElement. * * @param {HTMLElement} el DOM element to attach infinite scroll. @@ -109,7 +115,7 @@ App.InfiniteScrollMixin = Ember.Mixin.create({ _infiniteScrollEndHandler: function(options) { return function(e) { var self = this; - if (this.get('_infiniteScrollCallbackInProgress')) return; + if (this.get('_infiniteScrollCallbackInProgress') || !this.get('_infiniteScrollMoreData')) return; this._infiniteScrollAppendHtml(options.appendHtml); // always scroll to bottom this.get('_infiniteScrollEl').scrollTop(this.get('_infiniteScrollEl').get(0).scrollHeight); @@ -169,5 +175,13 @@ App.InfiniteScrollMixin = Ember.Mixin.create({ this.get('_infiniteScrollEl').off('scroll', this._infiniteScrollHandler); this.get('_infiniteScrollEl').off('infinite-scroll-end', this._infiniteScrollHandler); this.set('_infiniteScrollEl', null); + }, + + /** + * Set if there is more data to load on next scroll end event. + * @param {boolean} isAvailable <code>true</code> when there are more data to fetch + */ + infiniteScrollSetDataAvailable: function(isAvailable) { + this.set('_infiniteScrollMoreData', isAvailable); } }); http://git-wip-us.apache.org/repos/asf/ambari/blob/7cfcf657/ambari-web/app/models.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/models.js b/ambari-web/app/models.js index 1112b0c..575c0b0 100644 --- a/ambari-web/app/models.js +++ b/ambari-web/app/models.js @@ -56,6 +56,7 @@ require('models/rack'); require('models/background_operation'); require('models/client_component'); require('models/host_component'); +require('models/host_component_log'); require('models/target_cluster'); require('models/slave_component'); require('models/master_component'); @@ -76,4 +77,4 @@ require('models/configs/objects/service_config_category'); require('models/configs/objects/service_config_property'); require('models/widget'); require('models/widget_property'); -require('models/widget_layout'); \ No newline at end of file +require('models/widget_layout'); http://git-wip-us.apache.org/repos/asf/ambari/blob/7cfcf657/ambari-web/app/models/host_component.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/models/host_component.js b/ambari-web/app/models/host_component.js index f10947f..569879f 100644 --- a/ambari-web/app/models/host_component.js +++ b/ambari-web/app/models/host_component.js @@ -27,6 +27,7 @@ App.HostComponent = DS.Model.extend({ displayNameAdvanced: DS.attr('string'), staleConfigs: DS.attr('boolean'), host: DS.belongsTo('App.Host'), + componentLogs: DS.belongsTo('App.HostComponentLog'), hostName: DS.attr('string'), service: DS.belongsTo('App.Service'), adminState: DS.attr('string'), http://git-wip-us.apache.org/repos/asf/ambari/blob/7cfcf657/ambari-web/app/models/host_component_log.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/models/host_component_log.js b/ambari-web/app/models/host_component_log.js new file mode 100644 index 0000000..83eccb3 --- /dev/null +++ b/ambari-web/app/models/host_component_log.js @@ -0,0 +1,29 @@ +/** + * 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.HostComponentLog = DS.Model.extend({ + name: DS.attr('string'), + hostName: DS.attr('string'), + serviceName: DS.attr('string'), + hostComponent: DS.belongsTo('App.HostComponent'), + logFileNames: DS.attr('array') +}); + +App.HostComponentLog.FIXTURES = []; http://git-wip-us.apache.org/repos/asf/ambari/blob/7cfcf657/ambari-web/app/styles/application.less ---------------------------------------------------------------------- diff --git a/ambari-web/app/styles/application.less b/ambari-web/app/styles/application.less index 8a34462..b6514b3 100644 --- a/ambari-web/app/styles/application.less +++ b/ambari-web/app/styles/application.less @@ -1599,10 +1599,11 @@ a:focus { font-size: 16px; } } + #host-info, #service-info{ overflow: auto; - max-height: 340px; width: 100%; + max-height: 340px; &.scheduled{ max-height: 255px; } @@ -3732,6 +3733,11 @@ table.graphs { background: none repeat scroll 0 0 #F8F8F8; } } + .logs-tab-content { + a.external-link { + font-size: @smaller-font-size; + } + } .host-stack-version-status { .label { font-size: 14px; @@ -3740,6 +3746,12 @@ table.graphs { td.align-center { text-align: center; } + + .logs-tab-content { + .table { + table-layout: auto; + } + } } .services-menu { http://git-wip-us.apache.org/repos/asf/ambari/blob/7cfcf657/ambari-web/app/styles/common.less ---------------------------------------------------------------------- diff --git a/ambari-web/app/styles/common.less b/ambari-web/app/styles/common.less index b54cd06..326d404 100644 --- a/ambari-web/app/styles/common.less +++ b/ambari-web/app/styles/common.less @@ -167,6 +167,16 @@ @default-font-size: 14px; @smaller-font-size: 12px; +/************************************************************************ +* Modal popup properties +***********************************************************************/ +// modal body content padding +@modal-body-padding: 15px; +// modal header height +@modal-header-height: 50px; +// modal footer height +@modal-footer-height: 60px; + .editable-list-container.well{ padding: 10px; position: relative; @@ -381,4 +391,27 @@ .lh-btn { line-height: 30px; +} + +.text-bold { + font-weight: bold; +} + +.pre-styled { + display: block; + padding: 9.5px; + margin: 0 0 10px; + font-size: 11px; + line-height: 14px; + font-family: monospace; + word-break: break-all; + word-wrap: break-word; + white-space: pre; + white-space: pre-wrap; + background-color: #f5f5f5; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.15); + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; } \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ambari/blob/7cfcf657/ambari-web/app/styles/log_file_search.less ---------------------------------------------------------------------- diff --git a/ambari-web/app/styles/log_file_search.less b/ambari-web/app/styles/log_file_search.less index aeeea3f..cf5ef29 100644 --- a/ambari-web/app/styles/log_file_search.less +++ b/ambari-web/app/styles/log_file_search.less @@ -17,6 +17,8 @@ */ +@import 'common.less'; + @toolbar-context-menu-width: 40px; @toolbar-padding: 10px; @@ -153,3 +155,76 @@ } } } + +.log-tail-popup.full-height-modal { + @log-tail-header-height: 60px; + @log-tail-bottom-wrap-height: 50px; + @log-tail-content-height: calc(~"100%" - (@log-tail-header-height + @log-tail-bottom-wrap-height + @modal-footer-height)); + + .top-wrap { + border-bottom: none !important; + + .modal-label { + font-size: 20px; + line-height: 20px; + } + .refresh, + .open-in-log-search { + font-size: 24px; + cursor: pointer; + margin-right: 12px; + + i { + font-size: 20px; + vertical-align: middle; + } + + span { + font-size: 14px; + } + } + .action-bar { + + & > a { + margin: 0 5px; + + i { + font-size: 20px; + } + } + } + } + + .bottom-wrap { + select { + width: 80px; + } + } + + .log-tail-content { + width: 100%; + overflow-y: auto; + border: 1px solid #ddd; + white-space: normal; + box-sizing: border-box; + + & > div { + margin: 0; + padding: 0; + + &:hover { + background: #ccc; + } + } + + #infinite-scroll-append, + .log-tail-spinner-container + { + text-align: center; + + .icon-spinner { + font-size: 24px; + } + } + } +} http://git-wip-us.apache.org/repos/asf/ambari/blob/7cfcf657/ambari-web/app/styles/modal_popups.less ---------------------------------------------------------------------- diff --git a/ambari-web/app/styles/modal_popups.less b/ambari-web/app/styles/modal_popups.less index 07d2635..5e89dbc 100644 --- a/ambari-web/app/styles/modal_popups.less +++ b/ambari-web/app/styles/modal_popups.less @@ -16,6 +16,7 @@ * limitations under the License. */ @import 'common.less'; + /*90% width modal window start*/ .full-width-modal { .modal { @@ -107,7 +108,7 @@ td .table-striped tbody tr:nth-child(odd) td, tr:nth-child(even) th { - background-color: none; + background-color: transparent; } } @@ -247,7 +248,7 @@ margin-left: 22px; float: left; .checkbox { - margin: 0px; + margin: 0; } } } @@ -505,13 +506,39 @@ td .table-striped tbody tr:nth-child(odd) td, tr:nth-child(even) th { - background-color: none; + background-color: transparent; } } /*60% width modal window end*/ +/* modal fill screen popup */ + + +.full-height-modal { + // padding from the top and bottom for full-height popup of window + @modal-padding: 40px; + + .modal { + max-height: 90%; + + &.no-footer { + .modal-body { + // height: 100%; + } + } + + &.with-footer { + .modal-body { + max-height: calc(~"100%" - (@modal-body-padding*2 + @modal-footer-height + @modal-header-height)); + } + } + } +} + +/* modal fill screen popup end */ + #logs-popup { .controls-block { margin-bottom: 10px; @@ -520,4 +547,75 @@ cursor: pointer; } } +} + +.modal { + .modal-body { + .top-wrap { + &.top-wrap-header { + border-bottom: 1px solid #eee; + margin-bottom: 20px; + } + } + } +} + + +.host-progress-popup { + .task-detail-info { + + .task-detail-log-info { + padding-top: 10px; + } + + &.task-detail-info-tabbed { + + .task-detail-log-info { + padding-top: 0; + } + + .task-top-wrap { + padding: 0; + border-bottom: none; + } + + + .task-detail-nav { + padding-top: 10px; + } + + .log-tail-content { + width: 100%; + padding: 15px; + overflow-y: auto; + box-sizing: border-box; + white-space: normal; + margin: 0; + position: relative; + + & > div { + margin: 0; + padding: 0; + + &:hover { + background: #ccc; + } + } + + .log-tail-spinner-container { + position: absolute; + top: 0; + left: 3px; + } + } + + #infinite-scroll-append { + text-align: center; + + .icon-spinner { + font-size: 24px; + } + } + } + } } \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ambari/blob/7cfcf657/ambari-web/app/templates/common/host_progress_popup.hbs ---------------------------------------------------------------------- diff --git a/ambari-web/app/templates/common/host_progress_popup.hbs b/ambari-web/app/templates/common/host_progress_popup.hbs index fe330f7..c457738 100644 --- a/ambari-web/app/templates/common/host_progress_popup.hbs +++ b/ambari-web/app/templates/common/host_progress_popup.hbs @@ -184,57 +184,87 @@ <!-- TASK DETAILS ---> - <div {{bindAttr class="view.parentView.isLogWrapHidden:hidden :task-detail-info"}}> + <div {{bindAttr class="view.parentView.isLogWrapHidden:hidden :task-detail-info view.hostComponentLogsExists:task-detail-info-tabbed"}}> <div class="task-top-wrap"> <a class="task-detail-back" href="javascript:void(null)" {{action backToTaskList}} ><i - class="icon-arrow-left"></i> {{t common.tasks}}</a> + class="icon-arrow-left"></i> {{t common.tasks}}</a> - <div> + <div {{bindAttr class="view.hostComponentLogsExists:task-detail-log-nav-actions"}}> <i {{bindAttr class="view.openedTask.status :task-detail-status-ico view.openedTask.icon"}}></i> <div class="task-detail-ico-wrap"> <a {{translateAttr title="common.fullLogPopup.clickToCopy"}} {{action "textTrigger" taskInfo target="view"}} class="task-detail-copy"><i - class="icon-copy"></i> {{t common.copy}}</a> + class="icon-copy"></i> {{t common.copy}}</a> + <a {{translateAttr title="common.openNewWindow"}} {{action openTaskLogInDialog}} class="task-detail-open-dialog"><i + class="icon-external-link"></i> {{t common.open}}</a> {{#if App.supports.logSearch}} - <a {{action navigateToHostLogs target="view"}} {{bindAttr class="view.isLogsLinkVisible::hidden"}} href="#"> - <i class="icon-file"></i> {{t common.logs}} - </a> + {{#if view.isLogSearchInstalled}} + <a {{action navigateToHostLogs target="view"}} {{bindAttr class="view.isLogsLinkVisible::hidden"}} href="#"> + <i class="icon-file"></i> {{t common.host}} {{t common.logs}} + </a> + {{/if}} {{/if}} - <a {{translateAttr title="common.openNewWindow"}} {{action openTaskLogInDialog}} class="task-detail-open-dialog"><i - class="icon-external-link"></i> {{t common.open}}</a> </div> <span class="task-detail-log-rolename">{{view.openedTask.commandDetail}}</span> </div> + <ul {{bindAttr class="view.hostComponentLogsExists::hide :nav :nav-tabs :task-detail-nav"}}> + <li {{bindAttr class="view.isLevelLoaded:active"}}> + <a href="#" data-target="#task-log-tab" data-toggle="tab" {{action setActiveTaskLogTab target="view"}}>{{t app.name}} stdout/stderr</a> + </li> + {{#each hostLog in view.hostComponentLogs}} + <li> + <a href="#" {{action setActiveLogTab hostLog target="view"}} {{bindAttr data-target="hostLog.tabClassNameSelector"}} data-toggle="tab">{{hostLog.displayedFileName}}</a> + </li> + {{/each}} + </ul> </div> {{#if view.isLevelLoaded}} <div class="task-detail-log-info"> <div class="content-area"> - <div class="task-detail-log-clipboard-wrap"></div> - <div class="task-detail-log-maintext"> - {{#if view.openedTask.isRebalanceHDFSTask }} - <h5>{{t services.hdfs.rebalance.title}}</h5> + <div class="tab-content"> + <div class="task-detail-log-clipboard-wrap"></div> + <div id="task-log-tab" class="tab-pane active"> + <div {{bindAttr class=":task-detail-log-maintext view.isClipBoardActive:hidden"}}> + {{#if view.openedTask.isRebalanceHDFSTask }} + <h5>{{t services.hdfs.rebalance.title}}</h5> - <div class="progresspopup-rebalancehdfs"> - <div {{bindAttr class=":progress view.openedTask.isInProgress:progress-striped view.openedTask.barColor :active"}}> - <div class="bar" {{bindAttr style="view.openedTask.completionProgressStyle"}}></div> - </div> + <div class="progresspopup-rebalancehdfs"> + <div {{bindAttr class=":progress view.openedTask.isInProgress:progress-striped view.openedTask.barColor :active"}}> + <div class="bar" {{bindAttr style="view.openedTask.completionProgressStyle"}}></div> + </div> + </div> + <div class="clearfix"> + <div class="pull-left"> + {{view.openedTask.dataMoved}} moved / + {{view.openedTask.dataLeft}} left / + {{view.openedTask.dataBeingMoved}} being processed + </div> + {{#if view.openedTask.isNotComplete}} + <button class="btn btn-danger pull-right" {{action stopRebalanceHDFS}}>{{t common.cancel}}</button> + {{/if}} + </div> + <hr> + {{/if}} + <p class="text-bold">{{t common.stderr}}: <span class="muted">{{view.openedTask.errorLog}} </span></p> + <pre class="stderr">{{view.openedTask.stderr}}</pre> + <p class="text-bold">{{t common.stdout}}: <span class="muted"> {{view.openedTask.outputLog}} </span></p> + <pre class="stdout">{{view.openedTask.stdout}}</pre> </div> - <div class="clearfix"> - <div class="pull-left"> - {{view.openedTask.dataMoved}} moved / - {{view.openedTask.dataLeft}} left / - {{view.openedTask.dataBeingMoved}} being processed + </div> + {{#each hostLog in view.hostComponentLogs}} + <div {{bindAttr class=":tab-pane :log-component-tab hostLog.tabClassName"}}> + <p {{bindAttr class="view.isClipBoardActive:hidden"}}> + <span class="text-bold">{{t common.file}}: </span> + <span class="text-bold muted">{{hostLog.fileName}}</span> + <a class="pull-right" {{bindAttr href="hostLog.url"}} target="_blank"> + <i class="icon-external-link"></i> + {{t popup.logTail.openInLogSearch}}</a> + </p> + <div {{bindAttr class="view.isClipBoardActive:hidden"}}> + {{view view.logTailView contentBinding="hostLog"}} </div> - {{#if view.openedTask.isNotComplete}} - <button class="btn btn-danger pull-right" {{action stopRebalanceHDFS}}>{{t common.cancel}}</button> - {{/if}} </div> - <hr> - {{/if}} - <h5>{{t common.stderr}}: <span class="muted">{{view.openedTask.errorLog}} </span></h5> - <pre class="stderr">{{view.openedTask.stderr}}</pre> - <h5>{{t common.stdout}}: <span class="muted"> {{view.openedTask.outputLog}} </span></h5> - <pre class="stdout">{{view.openedTask.stdout}}</pre> + {{/each}} </div> </div> </div> http://git-wip-us.apache.org/repos/asf/ambari/blob/7cfcf657/ambari-web/app/templates/common/log_tail.hbs ---------------------------------------------------------------------- diff --git a/ambari-web/app/templates/common/log_tail.hbs b/ambari-web/app/templates/common/log_tail.hbs new file mode 100644 index 0000000..db70e1d --- /dev/null +++ b/ambari-web/app/templates/common/log_tail.hbs @@ -0,0 +1,34 @@ +{{! +* 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="log-tail-wrapper"> + <div class="log-tail-content pre-styled"> + {{#if view.isDataReady}} + {{#if view.oldLogsIsFetching}} + <div class="log-tail-spinner-container text-center"> + <i class="icon-spinner icon-spin"></i> + </div> + {{/if}} + {{#each row in view.logRows}} + <div>{{row.logtimeFormatted}} {{row.level}} {{row.logMessage}}</div> + {{/each}} + {{else}} + <div class="log-tail-spinner-container text-center"><i class="icon-spinner icon-spin"></i></div> + {{/if}} + </div> +</div> http://git-wip-us.apache.org/repos/asf/ambari/blob/7cfcf657/ambari-web/app/templates/common/modal_popup.hbs ---------------------------------------------------------------------- diff --git a/ambari-web/app/templates/common/modal_popup.hbs b/ambari-web/app/templates/common/modal_popup.hbs index 77e37f0..e4537e9 100644 --- a/ambari-web/app/templates/common/modal_popup.hbs +++ b/ambari-web/app/templates/common/modal_popup.hbs @@ -18,7 +18,7 @@ <div class="modal-backdrop"></div> -<div class="modal" id="modal" tabindex="-1" role="dialog" aria-labelledby="modal-label" aria-hidden="true"> +<div {{bindAttr class=":modal view.showFooter:with-footer:no-footer"}} id="modal" tabindex="-1" role="dialog" aria-labelledby="modal-label" aria-hidden="true"> <div class="modal-header"> {{#if view.showCloseButton}} <a class="close" {{action onClose target="view"}}>x</a> http://git-wip-us.apache.org/repos/asf/ambari/blob/7cfcf657/ambari-web/app/templates/common/modal_popups/log_tail_popup.hbs ---------------------------------------------------------------------- diff --git a/ambari-web/app/templates/common/modal_popups/log_tail_popup.hbs b/ambari-web/app/templates/common/modal_popups/log_tail_popup.hbs new file mode 100644 index 0000000..65a5f99 --- /dev/null +++ b/ambari-web/app/templates/common/modal_popups/log_tail_popup.hbs @@ -0,0 +1,49 @@ +{{! +* 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="top-wrap top-wrap-header"> + <div class="log-tail-popup-info"> + <span class="text-bold">{{t common.file}}: </span> + <span class="muted">{{view.content.filePath}}</span> + <div class="pull-right action-bar"> + <a href="#" {{action toggleCopy target="view"}}> + <i class="icon-copy"></i> + {{t common.copy}} + </a> + <a href="#" {{action openInNewTab target="view"}}> + <i class="icon-external-link"></i> + {{t common.open}} + </a> + <a class="open-in-log-search" {{bindAttr href="view.logSearchUrl"}} target="_blank"> + <i class="icon-external-link"></i> + {{t popup.logTail.openInLogSearch}} + </a> + </div> + </div> + <div class="clearfix"></div> +</div> +<div class="modal-content"> + <div {{bindAttr class="view.isCopyActive::hidden"}}> + <div class="clipboard-wrap"> + {{view Em.TextArea valueBinding="view.copyContent" classNames="copy-textarea"}} + </div> + </div> + <div {{bindAttr class="view.isCopyActive:hidden"}}> + {{view view.logTailContentView}} + </div> +</div> http://git-wip-us.apache.org/repos/asf/ambari/blob/7cfcf657/ambari-web/app/templates/main/host/logs.hbs ---------------------------------------------------------------------- diff --git a/ambari-web/app/templates/main/host/logs.hbs b/ambari-web/app/templates/main/host/logs.hbs index 000b4aa..9728417 100644 --- a/ambari-web/app/templates/main/host/logs.hbs +++ b/ambari-web/app/templates/main/host/logs.hbs @@ -32,13 +32,21 @@ <tbody> {{#if view.pageContent}} {{#each row in view.pageContent}} - <tr> - <td>{{row.serviceName}}</td> - <td>{{row.componentName}}</td> - <td> - <a {{action openLogFile row target="view"}} href="#">{{row.fileName}}</a> - </td> - </tr> + {{#view view.logFileRowView contentBinding="row"}} + <td>{{row.serviceDisplayName}}</td> + <td>{{row.componentDisplayName}}</td> + <td> + {{#each file in row.fileNamesObject}} + <p> + <a {{action openLogFile row file.filePath target="view.parentView"}} href="#" rel="log-file-name-tooltip" {{bindAttr data-original-title="file.filePath"}}>{{file.fileName}}</a> + <a {{bindAttr href="file.url"}} target="_blank" rel="log-file-name-tooltip" {{translateAttr title="popup.logTail.openInLogSearch"}} class="pull-right external-link"> + <i class="icon-external-link"></i> + {{t popup.logTail.openInLogSearch}} + </a> + </p> + {{/each}} + </td> + {{/view}} {{/each}} {{/if}} </tbody> http://git-wip-us.apache.org/repos/asf/ambari/blob/7cfcf657/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 631493b..3398bb2 100644 --- a/ambari-web/app/templates/main/host/summary.hbs +++ b/ambari-web/app/templates/main/host/summary.hbs @@ -181,7 +181,7 @@ {{/unless}} {{!logs metrics}} - {{#if App.supports.logSearch}} + {{#if App.supports.logCountVizualization}} <div class="box"> <div class="box-header"> <h4>{{t hosts.host.summary.hostLogMetrics}}</h4> http://git-wip-us.apache.org/repos/asf/ambari/blob/7cfcf657/ambari-web/app/utils/ajax/ajax.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/utils/ajax/ajax.js b/ambari-web/app/utils/ajax/ajax.js index 1cb70c5..a7087c9 100644 --- a/ambari-web/app/utils/ajax/ajax.js +++ b/ambari-web/app/utils/ajax/ajax.js @@ -1384,6 +1384,11 @@ var urls = { }; } }, + + 'cluster.logging.searchEngine': { + real: '/clusters/{clusterName}/logging/searchEngine?{query}', + mock: '' + }, 'admin.high_availability.polling': { 'real': '/clusters/{clusterName}/requests/{requestId}?fields=tasks/*,Requests/*', 'mock': '/data/background_operations/host_upgrade_tasks.json' @@ -2403,6 +2408,11 @@ var urls = { } } }, + + 'host.logging': { + 'real': '/clusters/{clusterName}/hosts/{hostName}?fields=host_components/logging,host_components/HostRoles/service_name{fields}{query}&minimal_response=true', + 'mock': '' + }, 'components.filter_by_status': { 'real': '/clusters/{clusterName}/components?fields=host_components/HostRoles/host_name,ServiceComponentInfo/component_name,ServiceComponentInfo/started_count{urlParams}&minimal_response=true', 'mock': '' @@ -2609,6 +2619,12 @@ var urls = { } } }, + + 'logtail.get': { + 'real': '/clusters/{clusterName}/logging/searchEngine?component_name={logComponentName}&host_name={hostName}&pageSize={pageSize}&startIndex={startIndex}', + 'mock': '' + }, + 'service.serviceConfigVersions.get': { real: '/clusters/{clusterName}/configurations/service_config_versions?service_name={serviceName}&fields=service_config_version,user,hosts,group_id,group_name,is_current,createtime,service_name,service_config_version_note,stack_id,is_cluster_compatible&minimal_response=true', mock: '/data/configurations/service_versions.json' http://git-wip-us.apache.org/repos/asf/ambari/blob/7cfcf657/ambari-web/app/utils/file_utils.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/utils/file_utils.js b/ambari-web/app/utils/file_utils.js index 93e73ed..34a33b9 100644 --- a/ambari-web/app/utils/file_utils.js +++ b/ambari-web/app/utils/file_utils.js @@ -72,6 +72,10 @@ module.exports = { document.body.appendChild(linkEl); linkEl.click(); document.body.removeChild(linkEl); + }, + + fileNameFromPath: function(path) { + return path.split('/').slice(-1); } }; http://git-wip-us.apache.org/repos/asf/ambari/blob/7cfcf657/ambari-web/app/utils/host_progress_popup.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/utils/host_progress_popup.js b/ambari-web/app/utils/host_progress_popup.js index 1d2409d..177d806 100644 --- a/ambari-web/app/utils/host_progress_popup.js +++ b/ambari-web/app/utils/host_progress_popup.js @@ -841,7 +841,7 @@ App.HostPopup = Em.Object.create({ /** * @type {String[]} */ - classNames: ['sixty-percent-width-modal', 'host-progress-popup'], + classNames: ['sixty-percent-width-modal', 'host-progress-popup', 'full-height-modal'], /** * for the checkbox: do not show this dialog again @@ -903,5 +903,4 @@ App.HostPopup = Em.Object.create({ return this.get('isPopup'); } - }); http://git-wip-us.apache.org/repos/asf/ambari/blob/7cfcf657/ambari-web/app/views.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/views.js b/ambari-web/app/views.js index 8e02639..127a996 100644 --- a/ambari-web/app/views.js +++ b/ambari-web/app/views.js @@ -21,6 +21,7 @@ require('views/application'); require('views/common/log_file_search_view'); +require('views/common/log_tail_view'); require('views/common/global/spinner'); require('views/common/ajax_default_error_popup_body'); require('views/common/chart'); @@ -39,6 +40,7 @@ require('views/common/modal_popups/dependent_configs_list_popup'); require('views/common/modal_popups/select_groups_popup'); require('views/common/modal_popups/logs_popup'); require('views/common/modal_popups/log_file_search_popup'); +require('views/common/modal_popups/log_tail_popup'); require('views/common/editable_list'); require('views/common/host_progress_popup_body_view'); require('views/common/rolling_restart_view'); http://git-wip-us.apache.org/repos/asf/ambari/blob/7cfcf657/ambari-web/app/views/common/filter_view.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/views/common/filter_view.js b/ambari-web/app/views/common/filter_view.js index be1e1a7..6f3a7bd 100644 --- a/ambari-web/app/views/common/filter_view.js +++ b/ambari-web/app/views/common/filter_view.js @@ -584,6 +584,10 @@ module.exports = { return function (origin, compareValue) { return origin === (compareValue === 'enabled'); }; + case 'file_extension': + return function(origin, compareValue) { + return origin.endsWith(compareValue); + }; case 'string': default: return function (origin, compareValue) { http://git-wip-us.apache.org/repos/asf/ambari/blob/7cfcf657/ambari-web/app/views/common/host_progress_popup_body_view.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/views/common/host_progress_popup_body_view.js b/ambari-web/app/views/common/host_progress_popup_body_view.js index 12eb029..2168020 100644 --- a/ambari-web/app/views/common/host_progress_popup_body_view.js +++ b/ambari-web/app/views/common/host_progress_popup_body_view.js @@ -19,6 +19,7 @@ var App = require('app'); var batchUtils = require('utils/batch_scheduled_requests'); var date = require('utils/date/date'); +var fileUtils = require('utils/file_utils'); /** * @typedef {object} TaskRelationObject @@ -87,6 +88,13 @@ App.HostProgressPopupBodyView = App.TableView.extend({ sourceRequestScheduleCommand: null, /** + * @type {string} + */ + clipBoardContent: null, + + isClipBoardActive: false, + + /** * Alias for <code>controller.hosts</code> * * @type {wrappedHost[]} @@ -237,15 +245,96 @@ App.HostProgressPopupBodyView = App.TableView.extend({ didInsertElement: function () { this.updateHostInfo(); + this.subscribeResize(); }, willDestroyElement: function () { if (this.get('controller.dataSourceController.name') == 'highAvailabilityProgressPopupController') { this.set('controller.dataSourceController.isTaskPolling', false); } + this.unsubscribeResize(); }, /** + * Subscribe for window <code>resize</code> event. + * + * @method subscribeResize + */ + subscribeResize: function() { + var self = this; + $(window).on('resize', this.resizeHandler.bind(this)); + Em.run.next(this, function() { + self.resizeHandler(); + }); + }, + + /** + * Remove event listener for window <code>resize</code> event. + */ + unsubscribeResize: function() { + $(window).off('resize', this.resizeHandler.bind(this)); + }, + + /** + * This method handles window resize and fit modal body content according to visible items for each <code>level</code>. + * + * @method resizeHandler + */ + resizeHandler: function() { + if (this.get('state') === 'destroyed' || !this.get('parentView.isOpen')) return; + var headerHeight = 48, + modalFooterHeight = 60, + taskTopWrapHeight = 40, + modalTopOffset = $('.modal').offset().top, + contentPaddingBottom = 40, + hostsPageBarHeight = 45, + tabbedContentNavHeight = 68, + logComponentFileNameHeight = 30, + levelName = this.get('currentLevelName'), + boLevelHeightMap = { + 'REQUESTS_LIST': { + height: window.innerHeight - 2*modalTopOffset - headerHeight - taskTopWrapHeight - modalFooterHeight - contentPaddingBottom, + target: '#service-info' + }, + 'HOSTS_LIST': { + height: window.innerHeight - 2*modalTopOffset - headerHeight - taskTopWrapHeight - modalFooterHeight - contentPaddingBottom - hostsPageBarHeight, + target: '#host-info' + }, + 'TASKS_LIST': { + height: window.innerHeight - 2*modalTopOffset - headerHeight - taskTopWrapHeight - modalFooterHeight - contentPaddingBottom, + target: '#host-log' + }, + 'TASK_DETAILS': { + height: window.innerHeight - 2*modalTopOffset - headerHeight - taskTopWrapHeight - modalFooterHeight - contentPaddingBottom, + target: ['.task-detail-log-info', '.log-tail-content.pre-styled'] + } + }, + currentLevelHeight, + resizeTarget; + + if (levelName && levelName in boLevelHeightMap) { + resizeTarget = boLevelHeightMap[levelName].target; + currentLevelHeight = boLevelHeightMap[levelName].height; + if (levelName === 'TASK_DETAILS' && $('.task-detail-info').hasClass('task-detail-info-tabbed')) { + currentLevelHeight -= tabbedContentNavHeight; + } + if (!Em.isArray(resizeTarget)) { + resizeTarget = [resizeTarget]; + } + resizeTarget.forEach(function(target) { + if (target === '.log-tail-content.pre-styled') { + currentLevelHeight -= logComponentFileNameHeight; + } + $(target).css('maxHeight', currentLevelHeight + 'px'); + }); + } + }, + + currentLevelName: function() { + return this.get('controller.dataSourceController.levelInfo.name'); + }.property('controller.dataSourceController.levelInfo.name'), + + /** * Preset values on init * * @method setOnStart @@ -277,6 +366,7 @@ App.HostProgressPopupBodyView = App.TableView.extend({ }); this.get("controller").setBackgroundOperationHeader(false); this.setOnStart(); + this.rerender(); } }.observes('parentView.isOpen'), @@ -421,6 +511,7 @@ App.HostProgressPopupBodyView = App.TableView.extend({ switchLevel: function (levelName) { var dataSourceController = this.get('controller.dataSourceController'); var args = [].slice.call(arguments); + this.get('hostComponentLogs').clear(); if (this.get("controller.isBackgroundOperations")) { var levelInfo = dataSourceController.get('levelInfo'); levelInfo.set('taskId', this.get('openedTaskId')); @@ -453,6 +544,24 @@ App.HostProgressPopupBodyView = App.TableView.extend({ } }, + levelDidChange: function() { + var levelName = this.get('controller.dataSourceController.levelInfo.name'), + self = this; + + if (levelName && this.get('isLevelLoaded')) { + Em.run.next(this, function() { + self.resizeHandler(); + }); + } + }.observes('controller.dataSourceController.levelInfo.name', 'isLevelLoaded'), + + + popupIsOpenDidChange: function() { + if (!this.get('isOpen')) { + this.get('hostComponentLogs').clear(); + } + }.observes('parentView.isOpen'), + /** * Switch-level custom method for <code>highAvailabilityProgressPopupController</code> * @@ -589,7 +698,7 @@ App.HostProgressPopupBodyView = App.TableView.extend({ */ navigateToHostLogs: function() { var relationType = this._determineRoleRelation(this.get('openedTask')), - hostModel = App.Host.find().findProperty('id', this.get('currentHost.name')), + hostModel = App.Host.find().findProperty('hostName', this.get('openedTask.hostName')), queryParams = [], model; @@ -601,7 +710,7 @@ App.HostProgressPopupBodyView = App.TableView.extend({ if (relationType.type === 'service') { queryParams.push('service_name=' + relationType.value); } - App.router.transitionTo('main.hosts.hostDetails.logs', hostModel, { query: '?' + queryParams.join('&') }); + App.router.transitionTo('main.hosts.hostDetails.logs', hostModel, { query: ''}); if (this.get('parentView') && typeof this.get('parentView').onClose === 'function') this.get('parentView').onClose(); }, @@ -754,12 +863,19 @@ App.HostProgressPopupBodyView = App.TableView.extend({ * @method openTaskLogInDialog */ openTaskLogInDialog: function () { + var target = ".task-detail-log-info", + activeHostLog = this.get('hostComponentLogs').findProperty('isActive', true), + activeHostLogSelector = activeHostLog ? activeHostLog.get('tabClassNameSelector') + '.active' : false; + if ($(".task-detail-log-clipboard").length) { this.destroyClipBoard(); } + if (activeHostLog && $(activeHostLogSelector).length) { + target = activeHostLogSelector; + } var newWindow = window.open(); var newDocument = newWindow.document; - newDocument.write($(".task-detail-log-info").html()); + newDocument.write($(target).html()); newDocument.close(); }, @@ -798,17 +914,19 @@ App.HostProgressPopupBodyView = App.TableView.extend({ * @method createClipBoard */ createClipBoard: function () { - var logElement = $(".task-detail-log-maintext"), - logElementRect = logElement[0].getBoundingClientRect(); + var isLogComponentActive = this.get('hostComponentLogs').someProperty('isActive', true), + logElement = isLogComponentActive ? $('.log-component-tab.active .log-tail-content'): $(".task-detail-log-maintext"), + logElementRect = logElement[0].getBoundingClientRect(), + clipBoardContent = this.get('clipBoardContent'); $(".task-detail-log-clipboard-wrap").html('<textarea class="task-detail-log-clipboard"></textarea>'); $(".task-detail-log-clipboard") - .html("stderr: \n" + $(".stderr").html() + "\n stdout:\n" + $(".stdout").html()) + .html(isLogComponentActive ? this.get('clipBoardContent') : "stderr: \n" + $(".stderr").html() + "\n stdout:\n" + $(".stdout").html()) .css('display', 'block') .width(logElementRect.width) - .height(logElementRect.height) + .height(isLogComponentActive ? logElement[0].scrollHeight : logElementRect.height) .select(); - logElement.css("display", "none"); + this.set('isClipBoardActive', true); }, /** @@ -817,8 +935,142 @@ App.HostProgressPopupBodyView = App.TableView.extend({ * @method destroyClipBoard */ destroyClipBoard: function () { + var logElement = this.get('hostComponentLogs').someProperty('isActive', true) ? $('.log-component-tab.active .log-tail-content'): $(".task-detail-log-maintext"); + $(".task-detail-log-clipboard").remove(); - $(".task-detail-log-maintext").css("display", "block"); - } + logElement.css("display", "block"); + this.set('isClipBoardActive', false); + }, + + isLogSearchInstalled: function() { + return App.Service.find().someProperty('serviceName', 'LOGSEARCH'); + }.property(), + + /** + * Host component logs associated with selected component on 'TASK_DETAILS' level. + * + * @property {object[]} + */ + hostComponentLogs: function() { + var relationType, + componentName, + hostName, + logFile, + self = this; + + if (this.get('openedTask.id')) { + relationType = this._determineRoleRelation(this.get('openedTask')); + if (relationType.type === 'component') { + hostName = this.get('currentHost.name'); + componentName = relationType.value; + return App.HostComponentLog.find() + .filterProperty('hostComponent.host.hostName', hostName) + .filterProperty('hostComponent.componentName', componentName) + .reduce(function(acc, item, index) { + var serviceName = item.get('hostComponent.service.serviceName'), + logComponentName = item.get('name'), + componentHostName = item.get('hostComponent.host.hostName'); + acc.pushObjects(item.get('logFileNames').map(function(logFileName, idx) { + var tabClassName = logComponentName + '_' + index + '_' + idx; + return Em.Object.create({ + hostName: componentHostName, + logComponentName: logComponentName, + fileName: logFileName, + tabClassName: tabClassName, + tabClassNameSelector: '.' + tabClassName, + displayedFileName: fileUtils.fileNameFromPath(logFileName), + url: self.get('logSearchUrlTemplate').format(hostName, logFileName, logComponentName), + isActive: false + }); + })); + return acc; + }, []); + } + } + return []; + }.property('openedTaskId', 'isLevelLoaded'), + + + /** + * Log Search UI link template used for 'Open In Log Search' links. + * + * @property {string} + */ + logSearchUrlTemplate: function() { + var quickLink = App.QuickLinks.find().findProperty('site', 'logsearch-site'), + logSearchServerHost = App.HostComponent.find().findProperty('componentName', 'LOGSEARCH_SERVER').get('host.hostName'); + + if (quickLink) { + return quickLink.get('template').fmt('http', logSearchServerHost, quickLink.get('default_http_port')) + '?host_name={0}&file_name={1}&component_name={2}'; + } + return '#'; + }.property('hostComponentLogsExists'), + /** + * Determines if there are component logs for selected component within 'TASK_DETAILS' level. + * + * @property {boolean} + */ + hostComponentLogsExists: function() { + return this.get('isLogSearchInstalled') && !!this.get('hostComponentLogs.length') && this.get('parentView.isOpen'); + }.property('hostComponentLogs.length', 'isLogSearchInstalled', 'parentView.isOpen'), + + /** + * Minimum required content to embed in App.LogTailView. This property observes current active host component log. + * + * @property {object} + */ + logTailViewContent: function() { + if (!this.get('hostComponentLog')) { + return null; + } + return Em.Object.create({ + hostName: this.get('currentHost.name'), + logComponentName: this.get('hostComponentLog.name') + }); + }.property('[email protected]'), + + logTailView: App.LogTailView.extend({ + + isActiveDidChange: function() { + var self = this; + if (this.get('content.isActive') === false) return; + setTimeout(function() { + self.scrollToBottom(); + self.storeToClipBoard(); + }, 500); + }.observes('content.isActive'), + + logRowsLengthDidChange: function() { + if (!this.get('content.isActive') || this.get('state') === 'destroyed') return; + this.storeToClipBoard(); + }.observes('logRows.length'), + + /** + * Stores log content to use for clip board. + */ + storeToClipBoard: function() { + this.get('parentView').set('clipBoardContent', this.get('logRows').map(function(i) { + return i.get('logtimeFormatted') + ' ' + i.get('level') + ' ' + i.get('logMessage'); + }).join('\n')); + } + }), + + setActiveLogTab: function(e) { + var content = e.context; + this.set('clipBoardContent', null); + this.get('hostComponentLogs').without(content).setEach('isActive', false); + if (this.get('isClipBoardActive')) { + this.destroyClipBoard(); + } + content.set('isActive', true); + }, + + setActiveTaskLogTab: function() { + this.set('clipBoardContent', null); + this.get('hostComponentLogs').setEach('isActive', false); + if (this.get('isClipBoardActive')) { + this.destroyClipBoard(); + } + } }); http://git-wip-us.apache.org/repos/asf/ambari/blob/7cfcf657/ambari-web/app/views/common/log_tail_view.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/views/common/log_tail_view.js b/ambari-web/app/views/common/log_tail_view.js new file mode 100644 index 0000000..ca66e33 --- /dev/null +++ b/ambari-web/app/views/common/log_tail_view.js @@ -0,0 +1,232 @@ +/** + * 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'); +var dateUtils = require('utils/date/date'); + +App.LogTailView = Em.View.extend(App.InfiniteScrollMixin, { + startIndex: 0, + selectedTailCount: 50, + autoResize: false, + logRows: Em.A([]), + totalCount: 0, + totalItems: 0, + lastLogTime: 0, + isDataReady: false, + refreshEnd: true, + pollLogs: true, + pollLogInterval: 2000, + pollLogTimeoutId: null, + + oldLogsAvailable: false, + oldLogsIsFetching: false, + + templateName: require('templates/common/log_tail'), + + content: null, + + /** + * elements size are: + * .modal margin 40px x 2 + * .modal inner padding 15px x 2 + * .top-wrap-header height + margin 60px + * .modal-footer 60px + * .log-tail-popup-info 45px + * + * @property {number} resizeDelta + */ + resizeDelta: 15*2 + 60 + 60 + 40 + 45, + + didInsertElement: function() { + var self = this; + this.infiniteScrollInit(this.$('.log-tail-content'), { + appendHtml: '<span id="empty-span"></span>' + }); + this.fetchRows({ + startIndex: 0 + }).then(function(data) { + var logs = self.fetchRowsSuccess(data); + self.infiniteScrollSetDataAvailable(true); + self.appendLogRows(logs.reverse()); + self.saveLastTimestamp(logs); + self.set('isDataReady', true); + self.scrollToBottom(); + self.startLogPolling(); + }); + this.subscribeResize(); + }, + + scrollToBottom: function() { + Em.run.next(this, function() { + this.$('.log-tail-content').scrollTop(this.$('.log-tail-content').prop('scrollHeight')); + }); + }, + + _infiniteScrollHandler: function(e) { + var self = this; + this._super(e); + if ($(e.target).scrollTop() === 0) { + if (this.get('noOldLogs') && !this.get('oldLogsIsFetching')) return; + self.set('oldLogsIsFetching', true); + this.fetchRows(this.oldestLogs()).then(function(data) { + var oldestLog = self.get('logRows.0.logtime'); + var logRows = self.fetchRowsSuccess(data).filter(function(i) { + return parseInt(i.get('logtime'), 10) < parseInt(oldestLog, 10); + }); + if (logRows.length) { + self.get('logRows').unshiftObjects(logRows.reverse()); + } else { + self.set('noOldLogs', true); + } + self.set('oldLogsIsFetching', false); + }); + } + }, + + willDestroyElement: function() { + this._super(); + this.stopLogPolling(); + this.unsubscribeResize(); + }, + + resizeHandler: function() { + // window.innerHeight * 0.1 = modal popup top 5% from both sides = 10% + if (this.get('state') === 'destroyed') return; + var newSize = $(window).height() - this.get('resizeDelta') - window.innerHeight*0.08; + this.$().find('.log-tail-content.pre-styled').css('maxHeight', newSize + 'px'); + }, + + unsubscribeResize: function() { + if (!this.get('autoResize')) return; + $(window).off('resize', this.resizeHandler.bind(this)); + }, + + subscribeResize: function() { + if (!this.get('autoResize')) return; + this.resizeHandler(); + $(window).on('resize', this.resizeHandler.bind(this)); + }, + + fetchRows: function(opts) { + var options = $.extend({}, true, { + startIndex: this.get('startIndex'), + pageSize: this.get('selectedTailCount') + }, opts || {}); + return App.ajax.send({ + sender: this, + name: 'logtail.get', + data: { + hostName: this.get('content.hostName'), + logComponentName: this.get('content.logComponentName'), + pageSize: "" + options.pageSize, + startIndex: "" + options.startIndex + }, + error: 'fetchRowsError' + }); + }, + + fetchRowsSuccess: function(data) { + var logRows = Em.getWithDefault(data, 'logList', []).map(function(logItem, i) { + var item = App.keysUnderscoreToCamelCase(App.permit(logItem, ['log_message', 'logtime', 'level', 'id'])); + item.logtimeFormatted = dateUtils.dateFormat(parseInt(item.logtime, 10), 'YYYY-MM-DD HH:mm:ss,SSS'); + return Em.Object.create(item); + }); + if (logRows.length === 0) { + this.infiniteScrollSetDataAvailable(false); + return []; + } + return logRows; + }, + + fetchRowsError: function() {}, + + /** actions **/ + + openInLogSearch: function() {}, + + refreshContent: function() { + var self = this, + allIds; + if (!this.get('refreshEnd')) { + return $.Deferred().resolve().promise(); + } + this.set('refreshEnd', false); + //this.infiniteScrollSetDataAvailable(true); + allIds = this.get('logRows').mapProperty('id'); + return this.fetchRows(this.currentPage()).then(function(data) { + var logRows = self.fetchRowsSuccess(data).filter(function(i) { + return parseInt(i.logtime, 10) > parseInt(self.get('lastLogTime'), 10) && !allIds.contains(i.get('id')); + }); + if (logRows.length) { + self.appendLogRows(logRows.reverse()); + self.saveLastTimestamp(logRows); + } + self.set('refreshEnd', true); + }); + }, + + appendLogRows: function(logRows) { + this.get('logRows').pushObjects(logRows); + }, + + saveLastTimestamp: function(logRows) { + this.set('lastLogTime', Em.getWithDefault(logRows, '0.logtime', 0)); + return this.get('lastLogTime'); + }, + + currentPage: function() { + return { + startIndex: 0, + pageSize: this.get('selectedTailCount') + }; + }, + + nextPage: function() { + var newIndex = this.get('startIndex') + this.get('selectedTailCount'); + if (newIndex < 0) { + newIndex = 0; + } + this.set('startIndex', newIndex); + return { + startIndex: newIndex, + pageSize: this.get('selectedTailCount') + }; + }, + + oldestLogs: function() { + return { + startIndex: this.get('logRows.length'), + pageSize: this.get('selectedTailCount') + }; + }, + + startLogPolling: function() { + var self = this; + if (!this.get('pollLogs') || this.get('state') === 'destroyed') return; + this.set('pollLogTimeoutId', setTimeout(function() { + self.stopLogPolling(); + self.refreshContent().then(self.startLogPolling.bind(self)); + }, this.get('pollLogInterval'))); + }, + + stopLogPolling: function() { + if (!this.get('pollLogs') || this.get('pollLogTimeoutId') === null) return; + clearTimeout(this.get('pollLogTimeoutId')); + } + +}); http://git-wip-us.apache.org/repos/asf/ambari/blob/7cfcf657/ambari-web/app/views/common/modal_popup.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/views/common/modal_popup.js b/ambari-web/app/views/common/modal_popup.js index 571bf82..5e3544a 100644 --- a/ambari-web/app/views/common/modal_popup.js +++ b/ambari-web/app/views/common/modal_popup.js @@ -68,19 +68,25 @@ App.ModalPopup = Ember.View.extend({ this.$().find('#modal') .on('enter-key-pressed', this.enterKeyPressed.bind(this)) .on('escape-key-pressed', this.escapeKeyPressed.bind(this)); + this.fitZIndex(); + var firstInputElement = this.$('#modal').find(':input').not(':disabled, .no-autofocus').first(); + this.focusElement(firstInputElement); + this.resizeHandler(); + $(window).on('resize', this.resizeHandler.bind(this)); + }, + + resizeHandler: function() { if (this.autoHeight && !$.mocho) { var block = this.$().find('#modal > .modal-body').first(); if(block.offset()) { block.css('max-height', $(window).height() - block.offset().top - this.marginBottom + $(window).scrollTop()); // fix popup height } } - this.fitZIndex(); - var firstInputElement = this.$('#modal').find(':input').not(':disabled, .no-autofocus').first(); - this.focusElement(firstInputElement); }, willDestroyElement: function() { this.$().find('#modal').off('enter-key-pressed').off('escape-key-pressed'); + $(window).off('resize', this.resizeHandler); }, escapeKeyPressed: function (event) { http://git-wip-us.apache.org/repos/asf/ambari/blob/7cfcf657/ambari-web/app/views/common/modal_popups/log_tail_popup.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/views/common/modal_popups/log_tail_popup.js b/ambari-web/app/views/common/modal_popups/log_tail_popup.js new file mode 100644 index 0000000..bb0fb64 --- /dev/null +++ b/ambari-web/app/views/common/modal_popups/log_tail_popup.js @@ -0,0 +1,115 @@ +/** + * 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'); +var dateUtils = require('utils/date/date'); +var fileUtils = require('utils/file_utils'); + +App.showLogTailPopup = function(content) { + return App.ModalPopup.show({ + classNames: ['log-tail-popup', 'full-width-modal', 'full-height-modal'], + header: fileUtils.fileNameFromPath(content.get('filePath')), + primary: false, + secondary: Em.I18n.t('common.dismiss'), + secondaryClass: 'btn-success', + showFooter: true, + autoHeight: false, + bodyClass: Em.View.extend({ + templateName: require('templates/common/modal_popups/log_tail_popup'), + content: content, + selectedTailCount: 50, + isCopyActive: false, + copyContent: null, + + logSearchUrl: function() { + var quickLink = App.QuickLinks.find().findProperty('site', 'logsearch-site'), + logSearchServerHost = App.HostComponent.find().findProperty('componentName', 'LOGSEARCH_SERVER').get('host.hostName'); + + if (quickLink) { + return quickLink.get('template').fmt('http', logSearchServerHost, quickLink.get('default_http_port')) + '?host_name=' + this.get('content.hostName') + '&file_name=' + this.get('content.filePath') + '&component_name=' + this.get('content.logComponentName'); + } + return '#'; + }.property('content'), + + logTailViewInstance: null, + + /** actions **/ + openInNewTab: function() { + var newWindow = window.open(); + var newDocument = newWindow.document; + newDocument.write($('.log-tail-content.pre-styled').html()); + newDocument.close(); + }, + + toggleCopy: function() { + if (!this.get('isCopyActive')) { + this.initCopy(); + } else { + this.destroyCopy(); + } + }, + + initCopy: function() { + var self = this; + this.set('copyContent', this.logsToString()); + this.set('isCopyActive', true); + Em.run.next(function() { + self.$().find('.copy-textarea').select(); + }); + }, + + destroyCopy: function() { + this.set('copyContent', null); + this.set('isCopyActive', false); + }, + + logsToString: function() { + return this.get('logTailViewInstance.logRows').map(function(i) { + return i.get('logtimeFormatted') + ' ' + i.get('level') + ' ' + i.get('logMessage'); + }).join('\n'); + }, + + logTailContentView: App.LogTailView.extend({ + contentBinding: "parentView.content", + autoResize: true, + selectedTailCountBinding: "parentView.selectedTailCount", + + didInsertElement: function() { + this._super(); + this.set('parentView.logTailViewInstance', this); + }, + + resizeHandler: function() { + if (this.get('state') === 'destroyed') return; + this._super(); + var newSize = $(window).height() - this.get('resizeDelta') - window.innerHeight*0.08; + this.get('parentView').$().find('.copy-textarea').css({ + height: newSize + 'px', + width: '100%' + }); + }, + + willDestroyElement: function() { + this._super(); + this.set('parentView.logTailViewInstance', null); + } + }) + }) + }); +}; http://git-wip-us.apache.org/repos/asf/ambari/blob/7cfcf657/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 b51f955..7ca18f8 100644 --- a/ambari-web/app/views/main/host/logs_view.js +++ b/ambari-web/app/views/main/host/logs_view.js @@ -20,6 +20,7 @@ var App = require('app'); var filters = require('views/common/filter_view'); var sort = require('views/common/sort_view'); +var fileUtils = require('utils/file_utils'); App.MainHostLogsView = App.TableView.extend({ templateName: require('templates/main/host/logs'), @@ -31,16 +32,42 @@ App.MainHostLogsView = App.TableView.extend({ */ host: Em.computed.alias('App.router.mainHostDetailsController.content'), - content: function() { - return [ - Em.Object.create({ - serviceName: 'HDFS', - componentName: 'DATANODE', - fileExtension: '.log', - fileName: 'HDFS_DATANODE.log' - }) - ]; + hostLogs: function() { + return App.HostComponentLog.find().filterProperty('hostName', this.get('host.hostName')); + }.property('App.HostComponentLog.length'), + + logSearchUrlTemplate: function() { + var quickLink = App.QuickLinks.find().findProperty('site', 'logsearch-site'), + logSearchServerHost = App.HostComponent.find().findProperty('componentName', 'LOGSEARCH_SERVER').get('host.hostName'); + + if (quickLink) { + return quickLink.get('template').fmt('http', logSearchServerHost, quickLink.get('default_http_port')) + '?host_name=' + this.get('host.hostName') + '&file_name={0}&component_name={1}'; + } + return '#'; }.property(), + + content: function() { + var self = this; + return this.get('hostLogs').map(function(i) { + return Em.Object.create({ + serviceName: i.get('hostComponent.service.serviceName'), + serviceDisplayName: i.get('hostComponent.service.displayName'), + componentName: i.get('hostComponent.componentName'), + componentDisplayName: i.get('hostComponent.displayName'), + hostName: self.get('host.hostName'), + logComponentName: i.get('name'), + fileNamesObject: i.get('logFileNames').map(function(filePath) { + return { + fileName: fileUtils.fileNameFromPath(filePath), + filePath: filePath, + url: self.get('logSearchUrlTemplate').format(filePath, i.get('name')) + }; + }), + fileNamesFilterValue: i.get('logFileNames').join(',') + }); + }); + }.property('hostLogs.length'), + /** * @type {Ember.View} */ @@ -78,8 +105,8 @@ App.MainHostLogsView = App.TableView.extend({ }].concat(App.Service.find().mapProperty('serviceName').uniq().map(function(item) { return { value: item, - label: item - } + label: App.Service.find().findProperty('serviceName', item).get('displayName') + }; })); }.property('App.router.clusterController.isLoaded'), onChangeValue: function() { @@ -101,7 +128,7 @@ App.MainHostLogsView = App.TableView.extend({ return { value: item.get('componentName'), label: item.get('displayName') - } + }; }); return [{ value: '', @@ -116,6 +143,7 @@ App.MainHostLogsView = App.TableView.extend({ fileExtensionsFilter: filters.createSelectView({ column: 3, fieldType: 'filter-input-width', + type: 'string', didInsertElement: function() { this.setValue(Em.getWithDefault(this, 'controller.serializedQuery.file_extension', '')); this._super(); @@ -131,11 +159,11 @@ App.MainHostLogsView = App.TableView.extend({ return { value: item, label: item - } - })) + }; + })); }.property('App.router.clusterController.isLoaded'), onChangeValue: function() { - this.get('parentView').updateFilter(this.get('column'), this.get('value'), 'select'); + this.get('parentView').updateFilter(this.get('column'), this.get('value'), 'file_extension'); } }), @@ -146,14 +174,33 @@ App.MainHostLogsView = App.TableView.extend({ var ret = []; ret[1] = 'serviceName'; ret[2] = 'componentName'; - ret[3] = 'fileExtension'; + ret[3] = 'fileNamesFilterValue'; return ret; }.property(), + logFileRowView: Em.View.extend({ + tagName: 'tr', + + didInsertElement: function() { + this._super(); + App.tooltip(this.$('[rel="log-file-name-tooltip"]')); + }, + + willDestroyElement: function() { + this.$('[rel="log-file-name-tooltip"]').tooltip('destroy'); + } + }), + openLogFile: function(e) { - var fileData = e.context; - if (e.context) { - App.LogFileSearchPopup(fileData.fileName); + var content = e.contexts, + filePath = content[1], + componentLog = content[0]; + if (e.contexts.length) { + App.showLogTailPopup(Em.Object.create({ + logComponentName: componentLog.get('logComponentName'), + hostName: componentLog.get('hostName'), + filePath: filePath + })); } } }); http://git-wip-us.apache.org/repos/asf/ambari/blob/7cfcf657/ambari-web/app/views/main/host/menu.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/views/main/host/menu.js b/ambari-web/app/views/main/host/menu.js index d45d930..155f6bb 100644 --- a/ambari-web/app/views/main/host/menu.js +++ b/ambari-web/app/views/main/host/menu.js @@ -57,7 +57,10 @@ App.MainHostMenuView = Em.CollectionView.extend({ label: Em.I18n.t('hosts.host.menu.logs'), routing: 'logs', hidden: function () { - return !App.get('supports.logSearch'); + if (App.get('supports.logSearch')) { + return !App.Service.find().someProperty('serviceName', 'LOGSEARCH'); + } + return true; }.property('App.supports.logSearch'), id: 'host-details-summary-logs' }) @@ -109,4 +112,4 @@ App.MainHostMenuView = Em.CollectionView.extend({ '{{view.content.badgeText}}' + '</span> {{/if}}</a>{{/unless}}') }) -}); \ No newline at end of file +}); http://git-wip-us.apache.org/repos/asf/ambari/blob/7cfcf657/ambari-web/test/views/main/host/menu_test.js ---------------------------------------------------------------------- diff --git a/ambari-web/test/views/main/host/menu_test.js b/ambari-web/test/views/main/host/menu_test.js index f9636d8..c029acc 100644 --- a/ambari-web/test/views/main/host/menu_test.js +++ b/ambari-web/test/views/main/host/menu_test.js @@ -29,10 +29,12 @@ describe('App.MainHostMenuView', function () { beforeEach(function () { this.mock = sinon.stub(App, 'get'); + this.serviceMock = sinon.stub(App.Service, 'find'); }); afterEach(function () { App.get.restore(); + App.Service.find.restore(); }); Em.A([ @@ -57,19 +59,28 @@ describe('App.MainHostMenuView', function () { Em.A([ { logSearch: false, + services: [{serviceName: 'LOGSEARCH'}], m: '`logs` tab is invisible', e: true }, { logSearch: true, + services: [], + m: '`logs` tab is invisible because service not installed', + e: true + }, + { + logSearch: true, + services: [{serviceName: 'LOGSEARCH'}], m: '`logs` tab is visible', e: false } ]).forEach(function(test) { it(test.m, function() { - this.mock.withArgs('supports.logSearch').returns(test.logSearch); - view.propertyDidChange('content'); - expect(view.get('content').findProperty('name', 'logs').get('hidden')).to.equal(test.e); + this.mock.withArgs('supports.logSearch').returns(test.logSearch); + this.serviceMock.returns(test.services); + view.propertyDidChange('content'); + expect(view.get('content').findProperty('name', 'logs').get('hidden')).to.equal(test.e); }); }); }); @@ -115,4 +126,4 @@ describe('App.MainHostMenuView', function () { }); }); -}); \ No newline at end of file +});
