Browse Source

[AMBARI-24631] [Log Search UI] styles and layout fixes (#2339)

* [AMBARI-24631] [Log Search UI] styles and layout fixes - autorefresh styling

* [AMBARI-24631] [Log Search UI] styles and layout fixes

Change-Id: I6d233019638464cf39c7af038b1b350d242279e8

* [AMBARI-24631] [Log Search UI] styles and layout fixes - PR change requests

Change-Id: Id3434b605929fc9e0129eeea341cab25a3bf1180
Istvan Tobias 7 years ago
parent
commit
544b07e749
32 changed files with 557 additions and 148 deletions
  1. 1 1
      ambari-logsearch/ambari-logsearch-web/package.json
  2. 3 0
      ambari-logsearch/ambari-logsearch-web/src/app/classes/components/graph/graph.component.ts
  3. 1 1
      ambari-logsearch/ambari-logsearch-web/src/app/classes/components/graph/time-graph.component.less
  4. 6 0
      ambari-logsearch/ambari-logsearch-web/src/app/components/action-menu/action-menu.component.less
  5. 4 0
      ambari-logsearch/ambari-logsearch-web/src/app/components/action-menu/action-menu.component.ts
  6. 1 0
      ambari-logsearch/ambari-logsearch-web/src/app/components/collapsible-panel/collapsible-panel.component.html
  7. 2 0
      ambari-logsearch/ambari-logsearch-web/src/app/components/collapsible-panel/collapsible-panel.component.less
  8. 3 0
      ambari-logsearch/ambari-logsearch-web/src/app/components/log-index-filter/log-index-filter.component.less
  9. 2 2
      ambari-logsearch/ambari-logsearch-web/src/app/components/log-message/log-message.component.html
  10. 4 3
      ambari-logsearch/ambari-logsearch-web/src/app/components/log-message/log-message.component.spec.ts
  11. 19 22
      ambari-logsearch/ambari-logsearch-web/src/app/components/log-message/log-message.component.ts
  12. 29 12
      ambari-logsearch/ambari-logsearch-web/src/app/components/logs-container/logs-container.component.html
  13. 42 1
      ambari-logsearch/ambari-logsearch-web/src/app/components/logs-container/logs-container.component.less
  14. 3 1
      ambari-logsearch/ambari-logsearch-web/src/app/components/logs-container/logs-container.component.spec.ts
  15. 22 2
      ambari-logsearch/ambari-logsearch-web/src/app/components/logs-container/logs-container.component.ts
  16. 11 6
      ambari-logsearch/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.html
  17. 13 0
      ambari-logsearch/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.less
  18. 121 71
      ambari-logsearch/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.ts
  19. 3 0
      ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.html
  20. 3 0
      ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.less
  21. 1 1
      ambari-logsearch/ambari-logsearch-web/src/app/components/time-range-picker/time-range-picker.component.html
  22. 32 0
      ambari-logsearch/ambari-logsearch-web/src/app/modules/shared/components/circle-progress-bar/circle-progress-bar.component.html
  23. 45 0
      ambari-logsearch/ambari-logsearch-web/src/app/modules/shared/components/circle-progress-bar/circle-progress-bar.component.less
  24. 41 0
      ambari-logsearch/ambari-logsearch-web/src/app/modules/shared/components/circle-progress-bar/circle-progress-bar.component.spec.ts
  25. 86 0
      ambari-logsearch/ambari-logsearch-web/src/app/modules/shared/components/circle-progress-bar/circle-progress-bar.component.ts
  26. 8 5
      ambari-logsearch/ambari-logsearch-web/src/app/modules/shared/components/modal-dialog/modal-dialog.component.less
  27. 3 1
      ambari-logsearch/ambari-logsearch-web/src/app/modules/shared/components/modal-dialog/modal-dialog.component.ts
  28. 5 2
      ambari-logsearch/ambari-logsearch-web/src/app/modules/shared/shared.module.ts
  29. 14 2
      ambari-logsearch/ambari-logsearch-web/src/app/modules/shared/variables.less
  30. 20 12
      ambari-logsearch/ambari-logsearch-web/src/app/services/logs-container.service.ts
  31. 6 0
      ambari-logsearch/ambari-logsearch-web/src/assets/i18n/en.json
  32. 3 3
      ambari-logsearch/ambari-logsearch-web/yarn.lock

+ 1 - 1
ambari-logsearch/ambari-logsearch-web/package.json

@@ -39,7 +39,7 @@
     "jquery": "^1.12.4",
     "jquery": "^1.12.4",
     "moment": "^2.18.1",
     "moment": "^2.18.1",
     "moment-timezone": "^0.5.13",
     "moment-timezone": "^0.5.13",
-    "ngx-bootstrap": "^1.9.3",
+    "ngx-bootstrap": "^2.0.5",
     "rxjs": "^5.4.3",
     "rxjs": "^5.4.3",
     "zone.js": "^0.8.4"
     "zone.js": "^0.8.4"
   },
   },

+ 3 - 0
ambari-logsearch/ambari-logsearch-web/src/app/classes/components/graph/graph.component.ts

@@ -63,6 +63,9 @@ export class GraphComponent implements AfterViewInit, OnChanges, OnInit, OnDestr
   @Input()
   @Input()
   labels: HomogeneousObject<string> = {};
   labels: HomogeneousObject<string> = {};
 
 
+  @Input()
+  chartLabel: string;
+
   /**
   /**
    * Indicates whether the graph represents dependency on time
    * Indicates whether the graph represents dependency on time
    * @type {boolean}
    * @type {boolean}

+ 1 - 1
ambari-logsearch/ambari-logsearch-web/src/app/classes/components/graph/time-graph.component.less

@@ -24,7 +24,7 @@
     cursor: crosshair;
     cursor: crosshair;
   }
   }
 
 
-  .time-gap {
+  .chart-label, .time-gap {
     color: @base-font-color;
     color: @base-font-color;
     font-size: 1.2rem;
     font-size: 1.2rem;
     text-align: center;
     text-align: center;

+ 6 - 0
ambari-logsearch/ambari-logsearch-web/src/app/components/action-menu/action-menu.component.less

@@ -111,4 +111,10 @@
       padding-right: 0;
       padding-right: 0;
     }
     }
   }
   }
+  /deep/ modal-dialog.log-index-filter .modal-header {
+    min-height: 4rem;
+    header {
+      margin-left: auto;
+    }
+  }
 }
 }

+ 4 - 0
ambari-logsearch/ambari-logsearch-web/src/app/components/action-menu/action-menu.component.ts

@@ -164,4 +164,8 @@ export class ActionMenuComponent  implements OnInit, OnDestroy {
     this.logsContainer.stopCaptureTimer();
     this.logsContainer.stopCaptureTimer();
   }
   }
 
 
+  cancelCapture(): void {
+    this.logsContainer.cancelCapture();
+  }
+
 }
 }

+ 1 - 0
ambari-logsearch/ambari-logsearch-web/src/app/components/collapsible-panel/collapsible-panel.component.html

@@ -20,6 +20,7 @@
         <i [ngClass]="{'fa': true, 'fa-caret-down': !isCollapsed, 'fa-caret-right': isCollapsed}"></i>
         <i [ngClass]="{'fa': true, 'fa-caret-down': !isCollapsed, 'fa-caret-right': isCollapsed}"></i>
         {{((isCollapsed ? collapsedTitle : openTitle) || commonTitle) | translate}}
         {{((isCollapsed ? collapsedTitle : openTitle) || commonTitle) | translate}}
       </a>
       </a>
+      <ng-content select="header"></ng-content>
     </div>
     </div>
     <div class="panel-body" [attr.aria-collapsed]="isCollapsed">
     <div class="panel-body" [attr.aria-collapsed]="isCollapsed">
       <ng-content></ng-content>
       <ng-content></ng-content>

+ 2 - 0
ambari-logsearch/ambari-logsearch-web/src/app/components/collapsible-panel/collapsible-panel.component.less

@@ -24,6 +24,8 @@
     background-color: @panel-heading;
     background-color: @panel-heading;
     border: 0 none;
     border: 0 none;
     color: @base-font-color;
     color: @base-font-color;
+    display: flex;
+    flex-direction: row;
     font-size: 1.25rem;
     font-size: 1.25rem;
     a, a:hover, a:visited {
     a, a:hover, a:visited {
       color: @base-font-color;
       color: @base-font-color;

+ 3 - 0
ambari-logsearch/ambari-logsearch-web/src/app/components/log-index-filter/log-index-filter.component.less

@@ -27,6 +27,9 @@
         position: sticky;
         position: sticky;
         top: -1px;
         top: -1px;
         z-index: 10;
         z-index: 10;
+        th {
+          padding: 8px 0;
+        }
       }
       }
       .component-column {
       .component-column {
         width: 25%;
         width: 25%;

+ 2 - 2
ambari-logsearch/ambari-logsearch-web/src/app/components/log-message/log-message.component.html

@@ -14,11 +14,11 @@
   See the License for the specific language governing permissions and
   See the License for the specific language governing permissions and
   limitations under the License.
   limitations under the License.
 -->
 -->
-<div [ngClass]="{
+<div #wrapper [ngClass]="{
   'log-message-container': true,
   'log-message-container': true,
   'log-message-container-collapsible': addCaret,
   'log-message-container-collapsible': addCaret,
   'log-message-container-open': isOpen
   'log-message-container-open': isOpen
   }">
   }">
   <button *ngIf="addCaret" (click)="onCaretClick($event)"><i class="caret"></i></button>
   <button *ngIf="addCaret" (click)="onCaretClick($event)"><i class="caret"></i></button>
-  <div #content class="log-message-content">{{isOpen ? message : (message | replace: multiLineTestRegexp : ' ')}}</div>
+  <div #content class="log-message-content">{{ message }}</div>
 </div>
 </div>

+ 4 - 3
ambari-logsearch/ambari-logsearch-web/src/app/components/log-message/log-message.component.spec.ts

@@ -48,7 +48,7 @@ describe('LogMessageComponent', () => {
   });
   });
 
 
   it('event handler should call the toggleOpen method', () => {
   it('event handler should call the toggleOpen method', () => {
-    let mockEvent: MouseEvent = document.createEvent('MouseEvent');
+    const mockEvent: MouseEvent = document.createEvent('MouseEvent');
     mockEvent.initEvent('click', true, true);
     mockEvent.initEvent('click', true, true);
     spyOn(component,'toggleOpen');
     spyOn(component,'toggleOpen');
     component.onCaretClick(mockEvent);
     component.onCaretClick(mockEvent);
@@ -56,7 +56,7 @@ describe('LogMessageComponent', () => {
   });
   });
 
 
   it('event handler should prevent the default behaviour of the action', () => {
   it('event handler should prevent the default behaviour of the action', () => {
-    let mockEvent: MouseEvent = document.createEvent('MouseEvent');
+    const mockEvent: MouseEvent = document.createEvent('MouseEvent');
     mockEvent.initEvent('click', true, true);
     mockEvent.initEvent('click', true, true);
     spyOn(mockEvent,'preventDefault');
     spyOn(mockEvent,'preventDefault');
     component.onCaretClick(mockEvent);
     component.onCaretClick(mockEvent);
@@ -64,13 +64,14 @@ describe('LogMessageComponent', () => {
   });
   });
 
 
   it('calling the toggleOpen method should negate the isOpen property', () => {
   it('calling the toggleOpen method should negate the isOpen property', () => {
-    let currentState = component.isOpen;
+    const currentState = component.isOpen;
     component.toggleOpen();
     component.toggleOpen();
     expect(component.isOpen).toEqual(!currentState);
     expect(component.isOpen).toEqual(!currentState);
   });
   });
 
 
   it('should set the addCaret prop to TRUE if the message prop has new line character.', () => {
   it('should set the addCaret prop to TRUE if the message prop has new line character.', () => {
     component.message = messages.withNewLine;
     component.message = messages.withNewLine;
+    component.reCalculateOnChange();
     component.checkAddCaret();
     component.checkAddCaret();
     expect(component['addCaret']).toEqual(true);
     expect(component['addCaret']).toEqual(true);
   });
   });

+ 19 - 22
ambari-logsearch/ambari-logsearch-web/src/app/components/log-message/log-message.component.ts

@@ -24,12 +24,11 @@ import {
   OnInit,
   OnInit,
   OnDestroy,
   OnDestroy,
   SimpleChanges,
   SimpleChanges,
-  HostListener,
   ChangeDetectorRef
   ChangeDetectorRef
 } from '@angular/core';
 } from '@angular/core';
-import {Observable} from 'rxjs/Observable';
+import {Subject} from 'rxjs/Subject';
 import {Subscription} from 'rxjs/Subscription';
 import {Subscription} from 'rxjs/Subscription';
-import 'rxjs/add/operator/debounceTime';
+import 'rxjs/add/operator/auditTime';
 
 
 /**
 /**
  * This is a simple UI component to display the log message. The goal is to be able to show one line and be collapsile
  * This is a simple UI component to display the log message. The goal is to be able to show one line and be collapsile
@@ -63,7 +62,7 @@ export class LogMessageComponent implements AfterViewInit, OnChanges, OnInit, On
    * LogMessageComponent should check if the caret should be visible or not.
    * LogMessageComponent should check if the caret should be visible or not.
    */
    */
   @Input()
   @Input()
-  listenChangesOn: any;
+  refreshOn$: Subject<any>;
 
 
   /**
   /**
    * This will be shown as log message in the component
    * This will be shown as log message in the component
@@ -76,7 +75,9 @@ export class LogMessageComponent implements AfterViewInit, OnChanges, OnInit, On
    * the content container element. Handled by the @checkAddCaret method
    * the content container element. Handled by the @checkAddCaret method
    * @type {boolean}
    * @type {boolean}
    */
    */
-  private addCaret = false;
+  addCaret = false;
+
+  private scrollWidth: number;
 
 
   /**
   /**
    * This is a regexp tester to check if the log message is multiline text or single line. Doing by checking the new
    * This is a regexp tester to check if the log message is multiline text or single line. Doing by checking the new
@@ -90,9 +91,7 @@ export class LogMessageComponent implements AfterViewInit, OnChanges, OnInit, On
    * caret to give a possibility to the user to see the message as it is (pre-wrapped).
    * caret to give a possibility to the user to see the message as it is (pre-wrapped).
    * @type {boolean}
    * @type {boolean}
    */
    */
-  private get isMultiLineMessage(): boolean {
-    return this.multiLineTestRegexp.test(this.message);
-  }
+  isMultiLineMessage = false;
 
 
   /**
   /**
    * The array to collect all the subscriptions created by the instance in order to unsubscribe when the component
    * The array to collect all the subscriptions created by the instance in order to unsubscribe when the component
@@ -109,15 +108,17 @@ export class LogMessageComponent implements AfterViewInit, OnChanges, OnInit, On
    * @param {SimpleChanges} changes
    * @param {SimpleChanges} changes
    */
    */
   ngOnChanges(changes: SimpleChanges): void {
   ngOnChanges(changes: SimpleChanges): void {
-    if (changes.listenChangesOn !== undefined) {
+    if (changes.message !== undefined) {
+      this.message = this.message.trim();
+      this.reCalculateOnChange();
       this.checkAddCaret();
       this.checkAddCaret();
     }
     }
   }
   }
 
 
   ngOnInit() {
   ngOnInit() {
-    this.subscriptions.push(
-      Observable.fromEvent(window, 'resize').debounceTime(100).subscribe(this.onWindowResize)
-    );
+    if (this.refreshOn$) {
+      this.subscriptions.push(this.refreshOn$.subscribe(this.checkAddCaret));
+    }
   }
   }
 
 
   ngOnDestroy() {
   ngOnDestroy() {
@@ -128,16 +129,13 @@ export class LogMessageComponent implements AfterViewInit, OnChanges, OnInit, On
    * The goal is to perform a initial caret display check when the component has been initialized.
    * The goal is to perform a initial caret display check when the component has been initialized.
    */
    */
   ngAfterViewInit(): void {
   ngAfterViewInit(): void {
+    this.reCalculateOnChange();
     this.checkAddCaret();
     this.checkAddCaret();
   }
   }
 
 
-  /**
-   * Since the size of the column is depends on the window size we have to listen the resize event and show/hide the
-   * caret corresponding the new size of the content container element.
-   * Using the arrow function will keep the instance scope.
-   */
-  onWindowResize = (): void => {
-    this.checkAddCaret();
+  reCalculateOnChange() {
+    this.isMultiLineMessage = this.multiLineTestRegexp.test(this.message);
+    this.scrollWidth = this.content.nativeElement.scrollWidth;
   }
   }
 
 
   /**
   /**
@@ -145,10 +143,9 @@ export class LogMessageComponent implements AfterViewInit, OnChanges, OnInit, On
    * scrollHeight and the clientHeight.
    * scrollHeight and the clientHeight.
    */
    */
   checkAddCaret = (): void =>  {
   checkAddCaret = (): void =>  {
-    const el = this.content.nativeElement;
-    this.addCaret = this.isMultiLineMessage || (el.scrollHeight > el.clientHeight) || (el.scrollWidth > el.clientWidth);
+    this.addCaret = this.isMultiLineMessage || (this.scrollWidth > this.content.nativeElement.clientWidth);
     this.cdRef.detectChanges();
     this.cdRef.detectChanges();
-  };
+  }
 
 
   /**
   /**
    * This is the click event handler of the caret button element. It will only toggle the isOpen property so that the
    * This is the click event handler of the caret button element. It will only toggle the isOpen property so that the

+ 29 - 12
ambari-logsearch/ambari-logsearch-web/src/app/components/logs-container/logs-container.component.html

@@ -29,22 +29,20 @@
   [ngClass]="{'container-fluid': true, 'logs-container': true, 'fixed-filterbar': isFilterPanelFixedPostioned}">
   [ngClass]="{'container-fluid': true, 'logs-container': true, 'fixed-filterbar': isFilterPanelFixedPostioned}">
   <filters-panel class="row" [filtersForm]="filtersForm" #filtersPanel></filters-panel>
   <filters-panel class="row" [filtersForm]="filtersForm" #filtersPanel></filters-panel>
   <div class="row events-count">
   <div class="row events-count">
-    <div *ngIf="autoRefreshRemainingSeconds" class="col-md-12">
-      <div class="auto-refresh-message pull-right">
-        {{'filter.capture.triggeringRefresh' | translate: autoRefreshMessageParams}}
-      </div>
+    <div *ngIf="captureTimeRangeCache" class="panel panel-default panel-capture-view col-md-2 col-md-offset-5">
+      <i class="fa fa-play"></i>
+      {{'filter.youAreInSnapshotView' | translate}}
+      <button type="button" class="close" [attr.aria-label]="'modal.close' | translate" (click)="clearCaptureTimeRangeCache()"
+        tooltip="{{'filter.closeSnapshotView' | translate}}" placement="right">
+        <span aria-hidden="true">&times;</span>
+      </button>
     </div>
     </div>
-
-    <!-- TODO use plugin for singular/plural -->
-    <div *ngIf="logsType === 'serviceLogs'" class="logs-header col-md-12">{{
-      (!totalEventsFoundMessageParams.totalCount ? 'logs.noEventFound' :
-        (totalEventsFoundMessageParams.totalCount === 1 ? 'logs.oneEventFound' : 'logs.totalEventFound'))
-            | translate: totalEventsFoundMessageParams
-    }}</div>
   </div>
   </div>
   <ng-container [ngSwitch]="logsType">
   <ng-container [ngSwitch]="logsType">
     <ng-container *ngSwitchCase="'serviceLogs'">
     <ng-container *ngSwitchCase="'serviceLogs'">
-      <collapsible-panel [class.hide]="isServiceLogsFileView$ | async" openTitle="logs.hideGraph" collapsedTitle="logs.showGraph">
+      <collapsible-panel [class.hide]="isServiceLogsFileView$ | async" openTitle="logs.hideGraph" collapsedTitle="logs.showGraph" class="service-logs-histogram">
+        <header>{{(!totalEventsFoundMessageParams.totalCount ? 'logs.noEventFound' :
+            (totalEventsFoundMessageParams.totalCount === 1 ? 'logs.oneEventFound' : 'logs.totalEventFound')) | translate: totalEventsFoundMessageParams}}</header>
         <time-histogram (selectArea)="setCustomTimeRange($event[0], $event[1])" [data]="serviceLogsHistogramData"
         <time-histogram (selectArea)="setCustomTimeRange($event[0], $event[1])" [data]="serviceLogsHistogramData"
                         [colors]="serviceLogsHistogramColors" [allowFractionalYTicks]="false"
                         [colors]="serviceLogsHistogramColors" [allowFractionalYTicks]="false"
                         svgId="service-logs-histogram"></time-histogram>
                         svgId="service-logs-histogram"></time-histogram>
@@ -65,3 +63,22 @@
   <log-context *ngIf="isServiceLogContextView" [id]="activeLog.id" [hostName]="activeLog.host_name"
   <log-context *ngIf="isServiceLogContextView" [id]="activeLog.id" [hostName]="activeLog.host_name"
                [componentName]="activeLog.component_name"></log-context>
                [componentName]="activeLog.component_name"></log-context>
 </div>
 </div>
+<modal-dialog
+  title="{{'filter.capture' | translate}}"
+  class="capture-dialog"
+  [visible]="autoRefreshRemainingSeconds"
+  (onCloseRequest)="cancelCapture()">
+  <span class="info">{{'filter.refreshingLogListIn' | translate}}</span>
+  <circle-progress-bar radius="50" strokeWidth="5" strokeColor="black"
+  [percent]="(autoRefreshRemainingSeconds / (autoRefreshInterval / 1000)) * 100">
+    <span>
+      <span class="remaining-seconds">
+        {{autoRefreshRemainingSeconds}}
+        <span class="unit">{{'filter.capture.sec' | translate}}</span>
+      </span>
+    </span>
+  </circle-progress-bar>
+  <footer>
+      <button class="btn btn-secondary" (click)="cancelCapture()">{{'modal.cancel' | translate}}</button>
+  </footer>
+</modal-dialog>

+ 42 - 1
ambari-logsearch/ambari-logsearch-web/src/app/components/logs-container/logs-container.component.less

@@ -17,6 +17,7 @@
  */
  */
 
 
 @import '../../modules/shared/mixins';
 @import '../../modules/shared/mixins';
+@import '../../modules/shared/variables';
 
 
 :host {
 :host {
   display: block;
   display: block;
@@ -40,7 +41,7 @@
 
 
   .fixed-filterbar {
   .fixed-filterbar {
     filters-panel {
     filters-panel {
-      background-color: fadeout(@filters-panel-background-color, 10%);
+      background-color: fadeout(@filters-panel-background-color, 5%);
       box-shadow: 0 2px 2px rgba(0,0,0,.1);
       box-shadow: 0 2px 2px rgba(0,0,0,.1);
       left: 0;
       left: 0;
       margin: 0;
       margin: 0;
@@ -55,4 +56,44 @@
     margin-top: @block-margin-top;
     margin-top: @block-margin-top;
   }
   }
 
 
+  /deep/ collapsible-panel.service-logs-histogram {
+    .panel-heading {
+      header {
+        margin-left: auto;
+      }
+    }
+  }
+
+  /deep/ modal-dialog.capture-dialog {
+    .modal-dialog {
+      max-width: 350px;
+    }
+    .modal-body {
+      display: flex;
+      flex-direction: column;
+      /deep/ circle-progress-bar {
+        display: inline-block;
+        align-self: center;
+        label {
+          font-size: 3rem;
+          font-weight: normal;
+          .unit {
+            color: @fluid-gray-2;
+            font-size: 1.2rem;
+          }
+        }
+      }
+    }
+  }
+
+  .panel-capture-view {
+    padding: 1rem;
+    i {
+      color: @fluid-gray-1;
+      &.fa-play {
+        padding-right: 1rem;
+      }
+    }
+  }
+
 }
 }

+ 3 - 1
ambari-logsearch/ambari-logsearch-web/src/app/components/logs-container/logs-container.component.spec.ts

@@ -19,6 +19,7 @@
 import {async, ComponentFixture, TestBed} from '@angular/core/testing';
 import {async, ComponentFixture, TestBed} from '@angular/core/testing';
 import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
 import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
 import {StoreModule} from '@ngrx/store';
 import {StoreModule} from '@ngrx/store';
+import {TooltipModule} from 'ngx-bootstrap';
 import {MockHttpRequestModules, TranslationModules} from '@app/test-config.spec';
 import {MockHttpRequestModules, TranslationModules} from '@app/test-config.spec';
 import {AppSettingsService, appSettings} from '@app/services/storage/app-settings.service';
 import {AppSettingsService, appSettings} from '@app/services/storage/app-settings.service';
 import {AppStateService, appState} from '@app/services/storage/app-state.service';
 import {AppStateService, appState} from '@app/services/storage/app-state.service';
@@ -75,7 +76,8 @@ describe('LogsContainerComponent', () => {
           hosts,
           hosts,
           serviceLogsTruncated
           serviceLogsTruncated
         }),
         }),
-        ...TranslationModules
+        ...TranslationModules,
+        TooltipModule.forRoot(),
       ],
       ],
       providers: [
       providers: [
         ...MockHttpRequestModules,
         ...MockHttpRequestModules,

+ 22 - 2
ambari-logsearch/ambari-logsearch-web/src/app/components/logs-container/logs-container.component.ts

@@ -58,7 +58,7 @@ export class LogsContainerComponent implements OnInit, OnDestroy {
     });
     });
   });
   });
 
 
-  private logsType: LogsType;
+  logsType: LogsType;
 
 
   serviceLogsHistogramData: HomogeneousObject<HomogeneousObject<number>>;
   serviceLogsHistogramData: HomogeneousObject<HomogeneousObject<number>>;
 
 
@@ -87,7 +87,7 @@ export class LogsContainerComponent implements OnInit, OnDestroy {
   private subscriptions: Subscription[] = [];
   private subscriptions: Subscription[] = [];
   private paramsSyncInProgress: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
   private paramsSyncInProgress: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
 
 
-  private isServiceLogsFileView$: Observable<boolean> = this.appState.getParameter('isServiceLogsFileView');
+  isServiceLogsFileView$: Observable<boolean> = this.appState.getParameter('isServiceLogsFileView');
 
 
   constructor(
   constructor(
     private appState: AppStateService,
     private appState: AppStateService,
@@ -176,6 +176,12 @@ export class LogsContainerComponent implements OnInit, OnDestroy {
   get autoRefreshRemainingSeconds(): number {
   get autoRefreshRemainingSeconds(): number {
     return this.logsContainerService.autoRefreshRemainingSeconds;
     return this.logsContainerService.autoRefreshRemainingSeconds;
   }
   }
+  get autoRefreshInterval(): number {
+    return this.logsContainerService.autoRefreshInterval;
+  }
+  get captureTimeRangeCache(): ListItem {
+    return this.logsContainerService.captureTimeRangeCache;
+  }
 
 
   get autoRefreshMessageParams(): object {
   get autoRefreshMessageParams(): object {
     return {
     return {
@@ -361,4 +367,18 @@ export class LogsContainerComponent implements OnInit, OnDestroy {
       this.router.navigate(['/logs', ...this.logsFilteringUtilsService.getNavigationForTab(newActiveTab)]);
       this.router.navigate(['/logs', ...this.logsFilteringUtilsService.getNavigationForTab(newActiveTab)]);
     }
     }
   }
   }
+  //
+  // CAPTURE FEATURES
+  //
+  cancelCapture(): void {
+    this.logsContainerService.cancelCapture();
+  }
+
+  clearCaptureTimeRangeCache(): void {
+    if (this.captureTimeRangeCache) {
+      this.filtersForm.controls.timeRange.setValue(this.captureTimeRangeCache);
+      this.logsContainerService.captureTimeRangeCache = null;
+    }
+  }
+
 }
 }

+ 11 - 6
ambari-logsearch/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.html

@@ -18,6 +18,11 @@
 <div class="panel panel-default">
 <div class="panel panel-default">
   <div [ngClass]="{'panel-body': true, 'layout-flex': layout==='FLEX', 'layout-table': layout==='TABLE'}">
   <div [ngClass]="{'panel-body': true, 'layout-flex': layout==='FLEX', 'layout-table': layout==='TABLE'}">
     <div class="service-logs-table-controls">
     <div class="service-logs-table-controls">
+      <div class="total-event-info">
+        <ng-container *ngIf="!totalEventsFoundMessageParams.totalCount">{{'logs.noEventFound' | translate}}</ng-container>
+        <ng-container *ngIf="totalEventsFoundMessageParams.totalCount === 1">{{'logs.oneEventFound' | translate}}</ng-container>
+        <ng-container *ngIf="totalEventsFoundMessageParams.totalCount > 1">{{'logs.totalEventFound' | translate: totalEventsFoundMessageParams}}</ng-container>
+      </div>
       <div *ngIf="tooManyColumnsSelected" class="list-layout-warning">
       <div *ngIf="tooManyColumnsSelected" class="list-layout-warning">
         <i class="fa fa-warning"></i>
         <i class="fa fa-warning"></i>
         {{'logs.brokenListLayoutMessage' | translate}}
         {{'logs.brokenListLayoutMessage' | translate}}
@@ -103,9 +108,9 @@
               <td *ngIf="isColumnDisplayed('path')" [ngClass]="'log-path'">
               <td *ngIf="isColumnDisplayed('path')" [ngClass]="'log-path'">
                 {{log.path}}
                 {{log.path}}
               </td>
               </td>
-              <td *ngIf="isColumnDisplayed('log_message')" [ngClass]="'log-message'" width="*"
+              <td *ngIf="isColumnDisplayed('log_message')" class="log-message" width="*"
                   (contextmenu)="openMessageContextMenu($event)">
                   (contextmenu)="openMessageContextMenu($event)">
-                <log-message [listenChangesOn]="displayedColumns" [message]="log.log_message"></log-message>
+                <log-message [refreshOn$]="tableRefresh$" [message]="log.log_message"></log-message>
               </td>
               </td>
             </tr>
             </tr>
           </ng-container>
           </ng-container>
@@ -113,9 +118,9 @@
         <tfoot>
         <tfoot>
           <tr>
           <tr>
             <td attr.colspan="{{displayedColumns.length + 1}}">
             <td attr.colspan="{{displayedColumns.length + 1}}">
-              <pagination class="col-md-12" *ngIf="logs && logs.length" [totalCount]="totalCount"
-                          [filtersForm]="filtersForm" [filterInstance]="filters.pageSize"
-                          [currentCount]="logs.length"></pagination>
+              <pagination *ngIf="logs && logs.length" [totalCount]="totalCount"
+                [filtersForm]="filtersForm" [filterInstance]="filters.pageSize"
+                [currentCount]="logs.length"></pagination>
             </td>
             </td>
           </tr>
           </tr>
         </tfoot>
         </tfoot>
@@ -158,7 +163,7 @@
               </div>
               </div>
             </div>
             </div>
             <div *ngIf="isColumnDisplayed('log_message')" class="log-message" (contextmenu)="openMessageContextMenu($event)">
             <div *ngIf="isColumnDisplayed('log_message')" class="log-message" (contextmenu)="openMessageContextMenu($event)">
-              <log-message [listenChangesOn]="displayedColumns" [message]="log.log_message"></log-message>
+              <log-message [refreshOn$]="tableRefresh$" [message]="log.log_message"></log-message>
             </div>
             </div>
           </div>
           </div>
         </ng-container>
         </ng-container>

+ 13 - 0
ambari-logsearch/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.less

@@ -20,9 +20,16 @@
 :host {
 :host {
 
 
   .service-logs-table-controls {
   .service-logs-table-controls {
+    align-items: center;
     display: flex;
     display: flex;
     flex-wrap: wrap;
     flex-wrap: wrap;
     justify-content: flex-end;
     justify-content: flex-end;
+    .total-event-info {
+      margin-right: auto;
+    }
+    pagination {
+      margin-right: auto;
+    }
     .layout-btn-group {
     .layout-btn-group {
       display: flex;
       display: flex;
       align-items: center;
       align-items: center;
@@ -145,6 +152,12 @@
       }
       }
       &.log-message {
       &.log-message {
         width: 100%;
         width: 100%;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+        &.log-message-open {
+          white-space: pre-wrap;
+        }
       }
       }
       &.log-event_count {
       &.log-event_count {
         width: 3em;
         width: 3em;

+ 121 - 71
ambari-logsearch/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.ts

@@ -16,7 +16,22 @@
  * limitations under the License.
  * limitations under the License.
  */
  */
 
 
-import {Component, AfterViewChecked, ViewChild, ElementRef, Input, ChangeDetectorRef} from '@angular/core';
+import {
+  Component,
+  OnInit,
+  OnDestroy,
+  AfterViewChecked,
+  ViewChild,
+  ElementRef,
+  Input,
+  ChangeDetectorRef,
+  SimpleChanges
+} from '@angular/core';
+
+import { Subject } from 'rxjs/Subject';
+import { Subscription } from 'rxjs/Subscription';
+import { Observable } from 'rxjs/Observable';
+import { auditTime } from 'rxjs/operator/auditTime';
 
 
 import {ListItem} from '@app/classes/list-item';
 import {ListItem} from '@app/classes/list-item';
 import {LogsTableComponent} from '@app/classes/components/logs-table/logs-table-component';
 import {LogsTableComponent} from '@app/classes/components/logs-table/logs-table-component';
@@ -35,21 +50,7 @@ export enum ListLayout {
   templateUrl: './service-logs-table.component.html',
   templateUrl: './service-logs-table.component.html',
   styleUrls: ['./service-logs-table.component.less']
   styleUrls: ['./service-logs-table.component.less']
 })
 })
-export class ServiceLogsTableComponent extends LogsTableComponent implements AfterViewChecked {
-
-  constructor(
-    private logsContainer: LogsContainerService,
-    private utils: UtilsService,
-    private cdRef: ChangeDetectorRef,
-    private notificationService: NotificationService
-  ) {
-    super();
-  }
-
-  ngAfterViewChecked() {
-    this.checkListLayout();
-    this.cdRef.detectChanges();
-  }
+export class ServiceLogsTableComponent extends LogsTableComponent implements AfterViewChecked, OnInit, OnDestroy {
 
 
   /**
   /**
    * The element reference is used to check if the table is broken or not.
    * The element reference is used to check if the table is broken or not.
@@ -72,21 +73,21 @@ export class ServiceLogsTableComponent extends LogsTableComponent implements Aft
    * @type {boolean}
    * @type {boolean}
    */
    */
   @Input()
   @Input()
-  showLabels: boolean = false;
+  showLabels = false;
 
 
   /**
   /**
    * The minimum width for the log message column. It is used when we check if the layout is broken or not.
    * The minimum width for the log message column. It is used when we check if the layout is broken or not.
    * @type {number}
    * @type {number}
    */
    */
   @Input()
   @Input()
-  logMessageColumnMinWidth: number = 175;
+  logMessageColumnMinWidth = 175;
 
 
   /**
   /**
    * We use this property in the broken table layout check process when the log message is displayed.
    * We use this property in the broken table layout check process when the log message is displayed.
    * @type {string}
    * @type {string}
    */
    */
   @Input()
   @Input()
-  logMessageColumnCssSelector: string = 'tbody tr td.log-message';
+  logMessageColumnCssSelector = 'tbody tr td.log-message';
 
 
   /**
   /**
    * Set the layout for the list.
    * Set the layout for the list.
@@ -99,9 +100,91 @@ export class ServiceLogsTableComponent extends LogsTableComponent implements Aft
   @Input()
   @Input()
   layout: ListLayout = ListLayout.Table;
   layout: ListLayout = ListLayout.Table;
 
 
-  readonly dateFormat: string = 'dddd, MMMM Do';
+  readonly dateFormat = 'dddd, MMMM Do';
+
+  readonly timeFormat = 'h:mm:ss A';
+
+  readonly customStyledColumns: string[] = ['level', 'type', 'logtime', 'log_message', 'path'];
+
+  private readonly messageFilterParameterName = 'log_message';
+
+  private readonly logsType = 'serviceLogs';
+
+  private selectedText = '';
+
+  /**
+   * This is a private flag to store the table layout check result. It is used to show user notifications about
+   * non-visible information.
+   * @type {boolean}
+   */
+  tooManyColumnsSelected = false;
+
+  get contextMenuItems(): ListItem[] {
+    return this.logsContainer.queryContextMenuItems;
+  }
+
+  get timeZone(): string {
+    return this.logsContainer.timeZone;
+  }
+
+  get filters(): any {
+    return this.logsContainer.filters;
+  }
+
+  get logsTypeMapObject(): object {
+    return this.logsContainer.logsTypeMap.serviceLogs;
+  }
 
 
-  readonly timeFormat: string = 'h:mm:ss A';
+  get isContextMenuDisplayed(): boolean {
+    return Boolean(this.selectedText);
+  };
+
+  /**
+   * 'left' CSS property value for context menu dropdown
+   * @type {number}
+   */
+  contextMenuLeft = 0;
+
+  /**
+   * 'top' CSS property value for context menu dropdown
+   * @type {number}
+   */
+  contextMenuTop = 0;
+
+  tableRefresh$ = new Subject();
+
+  subscriptions: Subscription[] = [];
+
+  constructor(
+    private logsContainer: LogsContainerService,
+    private utils: UtilsService,
+    private cdRef: ChangeDetectorRef,
+    private notificationService: NotificationService
+  ) {
+    super();
+  }
+
+  ngOnInit() {
+    this.subscriptions.push(
+      Observable.fromEvent(window, 'resize').auditTime(300).subscribe(this.onWindowResize)
+    );
+  }
+
+  ngAfterViewChecked() {
+    this.checkListLayout();
+    this.cdRef.detectChanges();
+  }
+
+  ngOnChanges(changes: SimpleChanges) {
+    if (changes.hasOwnProperty('columns')) {
+      this.displayedColumns = this.columns.filter((column: ListItem): boolean => column.isChecked);
+      this.tableRefresh$.next(Date.now());
+    }
+  }
+
+  ngOnDestroy() {
+    this.subscriptions.forEach((subscription: Subscription) => subscription.unsubscribe());
+  }
 
 
   private copyLog = (log: ServiceLog): void => {
   private copyLog = (log: ServiceLog): void => {
     if (document.queryCommandSupported('copy')) {
     if (document.queryCommandSupported('copy')) {
@@ -144,15 +227,19 @@ export class ServiceLogsTableComponent extends LogsTableComponent implements Aft
         message: 'logs.copy.notSupported'
         message: 'logs.copy.notSupported'
       });
       });
     }
     }
-  };
+  }
+
+  private onWindowResize = () => {
+    this.tableRefresh$.next(Date.now());
+  }
 
 
   private openLog = (log: ServiceLog): void => {
   private openLog = (log: ServiceLog): void => {
     this.logsContainer.openServiceLog(log);
     this.logsContainer.openServiceLog(log);
-  };
+  }
 
 
   private openContext = (log: ServiceLog): void => {
   private openContext = (log: ServiceLog): void => {
     this.logsContainer.loadLogContext(log.id, log.host, log.type);
     this.logsContainer.loadLogContext(log.id, log.host, log.type);
-  };
+  }
 
 
   readonly logActions = [
   readonly logActions = [
     {
     {
@@ -172,53 +259,6 @@ export class ServiceLogsTableComponent extends LogsTableComponent implements Aft
     }
     }
   ];
   ];
 
 
-  readonly customStyledColumns: string[] = ['level', 'type', 'logtime', 'log_message', 'path'];
-
-  private readonly messageFilterParameterName: string = 'log_message';
-
-  private readonly logsType: string = 'serviceLogs';
-
-  private selectedText: string = '';
-
-  /**
-   * This is a private flag to store the table layout check result. It is used to show user notifications about
-   * non-visible information.
-   * @type {boolean}
-   */
-  private tooManyColumnsSelected: boolean = false;
-
-  get contextMenuItems(): ListItem[] {
-    return this.logsContainer.queryContextMenuItems;
-  }
-
-  get timeZone(): string {
-    return this.logsContainer.timeZone;
-  }
-
-  get filters(): any {
-    return this.logsContainer.filters;
-  }
-
-  get logsTypeMapObject(): object {
-    return this.logsContainer.logsTypeMap.serviceLogs;
-  }
-
-  get isContextMenuDisplayed(): boolean {
-    return Boolean(this.selectedText);
-  };
-
-  /**
-   * 'left' CSS property value for context menu dropdown
-   * @type {number}
-   */
-  contextMenuLeft: number = 0;
-
-  /**
-   * 'top' CSS property value for context menu dropdown
-   * @type {number}
-   */
-  contextMenuTop: number = 0;
-
   isDifferentDates(dateA, dateB): boolean {
   isDifferentDates(dateA, dateB): boolean {
     return this.utils.isDifferentDates(dateA, dateB, this.timeZone);
     return this.utils.isDifferentDates(dateA, dateB, this.timeZone);
   }
   }
@@ -319,4 +359,14 @@ export class ServiceLogsTableComponent extends LogsTableComponent implements Aft
     this.logsContainer.updateSelectedColumns(columns, this.logsType);
     this.logsContainer.updateSelectedColumns(columns, this.logsType);
   }
   }
 
 
+  /**
+   * The goal is to provide the single source for the parameters of 'xyz events found' message.
+   * @returns {Object}
+   */
+  get totalEventsFoundMessageParams(): {totalCount: number} {
+    return {
+      totalCount: this.logsContainer.totalCount
+    };
+  }
+
 }
 }

+ 3 - 0
ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.html

@@ -24,6 +24,9 @@
       <graph-legend [ngClass]="{'col-md-5 text-right': true, 'md-offset-7': !chartTimeGap}"
       <graph-legend [ngClass]="{'col-md-5 text-right': true, 'md-offset-7': !chartTimeGap}"
                     [items]="legendItems"></graph-legend>
                     [items]="legendItems"></graph-legend>
     </div>
     </div>
+    <div *ngIf="chartLabel" class="row chart-label">
+      <div class="col-md-2 col-md-offset-5">{{ chartLabel }}</div>
+    </div>
   </div>
   </div>
 </header>
 </header>
 <div #graphContainer></div>
 <div #graphContainer></div>

+ 3 - 0
ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.less

@@ -22,4 +22,7 @@
   header {
   header {
     padding: @graph-padding;
     padding: @graph-padding;
   }
   }
+  .chart-label {
+    text-align: center;
+  }
 }
 }

+ 1 - 1
ambari-logsearch/ambari-logsearch-web/src/app/components/time-range-picker/time-range-picker.component.html

@@ -26,7 +26,7 @@
     <date-picker class="col-md-12 row" [time]="startTime" (timeChange)="setStartTime($event)"></date-picker>
     <date-picker class="col-md-12 row" [time]="startTime" (timeChange)="setStartTime($event)"></date-picker>
     <div class="col-md-12 row text-uppercase">{{'filter.timeRange.to' | translate}}</div>
     <div class="col-md-12 row text-uppercase">{{'filter.timeRange.to' | translate}}</div>
     <date-picker class="col-md-12 row" [time]="endTime" (timeChange)="setEndTime($event)"></date-picker>
     <date-picker class="col-md-12 row" [time]="endTime" (timeChange)="setEndTime($event)"></date-picker>
-    <button class="btn btn-success pull-right" type="button" (click)="setCustomTimeRange()"
+    <button class="btn btn-success" type="button" (click)="setCustomTimeRange()"
             [disabled]="!startTime || !endTime || startTime >= endTime">
             [disabled]="!startTime || !endTime || startTime >= endTime">
       {{'modal.apply' | translate}}
       {{'modal.apply' | translate}}
     </button>
     </button>

+ 32 - 0
ambari-logsearch/ambari-logsearch-web/src/app/modules/shared/components/circle-progress-bar/circle-progress-bar.component.html

@@ -0,0 +1,32 @@
+<!--
+  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.
+-->
+<svg [attr.height]="radius * 2" [attr.width]="radius * 2">
+    <circle class="full-circle"
+    [attr.stroke-width]="strokeWidth"
+    [attr.r]="normalizedRadius"
+    [attr.cx]="radius"
+    [attr.cy]="radius"/>
+  <circle #circle
+    [attr.stroke-dasharray]="circumference + ' ' + circumference"
+    [attr.stroke-width]="strokeWidth"
+    [attr.r]="normalizedRadius"
+    [attr.cx]="radius"
+    [attr.cy]="radius"/>
+</svg>
+<label>
+  <ng-content></ng-content>
+</label>

+ 45 - 0
ambari-logsearch/ambari-logsearch-web/src/app/modules/shared/components/circle-progress-bar/circle-progress-bar.component.less

@@ -0,0 +1,45 @@
+/**
+ * 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.
+ */
+ @import '../../variables';
+:host {
+  display: inline-block;
+  position: relative;
+  label {
+    align-items: center;
+    align-content: center;
+    background-color: transparent;
+    display: flex;
+    height: 100%;
+    justify-content: center;
+    left: 0;
+    position: absolute;
+    top: 0;
+    width: 100%;
+  }
+  svg {
+    circle {
+      fill: transparent;
+      stroke: @blue;
+      transition: stroke-dashoffset 0.35s;
+      transform: rotate(-90deg);
+      transform-origin: 50% 50%;
+      &.full-circle {
+        stroke: @grey;
+      }
+    }
+  }
+}

+ 41 - 0
ambari-logsearch/ambari-logsearch-web/src/app/modules/shared/components/circle-progress-bar/circle-progress-bar.component.spec.ts

@@ -0,0 +1,41 @@
+/**
+ * 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.
+ */
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { CircleProgressBarComponent } from './circle-progress-bar.component';
+
+describe('CircleProgressBarComponent', () => {
+  let component: CircleProgressBarComponent;
+  let fixture: ComponentFixture<CircleProgressBarComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [ CircleProgressBarComponent ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(CircleProgressBarComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 86 - 0
ambari-logsearch/ambari-logsearch-web/src/app/modules/shared/components/circle-progress-bar/circle-progress-bar.component.ts

@@ -0,0 +1,86 @@
+/**
+ * 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.
+ */
+import {
+  Component,
+  OnInit,
+  OnChanges,
+  Input,
+  ViewChild,
+  ElementRef,
+  SimpleChanges,
+  SimpleChange
+} from '@angular/core';
+
+@Component({
+  selector: 'circle-progress-bar',
+  templateUrl: './circle-progress-bar.component.html',
+  styleUrls: ['./circle-progress-bar.component.less']
+})
+export class CircleProgressBarComponent implements OnInit, OnChanges {
+
+  @Input()
+  radius: number;
+
+  @Input()
+  strokeColor = 'white';
+
+  @Input()
+  strokeWidth: number;
+
+  @Input()
+  fill = 'transparent';
+
+  @Input()
+  percent = 0;
+
+  @Input()
+  label: string;
+
+  @ViewChild('circle')
+  circleRef: ElementRef;
+
+  get normalizedRadius(): number {
+    return this.radius - this.strokeWidth;
+  }
+
+  get circumference(): number {
+    return this.normalizedRadius * 2 * Math.PI;
+  }
+
+  get strokeDashoffset(): number {
+    return this.circumference - (this.percent / 100 * this.circumference);
+  }
+
+  constructor() { }
+
+  ngOnInit() {
+    this.setProgress(this.percent);
+  }
+
+  ngOnChanges(changes: SimpleChanges) {
+    if (changes.percent) {
+      this.setProgress(this.percent);
+    }
+  }
+
+  setProgress(percent = this.percent) {
+    if (this.circleRef) {
+      this.circleRef.nativeElement.style.strokeDashoffset = this.strokeDashoffset;
+    }
+  }
+
+}

+ 8 - 5
ambari-logsearch/ambari-logsearch-web/src/app/modules/shared/components/modal-dialog/modal-dialog.component.less

@@ -14,6 +14,7 @@
  * See the License for the specific language governing permissions and
  * See the License for the specific language governing permissions and
  * limitations under the License.
  * limitations under the License.
  */
  */
+ @import '../../variables';
 :host {
 :host {
   .modal-backdrop {
   .modal-backdrop {
     opacity: .5;
     opacity: .5;
@@ -29,27 +30,29 @@
       flex-direction: column;
       flex-direction: column;
       max-height: 90vh;
       max-height: 90vh;
       overflow: hidden;
       overflow: hidden;
-      padding: 1.42rem;
+      padding: @modal-dialog-content-padding;
       .modal-header {
       .modal-header {
-        display: block;
+        display: flex;
         flex-shrink: 1;
         flex-shrink: 1;
         line-height: 1.42rem;
         line-height: 1.42rem;
+        padding: @modal-dialog-header-padding;
         position: relative;
         position: relative;
         &> * {
         &> * {
           display: inline-block;
           display: inline-block;
         }
         }
         .close {
         .close {
-          position: absolute;
-          top: 0;
-          right: 1em;
+          order: 1;
+          margin-left: auto;
         }
         }
       }
       }
       .modal-body {
       .modal-body {
         flex: 1;
         flex: 1;
         overflow: auto;
         overflow: auto;
+        padding: @modal-dialog-body-padding;
       }
       }
       .modal-footer {
       .modal-footer {
         flex-shrink: 1;
         flex-shrink: 1;
+        padding: @modal-dialog-footer-padding;
       }
       }
     }
     }
   }
   }

+ 3 - 1
ambari-logsearch/ambari-logsearch-web/src/app/modules/shared/components/modal-dialog/modal-dialog.component.ts

@@ -74,7 +74,9 @@ export class ModalDialogComponent implements AfterViewInit {
     if (this.showCloseBtn) {
     if (this.showCloseBtn) {
       totalBuiltInHeaderElement += 1;
       totalBuiltInHeaderElement += 1;
     }
     }
-    this.showHeader = this.headerElementRef && (this.headerElementRef.nativeElement.children.length - totalBuiltInHeaderElement > 0);
+    this.showHeader = this.showCloseBtn || !!this.title || (
+      this.headerElementRef && (this.headerElementRef.nativeElement.children.length - totalBuiltInHeaderElement > 0)
+    );
     this.showFooter = this.footerElementRef && this.footerElementRef.nativeElement.children.length;
     this.showFooter = this.footerElementRef && this.footerElementRef.nativeElement.children.length;
     this.cdRef.detectChanges();
     this.cdRef.detectChanges();
   }
   }

+ 5 - 2
ambari-logsearch/ambari-logsearch-web/src/app/modules/shared/shared.module.ts

@@ -40,6 +40,7 @@ import {ModalComponent} from './components/modal/modal.component';
 import { DataLoadingIndicatorComponent } from '@app/modules/shared/components/data-loading-indicator/data-loading-indicator.component';
 import { DataLoadingIndicatorComponent } from '@app/modules/shared/components/data-loading-indicator/data-loading-indicator.component';
 import { ModalDialogComponent } from './components/modal-dialog/modal-dialog.component';
 import { ModalDialogComponent } from './components/modal-dialog/modal-dialog.component';
 import { LoadingIndicatorComponent } from './components/loading-indicator/loading-indicator.component';
 import { LoadingIndicatorComponent } from './components/loading-indicator/loading-indicator.component';
+import { CircleProgressBarComponent } from './components/circle-progress-bar/circle-progress-bar.component';
 
 
 @NgModule({
 @NgModule({
   imports: [
   imports: [
@@ -64,7 +65,8 @@ import { LoadingIndicatorComponent } from './components/loading-indicator/loadin
     ModalComponent,
     ModalComponent,
     DataLoadingIndicatorComponent,
     DataLoadingIndicatorComponent,
     ModalDialogComponent,
     ModalDialogComponent,
-    LoadingIndicatorComponent
+    LoadingIndicatorComponent,
+    CircleProgressBarComponent
   ],
   ],
   providers: [
   providers: [
     Title,
     Title,
@@ -80,7 +82,8 @@ import { LoadingIndicatorComponent } from './components/loading-indicator/loadin
     ModalComponent,
     ModalComponent,
     DataLoadingIndicatorComponent,
     DataLoadingIndicatorComponent,
     ModalDialogComponent,
     ModalDialogComponent,
-    LoadingIndicatorComponent
+    LoadingIndicatorComponent,
+    CircleProgressBarComponent
   ]
   ]
 })
 })
 export class SharedModule { }
 export class SharedModule { }

+ 14 - 2
ambari-logsearch/ambari-logsearch-web/src/app/modules/shared/variables.less

@@ -17,6 +17,14 @@
  */
  */
 
 
 // Variables
 // Variables
+@blue: #1491C1;
+@grey: #DDD;
+
+@fluid-gray-1: #ccc;
+@fluid-gray-2: #999;
+@fluid-gray-3: #666;
+@fluid-gray-4: #333;
+
 @base-font-color: #666;
 @base-font-color: #666;
 @navbar-background-color: #323544;
 @navbar-background-color: #323544;
 @navbar-logo-background-color: #303d54;
 @navbar-logo-background-color: #303d54;
@@ -26,8 +34,8 @@
 @input-border: @input-border-width solid #CFD3D7;
 @input-border: @input-border-width solid #CFD3D7;
 @input-group-addon-padding: 6px 12px 6px 0;
 @input-group-addon-padding: 6px 12px 6px 0;
 @block-margin-top: 20px;
 @block-margin-top: 20px;
-@link-color: #1491C1;
-@link-hover-color: #23527C;
+@link-color: @blue;
+@link-hover-color: darken(@blue, 10%);
 @grey-color: #DDD;
 @grey-color: #DDD;
 @default-line-height: 1.42857143;
 @default-line-height: 1.42857143;
 @main-background-color: #ECECEC;
 @main-background-color: #ECECEC;
@@ -87,6 +95,10 @@
 
 
 // Modals
 // Modals
 @large-modal-width: 1200px;
 @large-modal-width: 1200px;
+@modal-dialog-content-padding: 2rem;
+@modal-dialog-header-padding: 0 0 1rem 0;
+@modal-dialog-body-padding: 0;
+@modal-dialog-footer-padding: 1rem 0 0 0;
 
 
 // Notifications
 // Notifications
 @notification-color: @base-font-color;
 @notification-color: @base-font-color;

+ 20 - 12
ambari-logsearch/ambari-logsearch-web/src/app/services/logs-container.service.ts

@@ -59,8 +59,7 @@ import {NodeItem} from '@app/classes/models/node-item';
 import {CommonEntry} from '@app/classes/models/common-entry';
 import {CommonEntry} from '@app/classes/models/common-entry';
 import {ClusterSelectionService} from '@app/services/storage/cluster-selection.service';
 import {ClusterSelectionService} from '@app/services/storage/cluster-selection.service';
 import {ActivatedRoute, Router} from '@angular/router';
 import {ActivatedRoute, Router} from '@angular/router';
-import {RoutingUtilsService} from '@app/services/routing-utils.service';
-import {LogsFilteringUtilsService, timeRangeFilterOptions} from '@app/services/logs-filtering-utils.service';
+import {LogsFilteringUtilsService} from '@app/services/logs-filtering-utils.service';
 import {BehaviorSubject} from 'rxjs/BehaviorSubject';
 import {BehaviorSubject} from 'rxjs/BehaviorSubject';
 import {LogsStateService} from '@app/services/storage/logs-state.service';
 import {LogsStateService} from '@app/services/storage/logs-state.service';
 import {LogLevelComponent} from '@app/components/log-level/log-level.component';
 import {LogLevelComponent} from '@app/components/log-level/log-level.component';
@@ -244,11 +243,11 @@ export class LogsContainerService {
     users: ['userList']
     users: ['userList']
   };
   };
 
 
-  readonly customTimeRangeKey: string = 'filter.timeRange.custom';
+  readonly customTimeRangeKey = 'filter.timeRange.custom';
 
 
-  readonly topResourcesCount: string = '10';
+  readonly topResourcesCount = '10';
 
 
-  readonly topUsersCount: string = '6';
+  readonly topUsersCount = '6';
 
 
   readonly logsTypeMap = {
   readonly logsTypeMap = {
     auditLogs: {
     auditLogs: {
@@ -289,16 +288,16 @@ export class LogsContainerService {
 
 
   timeZone: string = this.defaultTimeZone;
   timeZone: string = this.defaultTimeZone;
 
 
-  totalCount: number = 0;
+  totalCount = 0;
 
 
   /**
   /**
    * A configurable property to indicate the maximum capture time in milliseconds.
    * A configurable property to indicate the maximum capture time in milliseconds.
    * @type {number}
    * @type {number}
    * @default 600000 (10 minutes)
    * @default 600000 (10 minutes)
    */
    */
-  private readonly maximumCaptureTimeLimit: number = 600000;
+  readonly maximumCaptureTimeLimit = 600000;
 
 
-  isServiceLogsFileView: boolean = false;
+  isServiceLogsFileView = false;
 
 
   filtersForm: FormGroup;
   filtersForm: FormGroup;
 
 
@@ -336,16 +335,18 @@ export class LogsContainerService {
 
 
   private stopAutoRefreshCountdown: Subject<void> = new Subject();
   private stopAutoRefreshCountdown: Subject<void> = new Subject();
 
 
-  captureSeconds: number = 0;
+  captureSeconds = 0;
 
 
-  private readonly autoRefreshInterval: number = 30000;
+  readonly autoRefreshInterval = 30000;
 
 
-  autoRefreshRemainingSeconds: number = 0;
+  autoRefreshRemainingSeconds = 0;
 
 
   private startCaptureTime: number;
   private startCaptureTime: number;
 
 
   private stopCaptureTime: number;
   private stopCaptureTime: number;
 
 
+  captureTimeRangeCache: ListItem;
+
   topUsersGraphData: HomogeneousObject<HomogeneousObject<number>> = {};
   topUsersGraphData: HomogeneousObject<HomogeneousObject<number>> = {};
 
 
   topResourcesGraphData: HomogeneousObject<HomogeneousObject<number>> = {};
   topResourcesGraphData: HomogeneousObject<HomogeneousObject<number>> = {};
@@ -757,16 +758,23 @@ export class LogsContainerService {
     this.stopCaptureTime = new Date().valueOf();
     this.stopCaptureTime = new Date().valueOf();
     this.captureSeconds = 0;
     this.captureSeconds = 0;
     this.stopTimer.next();
     this.stopTimer.next();
-    this.setCustomTimeRange(this.startCaptureTime, this.stopCaptureTime);
     Observable.timer(0, 1000).takeUntil(this.stopAutoRefreshCountdown).subscribe((seconds: number): void => {
     Observable.timer(0, 1000).takeUntil(this.stopAutoRefreshCountdown).subscribe((seconds: number): void => {
       this.autoRefreshRemainingSeconds = autoRefreshIntervalSeconds - seconds;
       this.autoRefreshRemainingSeconds = autoRefreshIntervalSeconds - seconds;
       if (!this.autoRefreshRemainingSeconds) {
       if (!this.autoRefreshRemainingSeconds) {
         this.stopAutoRefreshCountdown.next();
         this.stopAutoRefreshCountdown.next();
+        this.captureTimeRangeCache = this.filtersForm.controls.timeRange.value;
         this.setCustomTimeRange(this.startCaptureTime, this.stopCaptureTime);
         this.setCustomTimeRange(this.startCaptureTime, this.stopCaptureTime);
       }
       }
     });
     });
   }
   }
 
 
+  cancelCapture(): void {
+    this.stopTimer.next();
+    this.stopAutoRefreshCountdown.next();
+    this.autoRefreshRemainingSeconds = 0;
+    this.captureSeconds = 0;
+  }
+
   loadClusters(): void {
   loadClusters(): void {
 
 
   }
   }

+ 6 - 0
ambari-logsearch/ambari-logsearch-web/src/assets/i18n/en.json

@@ -47,7 +47,13 @@
   "filter.users": "Users",
   "filter.users": "Users",
 
 
   "filter.capture": "Capture",
   "filter.capture": "Capture",
+  "filter.captureSnapshot": "Snapshot",
+  "filter.refreshingLogListIn": "Refreshing log list in...",
+  "filter.capture.min": "Min",
+  "filter.capture.sec": "Sec",
   "filter.capture.triggeringRefresh": "Triggering auto-refresh in {{remainingSeconds}} sec",
   "filter.capture.triggeringRefresh": "Triggering auto-refresh in {{remainingSeconds}} sec",
+  "filter.youAreInSnapshotView": "You are in snapshot view",
+  "filter.closeSnapshotView": "Close snapshot view",
 
 
   "filters.clear": "Clear",
   "filters.clear": "Clear",
 
 

+ 3 - 3
ambari-logsearch/ambari-logsearch-web/yarn.lock

@@ -4192,9 +4192,9 @@ negotiator@0.6.1:
   version "0.6.1"
   version "0.6.1"
   resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9"
   resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9"
 
 
-ngx-bootstrap@^1.9.3:
-  version "1.9.3"
-  resolved "https://registry.yarnpkg.com/ngx-bootstrap/-/ngx-bootstrap-1.9.3.tgz#28e75d14fb1beaee609383d7694de4eb3ba03b26"
+ngx-bootstrap@^2.0.5:
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/ngx-bootstrap/-/ngx-bootstrap-2.0.5.tgz#83aab39d1e4fe811fad2b34f7927f9ce19d68daa"
 
 
 no-case@^2.2.0:
 no-case@^2.2.0:
   version "2.3.1"
   version "2.3.1"