You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by al...@apache.org on 2016/02/05 17:58:56 UTC

[1/2] ambari git commit: AMBARI-14923. Log Search: Create Log File Search modal popup (alexantonenko)

Repository: ambari
Updated Branches:
  refs/heads/trunk cd35e806c -> b567ca441


AMBARI-14923. Log Search: Create Log File Search modal popup (alexantonenko)


Project: http://git-wip-us.apache.org/repos/asf/ambari/repo
Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/3a2add5d
Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/3a2add5d
Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/3a2add5d

Branch: refs/heads/trunk
Commit: 3a2add5dcde4f1cb730253627b87683f3057c5c1
Parents: cd35e80
Author: Alex Antonenko <hi...@gmail.com>
Authored: Fri Feb 5 18:21:10 2016 +0200
Committer: Alex Antonenko <hi...@gmail.com>
Committed: Fri Feb 5 18:21:10 2016 +0200

----------------------------------------------------------------------
 ambari-web/app/assets/licenses/NOTICE.txt       |   5 +-
 ambari-web/app/assets/test/tests.js             |   1 +
 ambari-web/app/config.js                        |   3 +-
 ambari-web/app/messages.js                      |   9 +-
 ambari-web/app/mixins.js                        |   1 +
 .../app/mixins/common/infinite_scroll_mixin.js  | 173 ++++++++++++
 ambari-web/app/styles/log_file_search.less      | 155 +++++++++++
 .../app/templates/common/log_file_search.hbs    | 109 ++++++++
 ambari-web/app/views.js                         |   3 +
 .../app/views/common/form/datepicker_view.js    |  37 +++
 .../app/views/common/log_file_search_view.js    | 272 +++++++++++++++++++
 .../modal_popups/log_file_search_popup.js       |  26 ++
 .../views/common/log_file_search_view_test.js   | 103 +++++++
 .../vendor/scripts/bootstrap-contextmenu.js     | 205 ++++++++++++++
 14 files changed, 1099 insertions(+), 3 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/3a2add5d/ambari-web/app/assets/licenses/NOTICE.txt
----------------------------------------------------------------------
diff --git a/ambari-web/app/assets/licenses/NOTICE.txt b/ambari-web/app/assets/licenses/NOTICE.txt
index bc29209..c750a37 100644
--- a/ambari-web/app/assets/licenses/NOTICE.txt
+++ b/ambari-web/app/assets/licenses/NOTICE.txt
@@ -56,4 +56,7 @@ This product includes bootstrap-checkbox v.1.0.1 (https://github.com/montrezorro
 Copyright (C) 2014 Roberto Montresor (info [at] robertomontresor [*dot*] it)
 
 This product includes sticky-kit v.1.1.2 (https://github.com/leafo/sticky-kit - MIT License)
-Copyright (C) 2015 Leaf Corcoran (leafot [at] gmail [*dot*] com)
\ No newline at end of file
+Copyright (C) 2015 Leaf Corcoran (leafot [at] gmail [*dot*] com)
+
+This product includes bootstrap-contextmenu v.0.3.3 (https://github.com/sydcanem/bootstrap-contextmenu - MIT License)
+Copyright (C) 2015 James Santos

http://git-wip-us.apache.org/repos/asf/ambari/blob/3a2add5d/ambari-web/app/assets/test/tests.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/assets/test/tests.js b/ambari-web/app/assets/test/tests.js
index 85c814d..e82a27e 100644
--- a/ambari-web/app/assets/test/tests.js
+++ b/ambari-web/app/assets/test/tests.js
@@ -288,6 +288,7 @@ var files = [
   'test/views/common/configs/widgets/time_interval_spinner_view_test',
   'test/views/common/form/spinner_input_view_test',
   'test/views/common/form/manage_kdc_credentials_form_test',
+  'test/views/common/log_file_search_view_test',
   'test/views/wizard/step3/hostLogPopupBody_view_test',
   'test/views/wizard/step3/hostWarningPopupBody_view_test',
   'test/views/wizard/step3/hostWarningPopupFooter_view_test',

http://git-wip-us.apache.org/repos/asf/ambari/blob/3a2add5d/ambari-web/app/config.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/config.js b/ambari-web/app/config.js
index 0beafb3..7d727d7 100644
--- a/ambari-web/app/config.js
+++ b/ambari-web/app/config.js
@@ -80,7 +80,8 @@ App.supports = {
   storeKDCCredentials: true,
   preInstallChecks: false,
   hostComboSearchBox: false,
-  serviceAutoStart: false
+  serviceAutoStart: false,
+  logSearch: false
 };
 
 if (App.enableExperimental) {

http://git-wip-us.apache.org/repos/asf/ambari/blob/3a2add5d/ambari-web/app/messages.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/messages.js b/ambari-web/app/messages.js
index f24876b..8d1f6f1 100644
--- a/ambari-web/app/messages.js
+++ b/ambari-web/app/messages.js
@@ -305,6 +305,13 @@ Em.I18n.translations = {
   'common.openNewWindow': 'Open in New Window',
   'common.fullLogPopup.clickToCopy': 'Click to Copy',
   'common.nothingToDelete': 'Nothing to delete',
+  'common.exclude': 'Exclude',
+  'common.include': 'Include',
+  'common.exclude.short': 'Excl',
+  'common.include.short': 'Incl',
+  'common.filters': 'Filters',
+  'common.keywords': 'Keywods',
+  'common.levels': 'Levels',
 
   'models.alert_instance.tiggered.verbose': "Occurred on {0} <br> Checked on {1}",
   'models.alert_definition.triggered.verbose': "Occurred on {0}",
@@ -1555,7 +1562,7 @@ Em.I18n.translations = {
   'admin.stackUpgrade.failedHosts.details': "Open Details",
   'admin.stackUpgrade.doThisLater': "Do This Later",
   'admin.stackUpgrade.pauseUpgrade': "Pause Upgrade",
-  'admin.stackUpgrade.pauseDowngrade': "Pause Downgrade",  
+  'admin.stackUpgrade.pauseDowngrade': "Pause Downgrade",
   'admin.stackUpgrade.downgrade.proceed': "Proceed with Downgrade",
   'admin.stackUpgrade.downgrade.body': "Are you sure you wish to abort the upgrade process and downgrade to <b>{0}</b>?",
   'admin.stackUpgrade.downgrade.retry.body': "Are you sure you wish to retry downgrade to <b>{0}</b>?",

http://git-wip-us.apache.org/repos/asf/ambari/blob/3a2add5d/ambari-web/app/mixins.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/mixins.js b/ambari-web/app/mixins.js
index 38d112d..460953e 100644
--- a/ambari-web/app/mixins.js
+++ b/ambari-web/app/mixins.js
@@ -22,6 +22,7 @@
 require('mixins/common/blueprint');
 require('mixins/common/kdc_credentials_controller_mixin');
 require('mixins/common/localStorage');
+require('mixins/common/infinite_scroll_mixin');
 require('mixins/common/userPref');
 require('mixins/common/reload_popup');
 require('mixins/common/serverValidator');

http://git-wip-us.apache.org/repos/asf/ambari/blob/3a2add5d/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
new file mode 100644
index 0000000..70c424e
--- /dev/null
+++ b/ambari-web/app/mixins/common/infinite_scroll_mixin.js
@@ -0,0 +1,173 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var App = require('app');
+
+ /**
+ * @typedef {Object} InfiniteScrollMixinOptions
+ * @property {String} appendHtml html to append when scroll ends and callback executed. It's common
+ *   that root node has unique <code>id</code> or <code>class</code> attributes.
+ * @property {Function} callback function to execute when scroll ends. This function should return
+ *   <code>$.Deferred().promise()</code> instance
+ * @property {Function} onReject function to execute when <code>callback</code> rejected.
+ */
+
+/**
+ * @mixin App.InfiniteScrollMixin
+ * This mixin provides methods to attach infinite scroll to specific scrollable element.
+ *
+ * Usage:
+ * <code>
+ *  // mix it
+ *  var myView = Em.View.extend(App.InfiniteScrollMixin, {
+ *    didInsertElement: function() {
+ *    	// call method infiniteScrollInit
+ *    	this.infiniteScrollInit($('.some-scrollable'), {
+ *    		callback: this.someCallbacOnEndReached
+ *    	});
+ *    }
+ *  });
+ * </code>
+ *
+ */
+App.InfiniteScrollMixin = Ember.Mixin.create({
+
+  /**
+   * Stores callback execution progress.
+   *
+   * @type {Boolean}
+   */
+  _infiniteScrollCallbackInProgress: false,
+
+  /**
+   * Stores HTMLElement infinite scroll initiated on.
+   *
+   * @type {HTMLElement}
+   */
+  _infiniteScrollEl: null,
+
+  /**
+   * Default options for infinite scroll.
+   *
+   * @type {InfiniteScrollMixinOptions}
+   */
+  _infiniteScrollDefaults: {
+    appendHtml: '<div id="infinite-scroll-append"><i class="icon-spinner icon-spin"></i></div>',
+    callback: function() { return $.Deferred().resolve().promise(); },
+    onReject: function() {},
+    onResolve: function() {}
+  },
+
+  /**
+   * Initialize infinite scroll on specified HTMLElement.
+   *
+   * @param  {HTMLElement} el DOM element to attach infinite scroll.
+   * @param  {InfiniteScrollMixinOptions} opts
+   */
+  infiniteScrollInit: function(el, opts) {
+    var options = $.extend({}, this.get('_infiniteScrollDefaults'), opts || {});
+    this.set('_infiniteScrollEl', el);
+    this.get('_infiniteScrollEl').on('scroll', this._infiniteScrollHandler.bind(this));
+    this.get('_infiniteScrollEl').on('infinite-scroll-end', this._infiniteScrollEndHandler(options).bind(this));
+  },
+
+  /**
+   * Handler executed on scrolling.
+   * @param  {jQuery.Event} e
+   */
+  _infiniteScrollHandler: function(e) {
+    var el = $(e.target);
+    var height = el.get(0).clientHeight;
+    var scrollHeight = el.prop('scrollHeight');
+    var endPoint = scrollHeight - height;
+    if (endPoint === el.scrollTop() && !this.get('_infiniteScrollCallbackInProgress')) {
+      el.trigger('infinite-scroll-end');
+    }
+  },
+
+  /**
+   * Handler called when scroll ends.
+   *
+   * @param  {InfiniteScrollMixinOptions} options
+   * @return {Function}
+   */
+  _infiniteScrollEndHandler: function(options) {
+    return function(e) {
+      var self = this;
+      if (this.get('_infiniteScrollCallbackInProgress')) return;
+      this._infiniteScrollAppendHtml(options.appendHtml);
+      // always scroll to bottom
+      this.get('_infiniteScrollEl').scrollTop(this.get('_infiniteScrollEl').get(0).scrollHeight);
+      this.set('_infiniteScrollCallbackInProgress', true);
+      options.callback().then(function() {
+        options.onResolve();
+      }, function() {
+        options.onReject();
+      }).always(function() {
+        self.set('_infiniteScrollCallbackInProgress', false);
+        self._infiniteScrollRemoveHtml(options.appendHtml);
+      });
+    }.bind(this);
+  },
+
+  /**
+   * Helper function to append String as html node to.
+   * @param  {String} htmlString string to append
+   */
+  _infiniteScrollAppendHtml: function(htmlString) {
+    this.get('_infiniteScrollEl').append(htmlString);
+  },
+
+  /**
+   * Remove HTMLElement by specified string that can be converted to html. HTMLElement root node
+   * should have unique <code>id</code> or <code>class</code> attribute to avoid removing additional
+   * elements.
+   *
+   * @param  {String} htmlString string to remove
+   */
+  _infiniteScrollRemoveHtml: function(htmlString) {
+    this.get('_infiniteScrollEl').find(this._infiniteScrollGetSelector(htmlString)).remove();
+  },
+
+  /**
+   * Get root node selector.
+   * <code>id</code> attribute has higher priority and will return if found.
+   * <code>class</code> if no <code>id</code> attribute found <code>class</code> attribute
+   * will be used.
+   *
+   * @param  {String} htmlString string processed as HTML
+   * @return {[type]}            [description]
+   */
+  _infiniteScrollGetSelector: function(htmlString) {
+    var html = $(htmlString);
+    var elId = html.attr('id');
+    var elClass = (html.attr('class') || '').split(' ').join('.');
+    html = null;
+    return !!elId ? '#' + elId : '.' + elClass;
+  },
+
+  /**
+   * Remove infinite scroll.
+   * Unbind all listeners.
+   */
+  infiniteScrollDestroy: function() {
+    this.get('_infiniteScrollEl').off('scroll', this._infiniteScrollHandler);
+    this.get('_infiniteScrollEl').off('infinite-scroll-end', this._infiniteScrollHandler);
+    this.set('_infiniteScrollEl', null);
+  }
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/3a2add5d/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
new file mode 100644
index 0000000..aeeea3f
--- /dev/null
+++ b/ambari-web/app/styles/log_file_search.less
@@ -0,0 +1,155 @@
+/**
+ * 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.
+ */
+
+
+@toolbar-context-menu-width: 40px;
+@toolbar-padding: 10px;
+
+.log-file-search-popup {
+  .modal-body {
+    overflow: hidden;
+  }
+}
+
+.log-file-search-toolbar {
+  border: 1px solid #ddd;
+  margin-bottom: 20px;
+  overflow: hidden;
+  background: #e6f1f6;
+
+  .toolbar-row {
+    margin-bottom: 5px;
+
+    input {
+      margin: 0;
+    }
+    &:last-child {
+      padding-bottom: @toolbar-padding;
+      margin-bottom: 0;
+    }
+  }
+
+  .filter-block {
+    width: calc(~"100%" - @toolbar-context-menu-width + 4px);
+    padding: @toolbar-padding;
+    box-sizing: border-box;
+    margin-bottom: -400px;
+    padding-bottom: 400px;
+
+    input {
+      height: 16px;
+    }
+
+    .date-filter {
+      margin-left: 10px;
+
+      input {
+        width: 100px;
+      }
+    }
+  }
+
+  .levels-filter-block {
+    .level-checkbox {
+      margin-right: 20px;
+    }
+  }
+
+  .include-exclude-filter-block {
+    .keywords-list {
+      display: inline-block;
+
+      .keyword-item {
+        font-size: 12px;
+        display: inline-block;
+        padding: 2px;
+        border-radius: 0;
+
+        .close {
+          color: black;
+          font-size: 16px;
+          margin-left: 4px;
+        }
+      }
+    }
+  }
+
+  .context-menu {
+    width: @toolbar-context-menu-width;
+    border-left: 2px solid #ddd;
+    margin-bottom: -400px;
+    padding-bottom: 400px;
+    text-align: center;
+
+    .icon-external-link,
+    .move-to-top,
+    .move-to-bottom {
+      display: block;
+      font-size: 28px;
+      color: grey;
+    }
+
+    .move-to-top {
+      line-height: 20px;
+    }
+    .move-to-bottom {
+      line-height: 18px;
+    }
+
+    .icon-external-link {
+      margin-top: 5px;
+      font-size: 20px;
+      padding-left: 4px;
+    }
+  }
+}
+
+.log-file-search-content {
+  &.container {
+    width: 100%;
+    height: 300px;
+    overflow-y: auto;
+    border: 1px solid #ddd;
+  }
+
+  .log-data-item {
+    border-bottom: 1px solid #ddd;
+
+    &:nth-child(2n) {
+      background: #f2f2f2;
+    }
+
+    .log-data-date {
+      border-right: 2px solid #ddd;
+      width: 7%;
+      padding-left: 4px;
+    }
+    .log-data-message {
+      width: 91%;
+    }
+  }
+
+  #infinite-scroll-append {
+    text-align: center;
+    margin-top: 10px;
+
+    .icon-spinner {
+      font-size: 33px;
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/3a2add5d/ambari-web/app/templates/common/log_file_search.hbs
----------------------------------------------------------------------
diff --git a/ambari-web/app/templates/common/log_file_search.hbs b/ambari-web/app/templates/common/log_file_search.hbs
new file mode 100644
index 0000000..a874df6
--- /dev/null
+++ b/ambari-web/app/templates/common/log_file_search.hbs
@@ -0,0 +1,109 @@
+{{!
+* 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-file-search-toolbar">
+  <div class="filter-block pull-left">
+    <div class="toolbar-row">
+      <div class="display-inline-block">
+        <div class="display-inline-block">{{t common.keywords}}:</div>
+        {{view view.keywordsFilterView classNames="display-inline-block" valueBinding="view.keywordsFilterValue"}}
+        <button {{action submitKeywordsValue target="view"}} class="btn btn-primary display-inline-block">Search</button>
+      </div>
+      <div class="display-inline-block date-filter">
+        <div class="display-inline-block date-filter-item">
+          {{t from}}:
+          {{view App.DatepickerFieldView valueBinding="view.dateFromValue"}}
+        </div>
+        <div class="display-inline-block date-filter-item">
+          {{t to}}:
+          {{view App.DatepickerFieldView valueBinding="view.dateToValue"}}
+        </div>
+      </div>
+      <div class="pull-right">
+        <a {{action resetKeywordsDateFilter target="view"}} href="#" class="reset-link">{{t common.reset}}</a>
+      </div>
+    </div>
+    <div class="clearfix"></div>
+    <div class="toolbar-row levels-filter-block">
+      <div class="display-inline-block">{{t common.levels}}:</div>
+      {{#each level in view.levelsContext}}
+        <div class="display-inline-block level-checkbox">
+          <label>
+            {{view Em.Checkbox checkedBinding="level.checked"}}
+            {{level.displayName}} ({{level.counter}})
+          </label>
+        </div>
+      {{/each}}
+      <div class="pull-right">
+        <a {{action resetLevelsFilter target="view"}} href="#" class="reset-link">{{t common.reset}}</a>
+      </div>
+    </div>
+    <div class="toolbar-row include-exclude-filter-block">
+      <div class="display-inline-block">
+        {{t common.filters}}:
+        <div class="keywords-list">
+          {{#each keyword in view.selectedKeywords}}
+            <div {{bindAttr class=":keyword-item :btn keyword.isIncluded:btn-success:btn-danger"}}>
+              {{#if keyword.isIncluded}}
+                {{t common.include.short}}:
+              {{else}}
+                {{t common.exclude.short}}:
+              {{/if}}
+              <span> '{{keyword.value}}'</span>
+              <a {{action removeKeyword keyword target="view"}} class="close" href="#">x</a>
+            </div>
+          {{/each}}
+        </div>
+      </div>
+      <div class="pull-right">
+        <a {{action resetKeywordsFilter target="view"}} href="#" class="reset-link">{{t common.reset}}</a>
+      </div>
+    </div>
+  </div>
+  <div class="context-menu pull-left">
+    <a href="#" {{action moveTableTop target="view"}} class="move-to-top icon-caret-up"></a>
+    <a href="#" {{action moveTableBottom target="view"}} class="move-to-bottom icon-caret-down"></a>
+    <a href="#" {{action navigateToLogUI target="view"}} class="icon-external-link"></a>
+  </div>
+  <div class="clearfix"></div>
+</div>
+<div class="log-file-search-content container">
+  {{#each logData in view.content}}
+    <div class="row-fluid log-data-item">
+      <div class="span1 log-data-date">
+        {{logData.date}}
+        <br />
+        <span class="log-data-level">
+          {{logData.level}}
+        </span>
+      </div>
+      <div class="span11 log-data-message">{{logData.message}}</div>
+      <div class="clearfix"></div>
+    </div>
+  {{/each}}
+</div>
+<div id="log-file-search-item-context-menu">
+  <ul class="dropdown-menu" role="menu">
+    <li>
+      <a {{action includeSelected target="view"}} href="#">{{t common.include}}</a>
+    </li>
+    <li>
+      <a {{action excludeSelected target="view"}} href="#">{{t common.exclude}}</a>
+    </li>
+  </ul>
+</div>

http://git-wip-us.apache.org/repos/asf/ambari/blob/3a2add5d/ambari-web/app/views.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/views.js b/ambari-web/app/views.js
index afb12e1..4404a2a 100644
--- a/ambari-web/app/views.js
+++ b/ambari-web/app/views.js
@@ -20,6 +20,7 @@
 // load all views here
 
 require('views/application');
+require('views/common/log_file_search_view');
 require('views/common/global/spinner');
 require('views/common/ajax_default_error_popup_body');
 require('views/common/chart');
@@ -37,6 +38,7 @@ require('views/common/modal_popups/invalid_KDC_popup');
 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/editable_list');
 require('views/common/host_progress_popup_body_view');
 require('views/common/rolling_restart_view');
@@ -45,6 +47,7 @@ require('views/common/metric');
 require('views/common/time_range');
 require('views/common/time_range_list');
 require('views/common/form/field');
+require('views/common/form/datepicker_view');
 require('views/common/form/spinner_input_view');
 require('views/common/form/manage_credentials_form_view');
 require('views/common/quick_view_link_view');

http://git-wip-us.apache.org/repos/asf/ambari/blob/3a2add5d/ambari-web/app/views/common/form/datepicker_view.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/views/common/form/datepicker_view.js b/ambari-web/app/views/common/form/datepicker_view.js
new file mode 100644
index 0000000..b5bd104
--- /dev/null
+++ b/ambari-web/app/views/common/form/datepicker_view.js
@@ -0,0 +1,37 @@
+/*
+ * 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.DatepickerFieldView = Em.TextField.extend({
+  value: '',
+  dateFormat: 'dd/mm/yy',
+  maxDate: new Date(),
+
+  didInsertElement: function() {
+    this.$().datetimepicker({
+      format: this.get('dateFormat'),
+      endDate: this.get('maxDate')
+    });
+    this.$().on('changeDate', this.onChangeDate.bind(this));
+  },
+
+  onChangeDate: function(e) {
+    this.set('value', e.target.value);
+  }
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/3a2add5d/ambari-web/app/views/common/log_file_search_view.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/views/common/log_file_search_view.js b/ambari-web/app/views/common/log_file_search_view.js
new file mode 100644
index 0000000..c242ec8
--- /dev/null
+++ b/ambari-web/app/views/common/log_file_search_view.js
@@ -0,0 +1,272 @@
+/**
+ * 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 filters = require('views/common/filter_view');
+
+/**
+* @augments App.InfiniteScrollMixin
+* @type {Em.View}
+*/
+App.LogFileSearchView = Em.View.extend(App.InfiniteScrollMixin, {
+  classNames: ['log-file-search'],
+  templateName: require('templates/common/log_file_search'),
+  logLevels: ['fatal', 'critical', 'error', 'warning', 'info', 'debug'],
+
+  /**
+  * @typedef {Em.Object} FilterKeyword
+  * @property {Boolean} isIncluded determines include/exclude status of keyword
+  * @property {String} id unique identifier
+  * @property {String} value keyword value
+  */
+
+  /**
+  * Stores all selected keywords.
+  *
+  * @type {FilterKeyword[]}
+  */
+  selectedKeywords: [],
+
+  selectedKeywordsDidChange: function() {
+    this.fetchContent();
+  }.observes('selectedKeywords.length'),
+
+  levelsContext: function() {
+    var self = this;
+    var levels = this.get('logLevels');
+
+    return Em.A(levels.map(function(level) {
+      return Em.Object.create({name: level.toUpperCase(), counter: 0, displayName: level.capitalize(), checked: false});
+    }));
+  }.property(),
+
+  levelsContextDidChange: function(e) {
+    this.fetchContent();
+  }.observes('levelsContext.@each.checked'),
+
+  /** mock data **/
+  content: function() {
+    var data = [{
+      message: 'java.lang.NullPointerException',
+      date: '05.12.2016, 10:10:20',
+      level: 'INFO'
+    },
+    {
+      message: 'java.lang.NullPointerException',
+      date: '05.12.2016, 10:10:20',
+      level: 'ERROR'
+    }];
+
+    var initialSize = 20;
+    var ret = [];
+
+    for (var i = 0; i < 20; i++) {
+      ret.push(Em.Object.create(data[Math.ceil(Math.random()*2) - 1]));
+    }
+    return ret;
+  }.property(),
+
+  contentDidChange: function() {
+    this.refreshLevelCounters();
+  }.observes('content.length'),
+
+  dateFromValue: null,
+  dateToValue: null,
+
+  keywordsFilterView: filters.createTextView({
+    layout: Em.Handlebars.compile('{{yield}}')
+  }),
+
+  keywordsFilterValue: null,
+
+  didInsertElement: function() {
+    this._super();
+    this.infiniteScrollInit(this.$().find('.log-file-search-content'), {
+      callback: this.loadMore.bind(this)
+    });
+    this.$().find('.log-file-search-content').contextmenu({
+      target: '#log-file-search-item-context-menu'
+    });
+    this.refreshLevelCounters();
+  },
+
+  /** mock data **/
+  loadMore: function() {
+    var dfd = $.Deferred();
+    var self = this;
+    setTimeout(function() {
+      var data = self.get('content');
+      self.get('content').pushObjects(data.slice(0, 10));
+      dfd.resolve();
+    }, Math.ceil(Math.random()*4000));
+    return dfd.promise();
+  },
+
+  refreshLevelCounters: function() {
+    var self = this;
+    this.get('logLevels').forEach(function(level) {
+      var levelContext = self.get('levelsContext').findProperty('name', level.toUpperCase());
+      levelContext.set('counter', self.get('content').filterProperty('level', level.toUpperCase()).length);
+    });
+  },
+
+  /**
+  * Make request and get content with applied filters.
+  */
+  fetchContent: function(params) {
+    console.debug('Make Request with params:', this.serializeFilters());
+  },
+
+  submitKeywordsValue: function() {
+    this.fetchContent();
+  },
+
+  serializeFilters: function() {
+    var levels = this.serializeLevelFilters();
+    var keywords = this.serializeKeywordsFilter();
+    var date = this.serializeDateFilter();
+    var includedExcludedKeywords = this.serializeIncludedExcludedKeywordFilter();
+
+    return [levels, keywords, date, includedExcludedKeywords].compact().join('&');
+  },
+
+  serializeKeywordsFilter: function() {
+    return !!this.get('keywordsFilterValue') ? 'keywords=' + this.get('keywordsFilterValue'): null;
+  },
+
+  serializeDateFilter: function() {
+    var dateFrom = !!this.get('dateFromValue') ? 'dateFrom=' + this.get('dateFromValue') : null;
+    var dateTo = !!this.get('dateToValue') ? 'dateTo=' + this.get('dateFromValue') : null;
+    var ret = [dateTo, dateFrom].compact();
+    return ret.length ? ret.join('&') : null;
+  },
+
+  serializeLevelFilters: function() {
+    var selectedLevels = this.get('levelsContext').filterProperty('checked').mapProperty('name');
+    return selectedLevels.length ? 'levels=' + selectedLevels.join(',') : null;
+  },
+
+  serializeIncludedExcludedKeywordFilter: function() {
+    var self = this;
+    var getValues = function(included) {
+      return self.get('selectedKeywords').filterProperty('isIncluded', included).mapProperty('value');
+    };
+    var included = getValues(true).join(',');
+    var excluded = getValues(false).join(',');
+    var ret = [];
+    if (included.length) ret.push('include=' + included);
+    if (excluded.length) ret.push('exclude=' + excluded);
+    return ret.length ? ret.join('&') : null;
+  },
+
+  /** include/exclude keywords methods **/
+
+  keywordToId: function(keyword) {
+    return keyword.toLowerCase().split(' ').join('_');
+  },
+
+  /**
+  * Create keyword object
+  * @param  {string} keyword keyword value
+  * @param  {object} [opts]
+  * @return {Em.Object}
+  */
+  createSelectedKeyword: function(keyword, opts) {
+    var defaultOpts = {
+      isIncluded: false,
+      id: this.keywordToId(keyword),
+      value: keyword
+    };
+    return Em.Object.create($.extend({}, defaultOpts, opts));
+  },
+
+  /**
+  * Adds keyword if not added.
+  * @param  {FilterKeyword} keywordObject
+  */
+  addKeywordToList: function(keywordObject) {
+    if (!this.get('selectedKeywords').someProperty('id', keywordObject.get('id'))) {
+      this.get('selectedKeywords').pushObject(keywordObject);
+    }
+  },
+
+  /**
+  * @param  {FilterKeyword} keyword
+  */
+  includeSelectedKeyword: function(keyword) {
+    this.addKeywordToList(this.createSelectedKeyword(keyword, { isIncluded: true }));
+  },
+
+  /**
+  * @param  {FilterKeyword} keyword
+  */
+  excludeSelectedKeyword: function(keyword) {
+    this.addKeywordToList(this.createSelectedKeyword(keyword, { isIncluded: false }));
+  },
+
+  /** view actions **/
+
+  /** toolbar context menu actions **/
+  moveTableTop: function(e) {
+    var $el = $('.log-file-search-content');
+    $el.scrollTop(0);
+    $el = null;
+  },
+
+  moveTableBottom: function(e) {
+    var $el = $('.log-file-search-content');
+    $el.scrollTop($el.get(0).scrollHeight);
+    $el = null;
+  },
+
+  navigateToLogUI: function(e) {
+    console.error('navigate to Log UI');
+  },
+
+  removeKeyword: function(e) {
+    this.get('selectedKeywords').removeObject(e.context);
+  },
+
+  /** toolbar reset filter actions **/
+  resetKeywordsDateFilter: function(e) {
+    this.setProperties({
+      keywordsFilterValue: '',
+      dateFromValue: '',
+      dateToValue: ''
+    });
+  },
+
+  resetLevelsFilter: function(e) {
+    this.get('levelsContext').invoke('set', 'checked', false);
+  },
+
+  resetKeywordsFilter: function(e) {
+    this.get('selectedKeywords').clear();
+  },
+
+  /** log search item context menu actions **/
+  includeSelected: function() {
+    var selection = window.getSelection().toString();
+    if (!!selection) this.includeSelectedKeyword(selection);
+  },
+
+  excludeSelected: function() {
+    var selection = window.getSelection().toString();
+    if (!!selection) this.excludeSelectedKeyword(selection);
+  }
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/3a2add5d/ambari-web/app/views/common/modal_popups/log_file_search_popup.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/views/common/modal_popups/log_file_search_popup.js b/ambari-web/app/views/common/modal_popups/log_file_search_popup.js
new file mode 100644
index 0000000..4730a19
--- /dev/null
+++ b/ambari-web/app/views/common/modal_popups/log_file_search_popup.js
@@ -0,0 +1,26 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var App = require('app');
+
+App.LogFileSearchPopup = function(header) {
+  return App.ModalPopup.show({
+    classNames: ['modal-full-width', 'sixty-percent-width-modal', 'log-file-search-popup'],
+    bodyClass: App.LogFileSearchView.extend({})
+  });
+};

http://git-wip-us.apache.org/repos/asf/ambari/blob/3a2add5d/ambari-web/test/views/common/log_file_search_view_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/views/common/log_file_search_view_test.js b/ambari-web/test/views/common/log_file_search_view_test.js
new file mode 100644
index 0000000..ca208b3
--- /dev/null
+++ b/ambari-web/test/views/common/log_file_search_view_test.js
@@ -0,0 +1,103 @@
+/**
+ * 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');
+
+describe('App.LogFileSearchView', function() {
+  describe('#serializeFilters', function() {
+    var makeLevelItem = function(level, isChecked) {
+      return Em.Object.create({
+        name: level.toUpperCase(),
+        checked: !!isChecked
+      });
+    };
+    var makeSelectedKeyword = function(keyword, isIncluded) {
+      return Em.Object.create({
+        value: keyword,
+        isIncluded: !!isIncluded
+      });
+    };
+    var cases = [
+      {
+        viewContent: {
+          keywordsFilterValue: 'some_keyword'
+        },
+        e: 'keywords=some_keyword'
+      },
+      {
+        viewContent: {
+          keywordsFilterValue: 'some_keyword',
+          levelsContext: [
+            makeLevelItem('debug', true),
+            makeLevelItem('error', false),
+            makeLevelItem('info', true)
+          ]
+        },
+        e: 'levels=DEBUG,INFO&keywords=some_keyword'
+      },
+      {
+        viewContent: {
+          keywordsFilterValue: 'some_keyword',
+          dateFromValue: '12/12/2015',
+          dateToValue: '14/12/2015',
+          levelsContext: [
+            makeLevelItem('debug', true),
+            makeLevelItem('error', true),
+            makeLevelItem('info', true)
+          ]
+        },
+        e: 'levels=DEBUG,ERROR,INFO&keywords=some_keyword&dateTo=12/12/2015&dateFrom=12/12/2015'
+      },
+      {
+        viewContent: {
+          keywordsFilterValue: 'some_keyword',
+          dateFromValue: '12/12/2015',
+          levelsContext: [
+            makeLevelItem('debug', true),
+            makeLevelItem('error', true),
+            makeLevelItem('info', true)
+          ]
+        },
+        e: 'levels=DEBUG,ERROR,INFO&keywords=some_keyword&dateFrom=12/12/2015'
+      },
+      {
+        viewContent: {
+          keywordsFilterValue: 'some_keyword',
+          dateFromValue: '12/12/2015',
+          levelsContext: [
+            makeLevelItem('debug', true),
+            makeLevelItem('error', true),
+            makeLevelItem('info', true)
+          ],
+          selectedKeywords: [
+            makeSelectedKeyword("keyword1", true),
+            makeSelectedKeyword("keyword2", true),
+            makeSelectedKeyword("keyword3", false)
+          ]
+        },
+        e: 'levels=DEBUG,ERROR,INFO&keywords=some_keyword&dateFrom=12/12/2015&include=keyword1,keyword2&exclude=keyword3'
+      }
+    ].forEach(function(test) {
+      it('validate result: ' + test.e, function() {
+        var view = App.LogFileSearchView.extend(test.viewContent).create();
+        expect(view.serializeFilters()).to.be.eql(test.e);
+        view.destroy();
+      })
+    });
+  });
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/3a2add5d/ambari-web/vendor/scripts/bootstrap-contextmenu.js
----------------------------------------------------------------------
diff --git a/ambari-web/vendor/scripts/bootstrap-contextmenu.js b/ambari-web/vendor/scripts/bootstrap-contextmenu.js
new file mode 100644
index 0000000..62a60cd
--- /dev/null
+++ b/ambari-web/vendor/scripts/bootstrap-contextmenu.js
@@ -0,0 +1,205 @@
+/*!
+ * Bootstrap Context Menu
+ * Author: @sydcanem
+ * https://github.com/sydcanem/bootstrap-contextmenu
+ *
+ * Inspired by Bootstrap's dropdown plugin.
+ * Bootstrap (http://getbootstrap.com).
+ *
+ * Licensed under MIT
+ * ========================================================= */
+
+;(function($) {
+
+    'use strict';
+
+    /* CONTEXTMENU CLASS DEFINITION
+     * ============================ */
+    var toggle = '[data-toggle="context"]';
+
+    var ContextMenu = function (element, options) {
+        this.$element = $(element);
+
+        this.before = options.before || this.before;
+        this.onItem = options.onItem || this.onItem;
+        this.scopes = options.scopes || null;
+
+        if (options.target) {
+            this.$element.data('target', options.target);
+        }
+
+        this.listen();
+    };
+
+    ContextMenu.prototype = {
+
+        constructor: ContextMenu
+        ,show: function(e) {
+
+            var $menu
+                , evt
+                , tp
+                , items
+                , relatedTarget = { relatedTarget: this, target: e.currentTarget };
+
+            if (this.isDisabled()) return;
+
+            this.closemenu();
+
+            if (this.before.call(this,e,$(e.currentTarget)) === false) return;
+
+            $menu = this.getMenu();
+            $menu.trigger(evt = $.Event('show.bs.context', relatedTarget));
+
+            tp = this.getPosition(e, $menu);
+            items = 'li:not(.divider)';
+            $menu.attr('style', '')
+                .css(tp)
+                .addClass('open')
+                .on('click.context.data-api', items, $.proxy(this.onItem, this, $(e.currentTarget)))
+                .trigger('shown.bs.context', relatedTarget);
+
+            // Delegating the `closemenu` only on the currently opened menu.
+            // This prevents other opened menus from closing.
+            $('html')
+                .on('click.context.data-api', $menu.selector, $.proxy(this.closemenu, this));
+
+            return false;
+        }
+
+        ,closemenu: function(e) {
+            var $menu
+                , evt
+                , items
+                , relatedTarget;
+
+            $menu = this.getMenu();
+
+            if(!$menu.hasClass('open')) return;
+
+            relatedTarget = { relatedTarget: this };
+            $menu.trigger(evt = $.Event('hide.bs.context', relatedTarget));
+
+            items = 'li:not(.divider)';
+            $menu.removeClass('open')
+                .off('click.context.data-api', items)
+                .trigger('hidden.bs.context', relatedTarget);
+
+            $('html')
+                .off('click.context.data-api', $menu.selector);
+            // Don't propagate click event so other currently
+            // opened menus won't close.
+            e.stopPropagation();
+        }
+
+        ,keydown: function(e) {
+            if (e.which == 27) this.closemenu(e);
+        }
+
+        ,before: function(e) {
+            return true;
+        }
+
+        ,onItem: function(e) {
+            return true;
+        }
+
+        ,listen: function () {
+            this.$element.on('contextmenu.context.data-api', this.scopes, $.proxy(this.show, this));
+            $('html').on('click.context.data-api', $.proxy(this.closemenu, this));
+            $('html').on('keydown.context.data-api', $.proxy(this.keydown, this));
+        }
+
+        ,destroy: function() {
+            this.$element.off('.context.data-api').removeData('context');
+            $('html').off('.context.data-api');
+        }
+
+        ,isDisabled: function() {
+            return this.$element.hasClass('disabled') ||
+                    this.$element.attr('disabled');
+        }
+
+        ,getMenu: function () {
+            var selector = this.$element.data('target')
+                , $menu;
+
+            if (!selector) {
+                selector = this.$element.attr('href');
+                selector = selector && selector.replace(/.*(?=#[^\s]*$)/, ''); //strip for ie7
+            }
+
+            $menu = $(selector);
+
+            return $menu && $menu.length ? $menu : this.$element.find(selector);
+        }
+
+        ,getPosition: function(e, $menu) {
+            var mouseX = e.clientX
+                , mouseY = e.clientY
+                , boundsX = $(window).width()
+                , boundsY = $(window).height()
+                , menuWidth = $menu.find('.dropdown-menu').outerWidth()
+                , menuHeight = $menu.find('.dropdown-menu').outerHeight()
+                , tp = {"position":"absolute","z-index":9999}
+                , Y, X, parentOffset;
+
+            if (mouseY + menuHeight > boundsY) {
+                Y = {"top": mouseY - menuHeight + $(window).scrollTop()};
+            } else {
+                Y = {"top": mouseY + $(window).scrollTop()};
+            }
+
+            if ((mouseX + menuWidth > boundsX) && ((mouseX - menuWidth) > 0)) {
+                X = {"left": mouseX - menuWidth + $(window).scrollLeft()};
+            } else {
+                X = {"left": mouseX + $(window).scrollLeft()};
+            }
+
+            // If context-menu's parent is positioned using absolute or relative positioning,
+            // the calculated mouse position will be incorrect.
+            // Adjust the position of the menu by its offset parent position.
+            parentOffset = $menu.offsetParent().offset();
+            X.left = X.left - parentOffset.left;
+            Y.top = Y.top - parentOffset.top;
+
+            return $.extend(tp, Y, X);
+        }
+
+    };
+
+    /* CONTEXT MENU PLUGIN DEFINITION
+     * ========================== */
+
+    $.fn.contextmenu = function (option,e) {
+        return this.each(function () {
+            var $this = $(this)
+                , data = $this.data('context')
+                , options = (typeof option == 'object') && option;
+
+            if (!data) $this.data('context', (data = new ContextMenu($this, options)));
+            if (typeof option == 'string') data[option].call(data, e);
+        });
+    };
+
+    $.fn.contextmenu.Constructor = ContextMenu;
+
+    /* APPLY TO STANDARD CONTEXT MENU ELEMENTS
+     * =================================== */
+
+    $(document)
+       .on('contextmenu.context.data-api', function() {
+            $(toggle).each(function () {
+                var data = $(this).data('context');
+                if (!data) return;
+                data.closemenu();
+            });
+        })
+        .on('contextmenu.context.data-api', toggle, function(e) {
+            $(this).contextmenu('show', e);
+
+            e.preventDefault();
+            e.stopPropagation();
+        });
+
+}(jQuery));


[2/2] ambari git commit: AMBARI-14927. Log Search: Add Host Components Logs tab to Host Details page (alexantonenko)

Posted by al...@apache.org.
AMBARI-14927. Log Search: Add Host Components Logs tab to Host Details page
 (alexantonenko)


Project: http://git-wip-us.apache.org/repos/asf/ambari/repo
Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/b567ca44
Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/b567ca44
Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/b567ca44

Branch: refs/heads/trunk
Commit: b567ca441c213c816019dc5a45884f69a286f54c
Parents: 3a2add5
Author: Alex Antonenko <hi...@gmail.com>
Authored: Fri Feb 5 18:30:24 2016 +0200
Committer: Alex Antonenko <hi...@gmail.com>
Committed: Fri Feb 5 18:30:24 2016 +0200

----------------------------------------------------------------------
 .../app/controllers/global/update_controller.js |   4 +-
 ambari-web/app/messages.js                      |   3 +
 ambari-web/app/routes/main.js                   |  11 ++
 ambari-web/app/styles/application.less          |   3 +-
 ambari-web/app/templates/main/host/logs.hbs     |  62 ++++++++
 ambari-web/app/views.js                         |   1 +
 ambari-web/app/views/main/host/logs_view.js     | 147 +++++++++++++++++++
 ambari-web/app/views/main/host/menu.js          |  11 +-
 ambari-web/test/views/main/host/menu_test.js    |  20 ++-
 9 files changed, 257 insertions(+), 5 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/b567ca44/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 0a5d913..0168398 100644
--- a/ambari-web/app/controllers/global/update_controller.js
+++ b/ambari-web/app/controllers/global/update_controller.js
@@ -223,7 +223,7 @@ App.UpdateController = Em.Controller.extend({
     }
     else {
       if (App.router.get('currentState.parentState.name') == 'hostDetails') {
-        hostDetailsFilter = App.router.get('location.lastSetURL').match(/\/hosts\/(.*)\/(summary|configs|alerts|stackVersions)/)[1];
+        hostDetailsFilter = App.router.get('location.lastSetURL').match(/\/hosts\/(.*)\/(summary|configs|alerts|stackVersions|logs)/)[1];
         App.updater.updateInterval('updateHost', App.get('componentsUpdateInterval'));
         //if host details page opened then request info only of one displayed host
         this.get('queryParams').set('Hosts', [
@@ -579,7 +579,7 @@ App.UpdateController = Em.Controller.extend({
       complete: callback
     });
   },
-  
+
   updateUpgradeState: function (callback) {
     var currentStateName = App.get('router.currentState.name'),
       parentStateName = App.get('router.currentState.parentState.name'),

http://git-wip-us.apache.org/repos/asf/ambari/blob/b567ca44/ambari-web/app/messages.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/messages.js b/ambari-web/app/messages.js
index 8d1f6f1..a8b5db6 100644
--- a/ambari-web/app/messages.js
+++ b/ambari-web/app/messages.js
@@ -312,6 +312,7 @@ Em.I18n.translations = {
   'common.filters': 'Filters',
   'common.keywords': 'Keywods',
   'common.levels': 'Levels',
+  'common.extension': 'Extension',
 
   'models.alert_instance.tiggered.verbose': "Occurred on {0} <br> Checked on {1}",
   'models.alert_definition.triggered.verbose': "Occurred on {0}",
@@ -2280,6 +2281,8 @@ Em.I18n.translations = {
 
   'hosts.host.serviceNotAvailable': 'Service not available on this host',
 
+  'hosts.host.menu.logs': 'Logs',
+
   'hosts.host.menu.stackVersions': 'Versions',
   'hosts.host.stackVersions.table.allVersions': 'All Versions',
   'hosts.host.stackVersions.table.allNames': 'All Names',

http://git-wip-us.apache.org/repos/asf/ambari/blob/b567ca44/ambari-web/app/routes/main.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/routes/main.js b/ambari-web/app/routes/main.js
index bd66945..cbaf34d 100644
--- a/ambari-web/app/routes/main.js
+++ b/ambari-web/app/routes/main.js
@@ -264,6 +264,17 @@ module.exports = Em.Route.extend(App.RouterRedirections, {
         }
       }),
 
+      logs: Em.Route.extend({
+        route: '/logs',
+        connectOutlets: function (router, context) {
+          if (App.get('supports.logSearch')) {
+            router.get('mainHostDetailsController').connectOutlet('mainHostLogs')
+          } else {
+            router.transitionTo('summary');
+          }
+        }
+      }),
+
       hostNavigate: function (router, event) {
         var parent = event.view._parentView;
         parent.deactivateChildViews();

http://git-wip-us.apache.org/repos/asf/ambari/blob/b567ca44/ambari-web/app/styles/application.less
----------------------------------------------------------------------
diff --git a/ambari-web/app/styles/application.less b/ambari-web/app/styles/application.less
index ce10a98..a1969db 100644
--- a/ambari-web/app/styles/application.less
+++ b/ambari-web/app/styles/application.less
@@ -3700,7 +3700,8 @@ table.graphs {
       }
     }
   }
-  .host-tab-content {
+  .host-tab-content,
+  .logs-tab-content {
     margin-top: 20px;
     .table thead {
       background: none repeat scroll 0 0 #F8F8F8;

http://git-wip-us.apache.org/repos/asf/ambari/blob/b567ca44/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
new file mode 100644
index 0000000..000b4aa
--- /dev/null
+++ b/ambari-web/app/templates/main/host/logs.hbs
@@ -0,0 +1,62 @@
+{{!
+* 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.
+}}
+
+<table class="table advanced-header-table table-bordered table-striped">
+  <thead>
+    {{#view view.sortView classNames="label-row" contentBinding="view.filteredContent"}}
+      {{view view.parentView.serviceNameSort classNames="first"}}
+      {{view view.parentView.componentNameSort}}
+      {{view view.parentView.fileExtensionsSort}}
+    {{/view}}
+    <tr class="filter-row">
+      <th class="first service-name-filter">{{view view.serviceNameFilterView}}</th>
+      <th class="component-name-filter">{{view view.componentNameFilterView}}</th>
+      <th class="file-extension-filter">{{view view.fileExtensionsFilter}}</th>
+    </tr>
+  </thead>
+  <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>
+      {{/each}}
+    {{/if}}
+  </tbody>
+</table>
+
+<div class="page-bar">
+  <div class="filtered-info span4">
+    {{#if view.showFilteredContent}}
+      <label>{{view.filteredContentInfo}} - <a {{action clearFilters target="view"}}
+        href="#">{{t tableView.filters.clearAllFilters}}</a></label>
+    {{/if}}
+  </div>
+  <div class="items-on-page">
+    <label>{{t common.show}}: {{view view.rowsPerPageSelectView selectionBinding="view.parentView.displayLength"}}</label>
+  </div>
+  <div class="info">{{view.paginationInfo}}</div>
+  <div class="paging_two_button">
+    {{view view.paginationLeft}}
+    {{view view.paginationRight}}
+  </div>
+</div>

http://git-wip-us.apache.org/repos/asf/ambari/blob/b567ca44/ambari-web/app/views.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/views.js b/ambari-web/app/views.js
index 4404a2a..2224393 100644
--- a/ambari-web/app/views.js
+++ b/ambari-web/app/views.js
@@ -122,6 +122,7 @@ require('views/main/host/details/host_component_views/datanode_view');
 require('views/main/host/details/host_component_views/nodemanager_view');
 require('views/main/host/details/host_component_views/regionserver_view');
 require('views/main/host/details/host_component_views/tasktracker_view');
+require('views/main/host/logs_view');
 require('views/main/host/menu');
 require('views/main/host/summary');
 require('views/main/host/configs');

http://git-wip-us.apache.org/repos/asf/ambari/blob/b567ca44/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
new file mode 100644
index 0000000..dfe00e4
--- /dev/null
+++ b/ambari-web/app/views/main/host/logs_view.js
@@ -0,0 +1,147 @@
+/**
+ * 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 filters = require('views/common/filter_view');
+var sort = require('views/common/sort_view');
+
+App.MainHostLogsView = App.TableView.extend({
+  templateName: require('templates/main/host/logs'),
+
+  classNames: ['logs-tab-content'],
+
+  /**
+   * @type {Ember.Object}
+   */
+  host: Em.computed.alias('App.router.mainHostDetailsController.content'),
+
+  content: function() {
+    return [
+      Em.Object.create({
+        serviceName: 'HDFS',
+        componentName: 'DATANODE',
+        fileExtension: '.log',
+        fileName: 'HDFS_DATANODE.log'
+      })
+    ];
+  }.property(),
+  /**
+   * @type {Ember.View}
+   */
+  sortView: sort.wrapperView,
+
+  serviceNameSort: sort.fieldView.extend({
+    column: 1,
+    name: 'serviceName',
+    displayName: Em.I18n.t('common.service')
+  }),
+
+  componentNameSort: sort.fieldView.extend({
+    column: 2,
+    name: 'componentName',
+    displayName: Em.I18n.t('common.component')
+  }),
+
+  fileExtensionsSort: sort.fieldView.extend({
+    column: 3,
+    name: 'extension',
+    displayName: Em.I18n.t('common.extension')
+  }),
+
+  serviceNameFilterView: filters.createSelectView({
+    column: 1,
+    fieldType: 'filter-input-width',
+    content: function() {
+      return [{
+        value: '',
+        label: Em.I18n.t('common.all')
+      }].concat(App.Service.find().mapProperty('serviceName').uniq().map(function(item) {
+        return {
+          value: item,
+          label: item
+        }
+      }));
+    }.property('App.router.clusterController.isLoaded'),
+    onChangeValue: function() {
+      this.get('parentView').updateFilter(this.get('column'), this.get('value'), 'select');
+    }
+  }),
+
+  componentNameFilterView: filters.createSelectView({
+    column: 2,
+    fieldType: 'filter-input-width',
+    content: function() {
+      var hostName = this.get('parentView').get('host.hostName'),
+        hostComponents = App.HostComponent.find().filterProperty('hostName', hostName),
+        componentsMap = hostComponents.map(function(item) {
+          return {
+            value: item.get('componentName'),
+            label: item.get('displayName')
+          }
+        });
+      return [{
+        value: '',
+        label: Em.I18n.t('common.all')
+      }].concat(componentsMap);
+    }.property('App.router.clusterController.isLoaded'),
+    onChangeValue: function() {
+      this.get('parentView').updateFilter(this.get('column'), this.get('value'), 'select');
+    }
+  }),
+
+  fileExtensionsFilter: filters.createSelectView({
+    column: 3,
+    fieldType: 'filter-input-width',
+    content: function() {
+      return [{
+        value: '',
+        label: Em.I18n.t('common.all')
+      }].concat([
+        '.out',
+         '.log'
+       ].map(function(item) {
+        return {
+          value: item,
+          label: item
+        }
+      }))
+    }.property('App.router.clusterController.isLoaded'),
+    onChangeValue: function() {
+      this.get('parentView').updateFilter(this.get('column'), this.get('value'), 'select');
+    }
+  }),
+
+  /**
+   * @type {string[]}
+   */
+  colPropAssoc: function () {
+    var ret = [];
+    ret[1] = 'serviceName';
+    ret[2] = 'componentName';
+    ret[3] = 'fileExtension';
+    return ret;
+  }.property(),
+
+  openLogFile: function(e) {
+    var fileData = e.context;
+    if (e.context) {
+      App.LogFileSearchPopup(fileData.fileName);
+    }
+  }
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/b567ca44/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 15608d1..a53c21a 100644
--- a/ambari-web/app/views/main/host/menu.js
+++ b/ambari-web/app/views/main/host/menu.js
@@ -53,6 +53,15 @@ App.MainHostMenuView = Em.CollectionView.extend({
           return !App.get('supports.stackUpgrade') || !App.get('stackVersionsAvailable')
         }.property('App.supports.stackUpgrade'),
         id: 'host-details-summary-version'
+      }),
+      Em.Object.create({
+        name: 'logs',
+        label: Em.I18n.t('hosts.host.menu.logs'),
+        routing: 'logs',
+        hidden: function () {
+          return !App.get('supports.logSearch');
+        }.property('App.supports.logSearch'),
+        id: 'host-details-summary-logs'
       })
     ];
   }.property('App.stackVersionsAvailable'),
@@ -104,4 +113,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/b567ca44/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 4540486..59facf1 100644
--- a/ambari-web/test/views/main/host/menu_test.js
+++ b/ambari-web/test/views/main/host/menu_test.js
@@ -69,6 +69,24 @@ describe('App.MainHostMenuView', function () {
         });
       });
 
+    Em.A([
+      {
+        logSearch: false,
+        m: '`logs` tab is invisible',
+        e: true
+      },
+      {
+        logSearch: true,
+        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);
+      });
+    });
   });
 
-});
\ No newline at end of file
+});