You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by ab...@apache.org on 2018/01/19 09:12:40 UTC

[ambari] branch trunk updated: AMBARI-22795 LogSearch Fixes for LogList Display. (Istvan Tobias via ababiichuk)

This is an automated email from the ASF dual-hosted git repository.

ababiichuk pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ambari.git


The following commit(s) were added to refs/heads/trunk by this push:
     new b0b6106  AMBARI-22795 LogSearch Fixes for LogList Display. (Istvan Tobias via ababiichuk)
b0b6106 is described below

commit b0b61061b661739b13f0ed729b8dcdc23ec3de04
Author: Istvan Tobias <to...@gmail.com>
AuthorDate: Fri Jan 19 10:12:38 2018 +0100

    AMBARI-22795 LogSearch Fixes for LogList Display. (Istvan Tobias via ababiichuk)
    
    AMBARI-22795 LogSearch Fixes for LogList Display. (Istvan Tobias via ababiichuk)
---
 ambari-logsearch/ambari-logsearch-web/package.json |   1 +
 .../ambari-logsearch-web/src/app/app.module.ts     |   7 +-
 .../log-message/log-message.component.html         |   2 +-
 .../log-message/log-message.component.less         |  27 ++-
 .../log-message/log-message.component.spec.ts      |  14 ++
 .../log-message/log-message.component.ts           |  24 ++-
 .../service-logs-table.component.html              | 183 ++++++++++++-----
 .../service-logs-table.component.less              | 216 ++++++++++++++++++++-
 .../service-logs-table.component.spec.ts           |  17 +-
 .../service-logs-table.component.ts                | 193 +++++++++++++++---
 .../src/app/components/variables.less              |   7 +
 .../ambari-logsearch-web/src/assets/i18n/en.json   |   6 +-
 ambari-logsearch/ambari-logsearch-web/yarn.lock    |   4 +
 13 files changed, 591 insertions(+), 110 deletions(-)

diff --git a/ambari-logsearch/ambari-logsearch-web/package.json b/ambari-logsearch/ambari-logsearch-web/package.json
index 2a3df23..a87f7fc 100644
--- a/ambari-logsearch/ambari-logsearch-web/package.json
+++ b/ambari-logsearch/ambari-logsearch-web/package.json
@@ -26,6 +26,7 @@
     "@ngx-translate/core": "^6.0.1",
     "@ngx-translate/http-loader": "^0.0.3",
     "angular-moment-timezone": "^0.2.1",
+    "angular-pipes": "^6.5.3",
     "angular2-moment": "^1.4.0",
     "bootstrap": "^3.3.7",
     "core-js": "^2.4.1",
diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/app.module.ts b/ambari-logsearch/ambari-logsearch-web/src/app/app.module.ts
index bbf7935..c6c6922 100644
--- a/ambari-logsearch/ambari-logsearch-web/src/app/app.module.ts
+++ b/ambari-logsearch/ambari-logsearch-web/src/app/app.module.ts
@@ -21,12 +21,13 @@ import {NgModule, CUSTOM_ELEMENTS_SCHEMA, Injector} from '@angular/core';
 import {FormsModule, ReactiveFormsModule} from '@angular/forms';
 import {HttpModule, Http, XHRBackend, BrowserXhr, ResponseOptions, XSRFStrategy} from '@angular/http';
 import {InMemoryBackendService} from 'angular-in-memory-web-api';
-import {TypeaheadModule} from 'ngx-bootstrap';
+import {TypeaheadModule, TooltipModule} from 'ngx-bootstrap';
 import {TranslateModule, TranslateLoader} from '@ngx-translate/core';
 import {TranslateHttpLoader} from '@ngx-translate/http-loader';
 import {StoreModule} from '@ngrx/store';
 import {MomentModule} from 'angular2-moment';
 import {MomentTimezoneModule} from 'angular-moment-timezone';
+import {NgStringPipesModule} from 'angular-pipes';
 
 import {environment} from '@envs/environment';
 
@@ -167,6 +168,7 @@ export function getXHRBackend(injector: Injector, browser: BrowserXhr, xsrf: XSR
     ReactiveFormsModule,
     HttpModule,
     TypeaheadModule.forRoot(),
+    TooltipModule.forRoot(),
     TranslateModule.forRoot({
       loader: {
         provide: TranslateLoader,
@@ -176,7 +178,8 @@ export function getXHRBackend(injector: Injector, browser: BrowserXhr, xsrf: XSR
     }),
     StoreModule.provideStore(reducer),
     MomentModule,
-    MomentTimezoneModule
+    MomentTimezoneModule,
+    NgStringPipesModule
   ],
   providers: [
     HttpClientService,
diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/log-message/log-message.component.html b/ambari-logsearch/ambari-logsearch-web/src/app/components/log-message/log-message.component.html
index d4c2902..9e05397 100644
--- a/ambari-logsearch/ambari-logsearch-web/src/app/components/log-message/log-message.component.html
+++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/log-message/log-message.component.html
@@ -20,5 +20,5 @@
   'log-message-container-open': isOpen
   }">
   <button *ngIf="addCaret" (click)="onCaretClick($event)"><i class="caret"></i></button>
-  <div #content class="log-message-content"><ng-content></ng-content></div>
+  <div #content class="log-message-content">{{isOpen ? message : (message | replace: multiLineTestRegexp : ' ')}}</div>
 </div>
diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/log-message/log-message.component.less b/ambari-logsearch/ambari-logsearch-web/src/app/components/log-message/log-message.component.less
index 602d7bd..e716637 100644
--- a/ambari-logsearch/ambari-logsearch-web/src/app/components/log-message/log-message.component.less
+++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/log-message/log-message.component.less
@@ -26,8 +26,10 @@
       transition: transform 250ms;
       transform: rotate(-90deg);
     }
-    &.log-message-container-open .caret {
-      transform: rotate(0deg);
+    &.log-message-container-open {
+      .caret {
+        transform: rotate(0deg);
+      }
     }
 
     .log-message-content {
@@ -35,24 +37,19 @@
       overflow: hidden;
       padding-left: 1em;
       position: relative;
-    }
-    &.log-message-container-open .log-message-content {
-      max-height: none;
-      white-space: pre-wrap;
-      &:before {
-        display: none;
-      }
+      word-wrap: break-word;
     }
     &.log-message-container-collapsible {
       .log-message-content {
+        overflow: hidden;
         padding-left: 0;
-        &:before {
-          content: "...";
-          float: right;
-          margin-left: 1em;
-        }
+        text-overflow: ellipsis;
+        white-space: nowrap;
       }
-
+    }
+    &.log-message-container-open .log-message-content {
+      max-height: none;
+      white-space: pre-wrap;
     }
 
     button, button:active {
diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/log-message/log-message.component.spec.ts b/ambari-logsearch/ambari-logsearch-web/src/app/components/log-message/log-message.component.spec.ts
index edc2515..d2339c9 100644
--- a/ambari-logsearch/ambari-logsearch-web/src/app/components/log-message/log-message.component.spec.ts
+++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/log-message/log-message.component.spec.ts
@@ -15,15 +15,22 @@
  * limitations under the License.
  */
 import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {NgStringPipesModule} from 'angular-pipes';
 
 import {LogMessageComponent} from './log-message.component';
 
 describe('LogMessageComponent', () => {
   let component: LogMessageComponent;
   let fixture: ComponentFixture<LogMessageComponent>;
+  const messages = {
+    noNewLine: 'There is no newline here.',
+    withNewLine: `This is the first line.
+    This is the second one.`
+  };
 
   beforeEach(async(() => {
     TestBed.configureTestingModule({
+      imports: [NgStringPipesModule],
       declarations: [ LogMessageComponent ]
     })
     .compileComponents();
@@ -32,6 +39,7 @@ describe('LogMessageComponent', () => {
   beforeEach(() => {
     fixture = TestBed.createComponent(LogMessageComponent);
     component = fixture.componentInstance;
+    component.message = messages.withNewLine;
     fixture.detectChanges();
   });
 
@@ -61,4 +69,10 @@ describe('LogMessageComponent', () => {
     expect(component.isOpen).toEqual(!currentState);
   });
 
+  it('should set the addCaret prop to TRUE if the message prop has new line character.', () => {
+    component.message = messages.withNewLine;
+    component.checkAddCaret();
+    expect(component['addCaret']).toEqual(true);
+  });
+
 });
diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/log-message/log-message.component.ts b/ambari-logsearch/ambari-logsearch-web/src/app/components/log-message/log-message.component.ts
index b8be61b..2e04758 100644
--- a/ambari-logsearch/ambari-logsearch-web/src/app/components/log-message/log-message.component.ts
+++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/log-message/log-message.component.ts
@@ -51,6 +51,12 @@ export class LogMessageComponent implements AfterViewInit, OnChanges {
   listenChangesOn: any;
 
   /**
+   * This will be shown as log message in the component
+   */
+  @Input()
+  message: string;
+
+  /**
    * This is a private flag to check if it should display the caret or not, it depends on the size of the size of
    * the content container element. Handled by the @checkAddCaret method
    * @type {boolean}
@@ -58,11 +64,20 @@ export class LogMessageComponent implements AfterViewInit, OnChanges {
   private addCaret: boolean = false;
 
   /**
+   * This is a regexp tester to check if the log message is multiline text or single line. Doing by checking the new
+   * line characters.
+   * @type {RegExp}
+   */
+  private readonly multiLineTestRegexp = /\r?\n|\r/;
+
+  /**
    * This is a primary check if the message content does contain new line (/n) characters. If so than we display the
    * caret to give a possibility to the user to see the message as it is (pre-wrapped).
    * @type {boolean}
    */
-  private isMultiLineMessage: boolean = false;
+  private get isMultiLineMessage(): boolean {
+    return this.multiLineTestRegexp.test(this.message);
+  }
 
   constructor(private cdRef:ChangeDetectorRef) {}
 
@@ -82,9 +97,6 @@ export class LogMessageComponent implements AfterViewInit, OnChanges {
    * The goal is to perform a initial caret display check when the component has been initialized.
    */
   ngAfterViewInit(): void {
-    let text = this.content.nativeElement.textContent;
-    let newLinePos = text.indexOf('\n');
-    this.isMultiLineMessage = ((text.length - 1) > newLinePos) && (newLinePos > 0);
     this.checkAddCaret();
   }
 
@@ -95,7 +107,7 @@ export class LogMessageComponent implements AfterViewInit, OnChanges {
    */
   @HostListener('window:resize', ['$event'])
   onWindowResize = (): void => {
-    this.isMultiLineMessage || this.checkAddCaret();
+    this.checkAddCaret();
   };
 
   /**
@@ -104,7 +116,7 @@ export class LogMessageComponent implements AfterViewInit, OnChanges {
    */
   checkAddCaret = (): void =>  {
     let el = this.content.nativeElement;
-    this.addCaret = this.isMultiLineMessage || (el.scrollHeight > el.clientHeight);
+    this.addCaret = this.isMultiLineMessage || (el.scrollHeight > el.clientHeight) || (el.scrollWidth > el.clientWidth);
     this.cdRef.detectChanges();
   };
 
diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.html b/ambari-logsearch/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.html
index 23de664..3cd829e 100644
--- a/ambari-logsearch/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.html
+++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.html
@@ -15,62 +15,141 @@
   limitations under the License.
 -->
 
-<dropdown-button class="pull-right" label="{{'logs.columns' | translate}}" [options]="columns" [isRightAlign]="true"
-                 [isMultipleChoice]="true" action="updateSelectedColumns"
-                 [additionalArgs]="logsTypeMapObject.fieldsModel"></dropdown-button>
-<form *ngIf="logs && logs.length" [formGroup]="filtersForm" class="row pull-right">
-  <filter-dropdown class="col-md-12" label="{{filters.serviceLogsSorting.label | translate}}"
-                   formControlName="serviceLogsSorting" [options]="filters.serviceLogsSorting.options"
-                   [isRightAlign]="true"></filter-dropdown>
-</form>
 <div class="panel panel-default">
-  <div class="panel-body">
-    <table class="table table-hover">
-      <tbody>
-        <ng-container *ngFor="let log of logs; let i = index">
-          <tr *ngIf="i === 0 || isDifferentDates(log.logtime, logs[i - 1].logtime)" class="log-date-row">
-            <th attr.colspan="{{displayedColumns.length + 1}}">
-              {{log.logtime | amTz: timeZone | amDateFormat: dateFormat}}
-            </th>
-          </tr>
-          <tr class="log-item-row">
-            <td class="log-action">
-              <dropdown-button iconClass="fa fa-ellipsis-h action" [hideCaret]="true" [options]="logActions"
-                               [additionalArgs]="[log]"></dropdown-button>
-            </td>
-            <td *ngIf="isColumnDisplayed('logtime')" class="log-time">
-              <time>
-                {{log.logtime | amTz: timeZone | amDateFormat: timeFormat}}
-              </time>
-            </td>
-            <td *ngIf="isColumnDisplayed('level')" [ngClass]="'log-level ' + log.level.toLowerCase()">
-              <log-level [logEntry]="log"></log-level>
-            </td>
-            <td *ngIf="isColumnDisplayed('type')" [ngClass]="'log-type'">
-              {{log.type}}
-            </td>
-            <td *ngIf="isColumnDisplayed('log_message')" [ngClass]="'log-message'" width="*"
-                (contextmenu)="openMessageContextMenu($event)">
-              <log-message [listenChangesOn]="displayedColumns">{{log.log_message}}</log-message>
-            </td>
-            <ng-container *ngFor="let column of displayedColumns">
-              <td *ngIf="customStyledColumns.indexOf(column.value) === -1" [ngClass]="'log-' + column.value">
-                {{log[column.value]}}
+  <div [ngClass]="{'panel-body': true, 'layout-flex': layout==='FLEX', 'layout-table': layout==='TABLE'}">
+    <div class="service-logs-table-controls">
+      <div *ngIf="tooManyColumnsSelected" class="list-layout-warning">
+        <i class="fa fa-warning"></i>
+        {{'logs.brokenListLayoutMessage' | translate}}
+      </div>
+      <form *ngIf="logs && logs.length" [formGroup]="filtersForm">
+        <filter-dropdown label="{{filters.serviceLogsSorting.label | translate}}"
+                         formControlName="serviceLogsSorting" [options]="filters.serviceLogsSorting.options" [isRightAlign]="true">
+        </filter-dropdown>
+      </form>
+      <dropdown-button label="{{'logs.columns' | translate}}" [options]="columns" [isRightAlign]="true"
+                       [isMultipleChoice]="true" action="updateSelectedColumns" [additionalArgs]="logsTypeMapObject.fieldsModel">
+      </dropdown-button>
+      <div class="layout-btn-group">
+        <a *ngIf="layout==='FLEX'" class="btn" (click)="toggleShowLabels()" tooltip="{{'logs.toggleLabels' | translate}}">
+          <i [ngClass]="{'fa': true, 'fa-toggle-on': showLabels, 'fa-toggle-off': !showLabels}"></i>
+        </a>
+        <a class="btn" (click)="setLayout('TABLE')" tooltip="{{'logs.tableLayoutBtnTooltip' | translate}}">
+          <i [ngClass]="{'fa': true, 'fa-th-list': true, 'active': layout=='TABLE'}"></i>
+        </a>
+        <a class="btn" (click)="setLayout('FLEX')" tooltip="{{'logs.flexLayoutBtnTooltip' | translate}}">
+          <i [ngClass]="{'fa': true, 'fa-th': true, 'active': layout=='FLEX'}"></i>
+        </a>
+      </div>
+    </div>
+    <div *ngIf="layout=='TABLE'" [ngClass]="{'log-list-table-container':true, 'broken-layout': tooManyColumnsSelected}" #tableWrapperEl>
+      <table [ngClass]="{'table': true, 'table-hover': true}" #tableListEl
+             tooltip="{{tooManyColumnsSelected ? ('logs.brokenListLayoutTooltip' | translate) : ''}}"
+             placement="top" containerClass="tooltip-warning">
+        <colgroup>
+          <col class="log-action">
+          <col *ngIf="isColumnDisplayed('logtime')" class="log-time">
+          <col *ngIf="isColumnDisplayed('level')" class="log-level">
+          <col *ngIf="isColumnDisplayed('type')" class="log-type">
+          <ng-container *ngFor="let column of displayedColumns">
+            <col *ngIf="customStyledColumns.indexOf(column.value) === -1" [ngClass]="'col-default-fixed log-' + column.value">
+          </ng-container>
+          <col *ngIf="isColumnDisplayed('path')" class="log-path">
+          <col *ngIf="isColumnDisplayed('log_message')" class="log-message">
+        </colgroup>
+        <tbody>
+          <ng-container *ngFor="let log of logs; let i = index">
+            <tr *ngIf="i === 0 || isDifferentDates(log.logtime, logs[i - 1].logtime)" class="log-date-row">
+              <th attr.colspan="{{displayedColumns.length + 1}}">
+                {{log.logtime | amTz: timeZone | amDateFormat: dateFormat}}
+              </th>
+            </tr>
+            <tr class="log-item-row">
+              <td class="log-action">
+                <dropdown-button iconClass="fa fa-ellipsis-v action" [hideCaret]="true" [options]="logActions"
+                                 [additionalArgs]="[log]" [showSelectedValue]="false"></dropdown-button>
               </td>
-            </ng-container>
+              <td *ngIf="isColumnDisplayed('logtime')" class="log-time">
+                <time>
+                  {{log.logtime | amTz: timeZone | amDateFormat: timeFormat}}
+                </time>
+              </td>
+              <td *ngIf="isColumnDisplayed('level')" [ngClass]="'log-level ' + log.level.toLowerCase()">
+                <log-level [logEntry]="log"></log-level>
+              </td>
+              <td *ngIf="isColumnDisplayed('type')" [ngClass]="'log-type'">
+                {{log.type}}
+              </td>
+              <ng-container *ngFor="let column of displayedColumns">
+                <td *ngIf="customStyledColumns.indexOf(column.value) === -1"
+                    [ngClass]="'log-' + column.value">{{log[column.value]}}</td>
+              </ng-container>
+              <td *ngIf="isColumnDisplayed('path')" [ngClass]="'log-path'">
+                {{log.path}}
+              </td>
+              <td *ngIf="isColumnDisplayed('log_message')" [ngClass]="'log-message'" width="*"
+                  (contextmenu)="openMessageContextMenu($event)">
+                <log-message [listenChangesOn]="displayedColumns" [message]="log.log_message"></log-message>
+              </td>
+            </tr>
+          </ng-container>
+        </tbody>
+        <tfoot>
+          <tr>
+            <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>
+            </td>
           </tr>
+        </tfoot>
+      </table>
+    </div>
+    <ng-container *ngIf="layout=='FLEX'">
+      <div [ngClass]="{'log-list': true, 'show-labels': showLabels}">
+        <ng-container *ngFor="let log of logs; let i = index">
+          <div *ngIf="i === 0 || isDifferentDates(log.logtime, logs[i - 1].logtime)" class="log-date-row">
+            {{log.logtime | amTz: timeZone | amDateFormat: dateFormat}}
+          </div>
+          <div [ngClass]="{'log-row': true, 'odd': i % 2, 'even': !(i % 2)}">
+            <div class="log-header">
+              <div class="log-action">
+                <dropdown-button iconClass="fa fa-ellipsis-v action" [hideCaret]="true" [options]="logActions"
+                                 [additionalArgs]="[log]" [showSelectedValue]="false"></dropdown-button>
+              </div>
+              <div *ngIf="isColumnDisplayed('level')" [ngClass]="'log-level ' + log.level.toLowerCase()">
+                <log-level [logEntry]="log"></log-level>
+              </div>
+              <div *ngIf="isColumnDisplayed('type')" [ngClass]="'log-type'" [title]="log.type">
+                <label *ngIf="showLabels">{{getLabelForField('type') | translate}}</label>
+                {{log.type}}
+              </div>
+              <div *ngIf="isColumnDisplayed('path')" [ngClass]="'log-path'" [title]="log.path">
+                <label *ngIf="showLabels">{{getLabelForField('path') | translate}}</label>
+                {{log.path}}
+              </div>
+              <ng-container *ngFor="let column of displayedColumns">
+                <div *ngIf="customStyledColumns.indexOf(column.value) === -1" [ngClass]="'log-' + (column.value | lowercase)"
+                     [title]="(log[column.value] || '')">
+                  <label *ngIf="showLabels">{{getLabelForField(column.value) | translate}}</label>
+                  {{log[column.value]}}
+                </div>
+              </ng-container>
+              <div *ngIf="isColumnDisplayed('logtime')" class="log-time">
+                <time>
+                  {{log.logtime | amTz: timeZone | amDateFormat: timeFormat}}
+                </time>
+              </div>
+            </div>
+            <div *ngIf="isColumnDisplayed('log_message')" class="log-message" (contextmenu)="openMessageContextMenu($event)">
+              <log-message [listenChangesOn]="displayedColumns" [message]="log.log_message"></log-message>
+            </div>
+          </div>
         </ng-container>
-      </tbody>
-      <tfoot>
-        <tr>
-          <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>
-          </td>
-        </tr>
-      </tfoot>
-    </table>
+      </div>
+      <pagination class="col-md-12" *ngIf="logs && logs.length" [totalCount]="totalCount" [filtersForm]="filtersForm"
+                  [filterInstance]="filters.pageSize" [currentCount]="logs.length"></pagination>
+    </ng-container>
     <context-menu [isDisplayed]="isContextMenuDisplayed" [contextMenuItems]="contextMenuItems"
                   [leftPosition]="contextMenuLeft" [topPosition]="contextMenuTop" (itemSelect)="updateQuery($event)"
                   (contextMenuDismiss)="onContextMenuDismiss()"></context-menu>
diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.less b/ambari-logsearch/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.less
index dfa1889..27fafe5 100644
--- a/ambari-logsearch/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.less
+++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.less
@@ -18,17 +18,85 @@
 @import '../mixins';
 
 :host {
-  /deep/ filter-dropdown {
+
+  .service-logs-table-controls {
+    display: flex;
+    flex-wrap: wrap;
     justify-content: flex-end;
+    .layout-btn-group {
+      display: flex;
+      align-items: center;
+      .btn {
+        padding: .2em;
+        display: flex;
+        align-items: center;
+        i {
+          cursor: pointer;
+          margin: 0 .25em;
+          &.active {
+            color: @submit-color;
+          }
+        }
+      }
+    }
   }
 
   .panel-body {
-    overflow: hidden;
     width: 100%;
   }
-
+  .log-list-table-container {
+    width: 100%;
+    overflow-x: hidden;
+  }
   table {
+    min-width: 100%;
+    table-layout: fixed;
     width: 100%;
+    empty-cells: hide;
+    /deep/ col {
+      overflow: hidden;
+      text-overflow: ellipsis;
+      &.log-action {
+        overflow: visible;
+        padding-left: .25em;
+        padding-right: 0;
+        width: 1em;
+      }
+      &.log-time {
+        width: 7em;
+        padding-left: 0;
+        text-align: right;
+      }
+      &.log-level {
+        text-transform: uppercase;
+        width: 8em;
+      }
+      &.log-type {
+        color: @link-color;
+        width: 12em;
+      }
+      &.log-path {
+        overflow: hidden;
+        text-overflow: ellipsis;
+        width: 20em;
+      }
+      &.log-message {
+        width: 100%;
+      }
+      &.col-default-fixed {
+        width: 8em;
+      }
+      &.log-event_count {
+        width: 3em !important;
+      }
+      &.col-checkpoint {
+        padding: 0;
+        width: 1px;
+      }
+    }
+    tfoot td {
+      overflow: visible;
+    }
   }
 
   tr.log-date-row, tr.log-date-row:hover {
@@ -42,9 +110,15 @@
     background: none transparent;
   }
 
-  td {
+  table td {
+    text-overflow: ellipsis;
+    overflow: hidden;
+    word-wrap: break-word;
     &.log-action {
-      min-width: 3em;
+      overflow: visible;
+      padding-left: .25em;
+      padding-right: 0;
+      width: 1em;
       /deep/ .btn, /deep/ .filter-label {
         font-size: 1em;
         height: auto;
@@ -54,6 +128,7 @@
     }
     &.log-time {
       min-width: 7em;
+      padding-left: 0;
       text-align: right;
     }
     &.log-level {
@@ -64,9 +139,12 @@
     &.log-type {
       color: @link-color;
     }
-    &.log-message, &.log-path {
+    &.log-message {
       width: 100%;
     }
+    &.log-event_count {
+      width: 3em;
+    }
   }
 
   tr:hover td.log-action {
@@ -89,6 +167,132 @@
     }
   }
 
+  .list-layout-warning {
+    align-items: center;
+    color: @warning-color;
+    display: flex;
+    flex: 1;
+    font-size: .7em;
+    i {
+      margin-right: .6em;
+    }
+  }
+
+  /deep/ .tooltip {
+    font-size: .75em;
+    .tooltip-inner {
+      background-color: rgba(50, 50, 50, 1);
+    }
+    .tooltip-arrow {
+      border-top-color: rgba(50, 50, 50, 1);
+    }
+  }
+
+  .layout-flex {
+    .log-list {
+      color: @base-font-color;
+      border-bottom: 1px solid @log-list-border-color;
+      font-size: @log-list-font-size;
+      .log-date-row {
+        background: @list-header-background-color;
+        padding: @log-list-row-data-padding;
+      }
+      .log-row {
+        border: 1px solid transparent;
+        border-bottom: 1px solid @log-list-border-color;
+        display: block;
+        padding-bottom: .5em;
+        transition: all 100ms;
+        &:hover {
+          background: @log-list-row-hover-background-color;
+          border-color: @log-list-row-hover-border-color;
+          > div.log-header .log-action  /deep/ .btn {
+            opacity: 1;
+          }
+        }
+        &:first-of-type {
+          border-top-color: transparent;
+        }
+        &:last-of-type {
+          border-bottom-color: transparent;
+        }
+        div {
+          padding: (@log-list-row-data-padding / 2) @log-list-row-data-padding;
+        }
+        > div.log-header, > div.details {
+          padding: 0;
+        }
+        > div.log-header {
+          display: flex;
+          > div {
+            height: 2em;
+            text-overflow: ellipsis;
+            overflow: hidden;
+          }
+          .log-level {
+            align-items: center;
+            display: flex;
+            padding-right: 0;
+            text-transform: uppercase;
+            width: 7em;
+            .log-colors;
+          }
+          .log-type {
+            color: @link-color;
+          }
+          .log-time {
+            flex: 1 0 auto;
+            max-width: none;
+            min-width: 6em;
+            text-align: right;
+          }
+          .log-action {
+            display: flex;
+            justify-content: flex-end;
+            max-width: none;
+            overflow: visible;
+            padding-left: 5px;
+            padding-right: 0;
+            > * {
+              display: inline-block;
+            }
+            /deep/ .btn {
+              opacity: 0;
+              overflow: hidden;
+              transition: opacity 50ms;
+            }
+            /deep/ .btn, /deep/ .filter-label {
+              font-size: 1em;
+              height: auto;
+              line-height: 1em;
+              padding: 0;
+            }
+          }
+        }
+        .log-message {
+          flex: 1 1 auto;
+          max-width: none;
+          overflow: hidden;
+          padding: .25em 0;
+          width: 100%;
+        }
+        .log-path {
+          max-width: none;
+        }
+        label {
+          color: lighten(@base-font-color, 25%);
+          display: block;
+          font-size: .7em;
+          margin: 0;
+          padding: 0;
+        }
+      }
+      &.show-labels > .log-row > div.log-header > div {
+        height: 2.5em;
+      }
+    }
+  }
+
   .context-menu {
     position: fixed;
   }
diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.spec.ts b/ambari-logsearch/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.spec.ts
index 05420ff..e883c99 100644
--- a/ambari-logsearch/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.spec.ts
+++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.spec.ts
@@ -22,6 +22,7 @@ import {FormsModule, ReactiveFormsModule} from '@angular/forms';
 import {StoreModule} from '@ngrx/store';
 import {MomentModule} from 'angular2-moment';
 import {MomentTimezoneModule} from 'angular-moment-timezone';
+import {TooltipModule} from 'ngx-bootstrap';
 import {TranslationModules} from '@app/test-config.spec';
 import {AuditLogsService, auditLogs} from '@app/services/storage/audit-logs.service';
 import {ServiceLogsService, serviceLogs} from '@app/services/storage/service-logs.service';
@@ -47,7 +48,7 @@ import {AuthService} from '@app/services/auth.service';
 import {PaginationComponent} from '@app/components/pagination/pagination.component';
 import {DropdownListComponent} from '@app/components/dropdown-list/dropdown-list.component';
 
-import {ServiceLogsTableComponent} from './service-logs-table.component';
+import {ServiceLogsTableComponent, ListLayout} from './service-logs-table.component';
 
 describe('ServiceLogsTableComponent', () => {
   let component: ServiceLogsTableComponent;
@@ -88,7 +89,8 @@ describe('ServiceLogsTableComponent', () => {
           clusters,
           components,
           hosts
-        })
+        }),
+        TooltipModule.forRoot()
       ],
       providers: [
         LogsContainerService,
@@ -128,4 +130,15 @@ describe('ServiceLogsTableComponent', () => {
   it('should create component', () => {
     expect(component).toBeTruthy();
   });
+
+  it('should change the layout to TABLE', () => {
+    component.setLayout(ListLayout.Table);
+    expect(component.layout).toEqual(ListLayout.Table);
+  });
+
+  it('should change the layout to FLEX', () => {
+    component.setLayout(ListLayout.Flex);
+    expect(component.layout).toEqual(ListLayout.Flex);
+  });
+
 });
diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.ts b/ambari-logsearch/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.ts
index 141c1ab..681149d 100644
--- a/ambari-logsearch/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.ts
+++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.ts
@@ -16,23 +16,86 @@
  * limitations under the License.
  */
 
-import {Component} from '@angular/core';
+import {Component, AfterViewInit, AfterViewChecked, ViewChild, ElementRef, Input, ChangeDetectorRef} from '@angular/core';
+
 import {ListItem} from '@app/classes/list-item';
 import {LogsTableComponent} from '@app/classes/components/logs-table/logs-table-component';
 import {LogsContainerService} from '@app/services/logs-container.service';
 import {UtilsService} from '@app/services/utils.service';
 
+export enum ListLayout {
+  Table = 'TABLE',
+  Flex = 'FLEX'
+}
+
 @Component({
   selector: 'service-logs-table',
   templateUrl: './service-logs-table.component.html',
   styleUrls: ['./service-logs-table.component.less']
 })
-export class ServiceLogsTableComponent extends LogsTableComponent {
+export class ServiceLogsTableComponent extends LogsTableComponent implements AfterViewChecked {
 
-  constructor(private logsContainer: LogsContainerService, private utils: UtilsService) {
+  constructor(
+    private logsContainer: LogsContainerService,
+    private utils: UtilsService,
+    private cdRef:ChangeDetectorRef
+  ) {
     super();
   }
 
+  ngAfterViewChecked() {
+    this.checkListLayout();
+    this.cdRef.detectChanges();
+  }
+
+  /**
+   * The element reference is used to check if the table is broken or not.
+   */
+  @ViewChild('tableListEl', {
+    read: ElementRef
+  })
+  private tableListElRef: ElementRef;
+
+  /**
+   * The element reference is used to check if the table is broken or not.
+   */
+  @ViewChild('tableWrapperEl', {
+    read: ElementRef
+  })
+  private tableWrapperElRef: ElementRef;
+
+  /**
+   * We only show the labels in flex layout when this property is TRUE.
+   * @type {boolean}
+   */
+  @Input()
+  showLabels: boolean = false;
+
+  /**
+   * The minimum width for the log message column. It is used when we check if the layout is broken or not.
+   * @type {number}
+   */
+  @Input()
+  logMessageColumnMinWidth: number = 175;
+
+  /**
+   * We use this property in the broken table layout check process when the log message is displayed.
+   * @type {string}
+   */
+  @Input()
+  logMessageColumnCssSelector: string = 'tbody tr td.log-message';
+
+  /**
+   * Set the layout for the list.
+   * It can be:
+   * 'TABLE': good for comparison, but it is not useful whe the user wants to display too much fields
+   * 'FLEX': flexible layout (with flex box) is good for display lot of column or display the log list on a relative
+   * narrow display.
+   * @type {Layout}
+   */
+  @Input()
+  layout: ListLayout = ListLayout.Table;
+
   readonly dateFormat: string = 'dddd, MMMM Do';
 
   readonly timeFormat: string = 'h:mm:ss A';
@@ -55,17 +118,41 @@ export class ServiceLogsTableComponent extends LogsTableComponent {
     }
   ];
 
-  readonly customStyledColumns: string[] = ['level', 'type', 'logtime', 'log_message'];
+  readonly customStyledColumns: string[] = ['level', 'type', 'logtime', 'log_message', 'path'];
+
+  get contextMenuItems(): ListItem[] {
+    return this.logsContainer.queryContextMenuItems;
+  }
 
   private readonly messageFilterParameterName: string = 'log_message';
 
-  private contextMenuElement: HTMLElement;
+  /**
+   * The goal is to show or hide the context menu on right click.
+   * @type {boolean}
+   */
+  private isContextMenuDisplayed: boolean = false;
+
+  /**
+   * 'left' CSS property value for context menu dropdown
+   * @type {number}
+   */
+  private contextMenuLeft: number = 0;
+
+  /**
+   * 'top' CSS property value for context menu dropdown
+   * @type {number}
+   */
+  private contextMenuTop:number = 0;
 
   private selectedText: string = '';
 
-  get contextMenuItems(): ListItem[] {
-    return this.logsContainer.queryContextMenuItems;
-  }
+
+  /**
+   * 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 timeZone(): string {
     return this.logsContainer.timeZone;
@@ -79,22 +166,6 @@ export class ServiceLogsTableComponent extends LogsTableComponent {
     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 {
     return this.utils.isDifferentDates(dateA, dateB, this.timeZone);
   }
@@ -102,6 +173,7 @@ export class ServiceLogsTableComponent extends LogsTableComponent {
   openMessageContextMenu(event: MouseEvent): void {
     const selectedText = getSelection().toString();
     if (selectedText) {
+      this.isContextMenuDisplayed = true;
       this.contextMenuLeft = event.clientX;
       this.contextMenuTop = event.clientY;
       this.selectedText = selectedText;
@@ -117,8 +189,79 @@ export class ServiceLogsTableComponent extends LogsTableComponent {
     });
   }
 
-  onContextMenuDismiss(): void {
+  /**
+   * Handle the event when the contextual menu component hide itself.
+   */
+  private onContextMenuDismiss = (): void => {
+    this.isContextMenuDisplayed = false;
     this.selectedText = '';
+  };
+
+  /**
+   * The goal is to check if the log message column is readable or not. Doing this by checking if it is displayed or not
+   * and by checking the current width and comparing with the minimum configured width.
+   * @returns {boolean}
+   */
+  isLogMessageVisible(): boolean {
+    let visible:boolean = this.isColumnDisplayed('log_message');
+    if (this.logs.length && visible && this.layout === ListLayout.Table) {
+      const tableElement: HTMLElement = this.tableListElRef.nativeElement;
+      const lastTdElement = (tableElement && <HTMLElement>tableElement.querySelectorAll(this.logMessageColumnCssSelector)[0]) || undefined;
+      const minWidth = parseFloat(window.getComputedStyle(lastTdElement).minWidth) || this.logMessageColumnMinWidth;
+      const lastTdElementInfo = lastTdElement.getBoundingClientRect();
+      visible = lastTdElementInfo.width >= minWidth;
+    }
+    return visible;
+  }
+
+  /**
+   * Check if the log list (table) fits its container. The goal is to decide if the layout is broken or not.
+   * @returns {boolean}
+   */
+  isLogListFitToTheContainer(): boolean {
+    let result = this.layout === ListLayout.Flex;
+    if (!result) {
+      const tableElement: HTMLElement = this.tableListElRef.nativeElement;
+      const tableElementInfo = tableElement.getBoundingClientRect();
+      const wrapperElement: HTMLElement = this.tableWrapperElRef.nativeElement;
+      const wrapperElementInfo = wrapperElement.getBoundingClientRect();
+      result = wrapperElementInfo.width >= tableElementInfo.width;
+    }
+    return result;
+  }
+
+  /**
+   * The goal of this function is to check either the log message column is readable if displayed or the all table
+   * columns are visible otherwise.
+   */
+  private checkListLayout(): void {
+    this.tooManyColumnsSelected = this.isColumnDisplayed('log_message') ? !this.isLogMessageVisible() : !this.isLogListFitToTheContainer();
+  }
+
+  /**
+   * The goal is to enable the layout change to the user so that he/she can decide which view is more readable.
+   * @param {Layout} layout
+   */
+  public setLayout(layout: ListLayout): void {
+    this.layout = layout;
+  }
+
+  /**
+   * Find the label for the given field in the @columns ListItem array
+   * @param {string} field
+   * @returns {string}
+   */
+  private getLabelForField(field: string): string {
+    const column: ListItem = this.columns.find(column => column.value === field);
+    return column && column.label;
+  }
+
+  /**
+   * Toggle the true/false value of the showLabels property. The goal is to show/hide the labels in the flex box layout,
+   * so that the user can decide if he/she wants to see the labels and lost some space.
+   */
+  private toggleShowLabels(): void {
+    this.showLabels = !this.showLabels;
   }
 
 }
diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/variables.less b/ambari-logsearch/ambari-logsearch-web/src/app/components/variables.less
index 9b9bbfd..f26f5ca 100644
--- a/ambari-logsearch/ambari-logsearch-web/src/app/components/variables.less
+++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/variables.less
@@ -68,3 +68,10 @@
 
 // Graph
 @graph-padding: .5rem;
+
+// Log list
+@log-list-row-data-padding: 8px;
+@log-list-font-size: 13px;
+@log-list-row-hover-background-color: #E7F6FC;
+@log-list-row-hover-border-color: #A7DFF2;
+@log-list-border-color: rgb(238, 238, 238);
diff --git a/ambari-logsearch/ambari-logsearch-web/src/assets/i18n/en.json b/ambari-logsearch/ambari-logsearch-web/src/assets/i18n/en.json
index 923160a..2b34b4d 100644
--- a/ambari-logsearch/ambari-logsearch-web/src/assets/i18n/en.json
+++ b/ambari-logsearch/ambari-logsearch-web/src/assets/i18n/en.json
@@ -161,7 +161,11 @@
   "logs.showGraph": "Show Graph",
   "logs.topUsers": "Top {{number}} Users",
   "logs.topResources": "Top {{number}} Resources",
-  "logs.duration": "Duration",
+  "logs.brokenListLayoutMessage": "Some information may not be visible.",
+  "logs.brokenListLayoutTooltip": "It seems that your screen is too narrow to display this number of columns.",
+  "logs.tableLayoutBtnTooltip": "Table layout. Optimal when you want to display only few columns.",
+  "logs.flexLayoutBtnTooltip": "Flexible layout. Optimal when your screen is narrow or you want to display more columns.",
+  "logs.toggleLabels": "Turn on/off the labels.",
 
   "histogram.gap": "gap",
   "histogram.gaps": "gaps",
diff --git a/ambari-logsearch/ambari-logsearch-web/yarn.lock b/ambari-logsearch/ambari-logsearch-web/yarn.lock
index aed2c50..9eb5d04 100644
--- a/ambari-logsearch/ambari-logsearch-web/yarn.lock
+++ b/ambari-logsearch/ambari-logsearch-web/yarn.lock
@@ -512,6 +512,10 @@ angular-moment-timezone@^0.2.1:
     rxjs "^5.1.0"
     zone.js "^0.8.4"
 
+angular-pipes@^6.5.3:
+  version "6.5.3"
+  resolved "https://registry.yarnpkg.com/angular-pipes/-/angular-pipes-6.5.3.tgz#6bed37c51ebc2adaf3412663bfe25179d0489b02"
+
 angular2-moment@^1.3.3:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/angular2-moment/-/angular2-moment-1.4.0.tgz#3d59c1ebc28934fcfe9b888ab461e261724987e8"

-- 
To stop receiving notification emails like this one, please contact
['"commits@ambari.apache.org" <co...@ambari.apache.org>'].