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

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

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/branch-dev-patch-upgrade
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));