Parcourir la source

AMBARI-16151. Finalize LogSearch integration (alexantonenko)

Alex Antonenko il y a 9 ans
Parent
commit
7cfcf65795
30 fichiers modifiés avec 1193 ajouts et 88 suppressions
  1. 3 2
      ambari-web/app/config.js
  2. 4 0
      ambari-web/app/controllers/global/update_controller.js
  3. 20 0
      ambari-web/app/mappers/hosts_mapper.js
  4. 5 0
      ambari-web/app/messages.js
  5. 15 1
      ambari-web/app/mixins/common/infinite_scroll_mixin.js
  6. 2 1
      ambari-web/app/models.js
  7. 1 0
      ambari-web/app/models/host_component.js
  8. 29 0
      ambari-web/app/models/host_component_log.js
  9. 13 1
      ambari-web/app/styles/application.less
  10. 33 0
      ambari-web/app/styles/common.less
  11. 75 0
      ambari-web/app/styles/log_file_search.less
  12. 101 3
      ambari-web/app/styles/modal_popups.less
  13. 61 31
      ambari-web/app/templates/common/host_progress_popup.hbs
  14. 34 0
      ambari-web/app/templates/common/log_tail.hbs
  15. 1 1
      ambari-web/app/templates/common/modal_popup.hbs
  16. 49 0
      ambari-web/app/templates/common/modal_popups/log_tail_popup.hbs
  17. 15 7
      ambari-web/app/templates/main/host/logs.hbs
  18. 1 1
      ambari-web/app/templates/main/host/summary.hbs
  19. 16 0
      ambari-web/app/utils/ajax/ajax.js
  20. 4 0
      ambari-web/app/utils/file_utils.js
  21. 1 2
      ambari-web/app/utils/host_progress_popup.js
  22. 2 0
      ambari-web/app/views.js
  23. 4 0
      ambari-web/app/views/common/filter_view.js
  24. 262 10
      ambari-web/app/views/common/host_progress_popup_body_view.js
  25. 232 0
      ambari-web/app/views/common/log_tail_view.js
  26. 9 3
      ambari-web/app/views/common/modal_popup.js
  27. 115 0
      ambari-web/app/views/common/modal_popups/log_tail_popup.js
  28. 66 19
      ambari-web/app/views/main/host/logs_view.js
  29. 5 2
      ambari-web/app/views/main/host/menu.js
  30. 15 4
      ambari-web/test/views/main/host/menu_test.js

+ 3 - 2
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) {

+ 4 - 0
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 () {

+ 20 - 0
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
    */

+ 5 - 0
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',

+ 15 - 1
ambari-web/app/mixins/common/infinite_scroll_mixin.js

@@ -73,6 +73,12 @@ App.InfiniteScrollMixin = Ember.Mixin.create({
     onResolve: function() {}
   },
 
+  /**
+   * Determines that there is no data to load on next callback call.
+   *
+   */
+  _infiniteScrollMoreData: true,
+
   /**
    * Initialize infinite scroll on specified HTMLElement.
    *
@@ -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);
   }
 });

+ 2 - 1
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');
+require('models/widget_layout');

+ 1 - 0
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'),

+ 29 - 0
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 = [];

+ 13 - 1
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 {

+ 33 - 0
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;
 }

+ 75 - 0
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;
+      }
+    }
+  }
+}

+ 101 - 3
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;
+        }
+      }
+    }
+  }
 }

+ 61 - 31
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>&nbsp;{{t common.tasks}}</a>
+                                                                                              class="icon-arrow-left"></i>&nbsp;{{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}}: &nbsp; <span class="muted">{{view.openedTask.errorLog}} </span></p>
+                <pre class="stderr">{{view.openedTask.stderr}}</pre>
+                <p class="text-bold">{{t common.stdout}}: &nbsp; <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}}: &nbsp; </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}}: &nbsp; <span class="muted">{{view.openedTask.errorLog}} </span></h5>
-            <pre class="stderr">{{view.openedTask.stderr}}</pre>
-            <h5>{{t common.stdout}}: &nbsp; <span class="muted"> {{view.openedTask.outputLog}} </span></h5>
-            <pre class="stdout">{{view.openedTask.stdout}}</pre>
+            {{/each}}
           </div>
         </div>
       </div>

+ 34 - 0
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>

+ 1 - 1
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>

+ 49 - 0
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>

+ 15 - 7
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>

+ 1 - 1
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>

+ 16 - 0
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'

+ 4 - 0
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);
   }
 
 };

+ 1 - 2
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');
   }
-
 });

+ 2 - 0
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');

+ 4 - 0
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) {

+ 262 - 10
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
@@ -86,6 +87,13 @@ App.HostProgressPopupBodyView = App.TableView.extend({
    */
   sourceRequestScheduleCommand: null,
 
+  /**
+   * @type {string}
+   */
+  clipBoardContent: null,
+
+  isClipBoardActive: false,
+
   /**
    * Alias for <code>controller.hosts</code>
    *
@@ -237,14 +245,95 @@ 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
    *
@@ -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('hostComponentLogs.@each.isActive'),
+
+  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();
+    }
+  }
 });

+ 232 - 0
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'));
+  }
+
+});

+ 9 - 3
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) {

+ 115 - 0
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);
+        }
+      })
+    })
+  });
+};

+ 66 - 19
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
+      }));
     }
   }
 });

+ 5 - 2
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}}')
   })
-});
+});

+ 15 - 4
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 () {
     });
   });
 
-});
+});