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>'].