import { TreeModule } from 'ng2-tree';
import { AlertModule } from 'ngx-bootstrap/alert';
+import { BsDatepickerModule } from 'ngx-bootstrap/datepicker';
import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
import { ModalModule } from 'ngx-bootstrap/modal';
import { TabsModule } from 'ngx-bootstrap/tabs';
+import { TimepickerModule } from 'ngx-bootstrap/timepicker';
import { TooltipModule } from 'ngx-bootstrap/tooltip';
import { SharedModule } from '../../shared/shared.module';
AlertModule.forRoot(),
TooltipModule.forRoot(),
TreeModule,
- MgrModulesModule
+ MgrModulesModule,
+ TimepickerModule.forRoot(),
+ BsDatepickerModule.forRoot()
],
declarations: [
HostsComponent,
<div *ngIf="contentData">
+<ng-container *ngTemplateOutlet="logFiltersTpl"></ng-container>
<tabset>
<tab i18n-heading
heading="Cluster Logs">
<div class="well">
- <div *ngIf="contentData.clog">
- <p *ngFor="let line of contentData.clog">
+ <div *ngIf="clog">
+ <p *ngFor="let line of clog">
<span class="timestamp">{{ line.stamp }}</span>
<span class="priority {{ line.priority | logPriority }}">{{ line.priority }}</span>
<span class="message">{{ line.message }}</span>
<tab i18n-heading
heading="Audit Logs">
<div class="well">
- <div *ngIf="contentData.audit_log">
- <p *ngFor="let line of contentData.audit_log">
+ <div *ngIf="audit_log">
+ <p *ngFor="let line of audit_log">
<span class="timestamp">{{ line.stamp }}</span>
<span class="priority {{ line.priority | logPriority }}">{{ line.priority }}</span>
<span class="message">{{ line.message }}</span>
</tab>
</tabset>
</div>
+
+<ng-template #logFiltersTpl>
+ <div class="row log-filters">
+ <div class="col-xs-4 col-md-2 cd-col-1 filter-box">
+ <label i18n>Priority:</label>
+ <select class="form-control"
+ [(ngModel)]="priority"
+ (ngModelChange)="filterLogs()">
+ <option class="form-control"
+ *ngFor="let prio of prioritys"
+ [value]="prio.value">{{ prio.name }}</option>
+ </select>
+ </div>
+ <div class="col-xs-4 col-md-3 cd-col-3 filter-box">
+ <label i18n>Keyword:</label>
+ <div class="input-group">
+ <span class="input-group-addon">
+ <i class="glyphicon glyphicon-search"></i>
+ </span>
+ <input class="form-control"
+ type="text"
+ [(ngModel)]="search"
+ (keyup)="filterLogs()">
+ <span class="input-group-btn">
+ <button type="button"
+ class="btn btn-default clear-input tc_clearInputBtn"
+ (click)="clearSearchKey()">
+ <i class="icon-prepend fa fa-remove"></i>
+ </button>
+ </span>
+ </div>
+ </div>
+ <div class="col-xs-4 col-md-3 cd-col-2 filter-box">
+ <label i18n>Date:</label>
+ <div class="input-group">
+ <input type="text"
+ class="form-control"
+ i18n-placeholder
+ placeholder="Datepicker"
+ [bsConfig]="bsConfig"
+ bsDatepicker
+ [(ngModel)]="selectedDate"
+ (ngModelChange)="filterLogs()">
+ <span class="input-group-btn">
+ <button type="button"
+ class="btn btn-default clear-input tc_clearInputBtn"
+ (click)="clearDate()">
+ <i class="icon-prepend fa fa-remove"></i>
+ </button>
+ </span>
+ </div>
+ </div>
+ <div class="clearfix visible-xs-block"></div>
+ <div class="col-xs-8 col-md-4 cd-col-4 filter-box time-box">
+ <label i18n>Time range:</label>
+ <timepicker [showMeridian]="false"
+ [showSpinners]="showSpinners"
+ [minuteStep]="1"
+ [(ngModel)]="startTime"
+ (ngModelChange)="filterLogs()">
+ </timepicker>
+ <span> — </span>
+ <timepicker [showMeridian]="false"
+ [showSpinners]="showSpinners"
+ [minuteStep]="1"
+ [(ngModel)]="endTime"
+ (ngModelChange)="filterLogs()">
+ </timepicker>
+ </div>
+ </div>
+</ng-template>
color: $color-brand-teal;
}
}
+
+::ng-deep timepicker table tbody tr td {
+ .bs-timepicker-field {
+ width: 3.5rem;
+ font-size: 1rem;
+ padding: 4px 6px;
+ }
+ .btn {
+ font-size: 1rem;
+ }
+}
+
+.log-filters {
+ margin-bottom: 5px;
+ padding: 0 30px;
+ * {
+ box-sizing: border-box;
+ }
+
+ .filter-box {
+ margin: 0;
+ padding: 0 15px 5px 0;
+ display: -webkit-flex;
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+ label {
+ padding-top: 5px;
+ padding-right: 5px;
+ }
+ }
+
+ @media (max-width: 991px) {
+ .time-box {
+ margin-top: 20px;
+ }
+ }
+
+ @media (min-width: 1200px) {
+ .cd-col-4 {
+ width: 28vw;
+ }
+
+ .cd-col-3 {
+ width: 20vw;
+ }
+
+ .cd-col-2 {
+ width: 16vw;
+ }
+ .cd-col-1 {
+ width: 14vw;
+ }
+ }
+
+ @media (min-width: 1400px) {
+ .cd-col-4 {
+ width: 24vw;
+ }
+
+ .cd-col-3 {
+ width: 18vw;
+ }
+
+ .cd-col-2 {
+ width: 14vw;
+ }
+ .cd-col-1 {
+ width: 12vw;
+ }
+ }
+
+ @media (min-width: 1600px) {
+ .cd-col-4 {
+ width: 22vw;
+ }
+
+ .cd-col-3 {
+ width: 16vw;
+ }
+
+ .cd-col-2 {
+ width: 12vw;
+ }
+ .cd-col-1 {
+ width: 10vw;
+ }
+ }
+
+ @media (min-width: 1800px) {
+ .cd-col-3 {
+ width: 14vw;
+ }
+
+ .cd-col-2 {
+ width: 11vw;
+ }
+ .cd-col-1 {
+ width: 9vw;
+ }
+ }
+}
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormsModule } from '@angular/forms';
+import { BsDatepickerModule } from 'ngx-bootstrap/datepicker';
import { TabsModule } from 'ngx-bootstrap/tabs';
+import { TimepickerModule } from 'ngx-bootstrap/timepicker';
import { configureTestBed } from '../../../../testing/unit-test-helper';
import { SharedModule } from '../../../shared/shared.module';
let fixture: ComponentFixture<LogsComponent>;
configureTestBed({
- imports: [HttpClientTestingModule, TabsModule.forRoot(), SharedModule],
+ imports: [
+ HttpClientTestingModule,
+ TabsModule.forRoot(),
+ SharedModule,
+ BsDatepickerModule.forRoot(),
+ TimepickerModule.forRoot(),
+ FormsModule
+ ],
declarations: [LogsComponent]
});
it('should create', () => {
expect(component).toBeTruthy();
});
+
+ describe('abstractfilters', () => {
+ it('after initializaed', () => {
+ const filters = component.abstractfilters();
+ expect(filters.priority).toBe('All');
+ expect(filters.key).toBe('');
+ expect(filters.yearMonthDay).toBe('');
+ expect(filters.sTime).toBe(0);
+ expect(filters.eTime).toBe(1439);
+ });
+ it('change date', () => {
+ component.selectedDate = new Date(2019, 0, 1);
+ component.startTime = new Date(2019, 1, 1, 1, 10);
+ component.endTime = new Date(2019, 1, 1, 12, 10);
+ const filters = component.abstractfilters();
+ expect(filters.yearMonthDay).toBe('2019-01-01');
+ expect(filters.sTime).toBe(70);
+ expect(filters.eTime).toBe(730);
+ });
+ });
+
+ describe('filterLogs', () => {
+ const contentData = {
+ clog: [
+ {
+ name: 'priority',
+ stamp: '2019-02-21 09:39:49.572801',
+ message: 'Manager daemon localhost is now available',
+ priority: '[ERR]'
+ },
+ {
+ name: 'search',
+ stamp: '2019-02-21 09:39:49.572801',
+ message: 'Activating manager daemon localhost',
+ priority: '[INF]'
+ },
+ {
+ name: 'date',
+ stamp: '2019-01-21 09:39:49.572801',
+ message: 'Manager daemon localhost is now available',
+ priority: '[INF]'
+ },
+ {
+ name: 'time',
+ stamp: '2019-02-21 01:39:49.572801',
+ message: 'Manager daemon localhost is now available',
+ priority: '[INF]'
+ }
+ ],
+ audit_log: []
+ };
+ const resetFilter = () => {
+ component.selectedDate = null;
+ component.priority = 'All';
+ component.search = '';
+ component.startTime.setHours(0, 0);
+ component.endTime.setHours(23, 59);
+ };
+ beforeEach(() => {
+ component.contentData = contentData;
+ });
+
+ it('show all log', () => {
+ component.filterLogs();
+ expect(component.clog.length).toBe(4);
+ });
+
+ it('filter by search key', () => {
+ resetFilter();
+ component.search = 'Activating';
+ component.filterLogs();
+ expect(component.clog.length).toBe(1);
+ expect(component.clog[0].name).toBe('search');
+ });
+
+ it('filter by date', () => {
+ resetFilter();
+ component.selectedDate = new Date(2019, 0, 21);
+ component.filterLogs();
+ expect(component.clog.length).toBe(1);
+ expect(component.clog[0].name).toBe('date');
+ });
+
+ it('filter by priority', () => {
+ resetFilter();
+ component.priority = '[ERR]';
+ component.filterLogs();
+ expect(component.clog.length).toBe(1);
+ expect(component.clog[0].name).toBe('priority');
+ });
+
+ it('filter by time range', () => {
+ resetFilter();
+ component.startTime.setHours(1, 0);
+ component.endTime.setHours(2, 0);
+ component.filterLogs();
+ expect(component.clog.length).toBe(1);
+ expect(component.clog[0].name).toBe('time');
+ });
+ });
});
})
export class LogsComponent implements OnInit, OnDestroy {
contentData: any;
- interval: number;
+ clog: Array<any>;
+ audit_log: Array<any>;
- constructor(private logsService: LogsService) {}
+ interval: number;
+ bsConfig = {
+ dateInputFormat: 'YYYY-MM-DD',
+ containerClass: 'theme-default'
+ };
+ prioritys: Array<{ name: string; value: string }> = [
+ { name: 'Info', value: '[INF]' },
+ { name: 'Warning', value: '[WRN]' },
+ { name: 'Error', value: '[ERR]' },
+ { name: 'All', value: 'All' }
+ ];
+ priority = 'All';
+ search = '';
+ selectedDate: Date;
+ startTime: Date = new Date();
+ endTime: Date = new Date();
+ constructor(private logsService: LogsService) {
+ this.startTime.setHours(0, 0);
+ this.endTime.setHours(23, 59);
+ }
ngOnInit() {
this.getInfo();
getInfo() {
this.logsService.getLogs().subscribe((data: any) => {
this.contentData = data;
+ this.filterLogs();
});
}
+
+ abstractfilters(): any {
+ const priority = this.priority;
+ const key = this.search.toLowerCase().replace(/,/g, '');
+
+ let yearMonthDay: string;
+ if (this.selectedDate) {
+ const m = this.selectedDate.getMonth() + 1;
+ const d = this.selectedDate.getDate();
+
+ const year = this.selectedDate.getFullYear().toString();
+ const month = m <= 9 ? `0${m}` : `${m}`;
+ const day = d <= 9 ? `0${d}` : `${d}`;
+ yearMonthDay = `${year}-${month}-${day}`;
+ } else {
+ yearMonthDay = '';
+ }
+
+ const sHour = this.startTime ? this.startTime.getHours() : 0;
+ const sMinutes = this.startTime ? this.startTime.getMinutes() : 0;
+ const sTime = sHour * 60 + sMinutes;
+
+ const eHour = this.endTime ? this.endTime.getHours() : 23;
+ const eMinutes = this.endTime ? this.endTime.getMinutes() : 59;
+ const eTime = eHour * 60 + eMinutes;
+
+ return { priority, key, yearMonthDay, sTime, eTime };
+ }
+
+ filterExecutor(logs: Array<any>, filters: any): Array<any> {
+ return logs.filter((line) => {
+ const hour = parseInt(line.stamp.slice(11, 13), 10);
+ const minutes = parseInt(line.stamp.slice(14, 16), 10);
+ let prio: string, y_m_d: string, timeSpan: number;
+
+ prio = filters.priority === 'All' ? line.priority : filters.priority;
+ y_m_d = filters.yearMonthDay ? filters.yearMonthDay : line.stamp;
+ timeSpan = hour * 60 + minutes;
+ return (
+ line.priority === prio &&
+ line.message.toLowerCase().indexOf(filters.key) !== -1 &&
+ line.stamp.indexOf(y_m_d) !== -1 &&
+ timeSpan >= filters.sTime &&
+ timeSpan <= filters.eTime
+ );
+ });
+ }
+
+ filterLogs() {
+ const filters = this.abstractfilters();
+ this.clog = this.filterExecutor(this.contentData.clog, filters);
+ this.audit_log = this.filterExecutor(this.contentData.audit_log, filters);
+ }
+
+ clearSearchKey() {
+ this.search = '';
+ this.filterLogs();
+ }
+ clearDate() {
+ this.selectedDate = null;
+ this.filterLogs();
+ }
}
<source>No entries found</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/cluster/logs/logs.component.html</context>
- <context context-type="linenumber">14</context>
+ <context context-type="linenumber">15</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/cluster/logs/logs.component.html</context>
- <context context-type="linenumber">30</context>
+ <context context-type="linenumber">31</context>
</context-group>
</trans-unit><trans-unit id="d556ab48a65722b400e497f61737f553ee0f89e2" datatype="html">
<source>Cluster Logs</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/cluster/logs/logs.component.html</context>
- <context context-type="linenumber">4</context>
+ <context context-type="linenumber">5</context>
</context-group>
</trans-unit><trans-unit id="5f966baffd188be0e8adc2d7067b86e55fc9b9de" datatype="html">
<source>Audit Logs</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/cluster/logs/logs.component.html</context>
- <context context-type="linenumber">20</context>
+ <context context-type="linenumber">21</context>
+ </context-group>
+ </trans-unit><trans-unit id="4193c9eb868aeec119b78a14795241e0aa5e8b60" datatype="html">
+ <source>Priority:</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/cluster/logs/logs.component.html</context>
+ <context context-type="linenumber">41</context>
+ </context-group>
+ </trans-unit><trans-unit id="1d78ca51eab260ce3fd917d39190d64df5229b6e" datatype="html">
+ <source>Keyword:</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/cluster/logs/logs.component.html</context>
+ <context context-type="linenumber">51</context>
+ </context-group>
+ </trans-unit><trans-unit id="05fa0bded36de6e73a1fa44838b627349dace044" datatype="html">
+ <source>Date:</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/cluster/logs/logs.component.html</context>
+ <context context-type="linenumber">70</context>
+ </context-group>
+ </trans-unit><trans-unit id="31dadb362bf7f7c2afa10e5f96474ccd3977f91f" datatype="html">
+ <source>Datepicker</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/cluster/logs/logs.component.html</context>
+ <context context-type="linenumber">75</context>
+ </context-group>
+ </trans-unit><trans-unit id="85a400388de1899b1917138cf7e5286376f72847" datatype="html">
+ <source>Time range:</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/cluster/logs/logs.component.html</context>
+ <context context-type="linenumber">91</context>
</context-group>
</trans-unit><trans-unit id="2447796ddbda942f4e2c46619cb84d69f066e568" datatype="html">
<source>Loading configuration...</source>