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));