]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
mgr/dashboard: Add date range and log search functionality
authorguodan1 <guodan1@lenovo.com>
Thu, 31 Jan 2019 12:36:57 +0000 (20:36 +0800)
committerguodan1 <guodan1@lenovo.com>
Tue, 19 Mar 2019 10:28:45 +0000 (18:28 +0800)
Fixes: http://tracker.ceph.com/issues/37387t st
Signed-off-by: guodan1 <guodan1@lenovo.com>
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/logs/logs.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/logs/logs.component.scss
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/logs/logs.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/logs/logs.component.ts
src/pybind/mgr/dashboard/frontend/src/locale/messages.xlf

index a84f0544c49e7b277b1b5f80d53a38da6dbba122..fce9b6ab98826bf22251ce233d444adea4c04785 100644 (file)
@@ -5,9 +5,11 @@ import { RouterModule } from '@angular/router';
 
 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';
@@ -51,7 +53,9 @@ import { PrometheusListComponent } from './prometheus/prometheus-list/prometheus
     AlertModule.forRoot(),
     TooltipModule.forRoot(),
     TreeModule,
-    MgrModulesModule
+    MgrModulesModule,
+    TimepickerModule.forRoot(),
+    BsDatepickerModule.forRoot()
   ],
   declarations: [
     HostsComponent,
index d14dbf0be6bec35f23197fef2eb343f178fa39d6..a2c7d2389ebdd1b91c4e329750023edb92a0dabd 100644 (file)
@@ -1,10 +1,11 @@
 <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>
@@ -19,8 +20,8 @@
   <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>&nbsp;&mdash;&nbsp;</span>
+      <timepicker [showMeridian]="false"
+                  [showSpinners]="showSpinners"
+                  [minuteStep]="1"
+                  [(ngModel)]="endTime"
+                  (ngModelChange)="filterLogs()">
+      </timepicker>
+    </div>
+  </div>
+</ng-template>
index b084aada8ccc13a2f0e3e9676b2e4bb023d732b3..8c91d4ad8a98c247ca2b90b3341b9ec955fcfebc 100644 (file)
@@ -39,3 +39,105 @@ p {
     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;
+    }
+  }
+}
index 6a8007edbb08bba0a1a7a77a921e73642908ae32..8b4436cc7ef47981ba1267edb8b59fd88e497d62 100644 (file)
@@ -1,7 +1,10 @@
 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';
@@ -12,7 +15,14 @@ describe('LogsComponent', () => {
   let fixture: ComponentFixture<LogsComponent>;
 
   configureTestBed({
-    imports: [HttpClientTestingModule, TabsModule.forRoot(), SharedModule],
+    imports: [
+      HttpClientTestingModule,
+      TabsModule.forRoot(),
+      SharedModule,
+      BsDatepickerModule.forRoot(),
+      TimepickerModule.forRoot(),
+      FormsModule
+    ],
     declarations: [LogsComponent]
   });
 
@@ -25,4 +35,104 @@ describe('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');
+    });
+  });
 });
index d3a9a341ad7aedd987989b3060a94c53b19302ed..cd20503c3a35bfc62a2fe901553161bb5d0609a0 100644 (file)
@@ -9,9 +9,29 @@ import { LogsService } from '../../../shared/api/logs.service';
 })
 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();
@@ -27,6 +47,69 @@ export class LogsComponent implements OnInit, OnDestroy {
   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();
+  }
 }
index 11c6b814a7ed9fffa83540617a7427d020f8d7ba..2f184d852d19bc2dc78bbd3bb799f55e8cc0f223 100644 (file)
         <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>