You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by rl...@apache.org on 2017/10/03 20:16:05 UTC

[18/22] ambari git commit: AMBARI-22118 Log Search UI: implement time range selection from graph. (ababiichuk)

AMBARI-22118 Log Search UI: implement time range selection from graph. (ababiichuk)


Project: http://git-wip-us.apache.org/repos/asf/ambari/repo
Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/1f00c19d
Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/1f00c19d
Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/1f00c19d

Branch: refs/heads/branch-feature-AMBARI-20859
Commit: 1f00c19d09ffe9889a6fe59df28c2905a09c1333
Parents: c28b797
Author: ababiichuk <ab...@hortonworks.com>
Authored: Tue Oct 3 16:02:56 2017 +0300
Committer: ababiichuk <ab...@hortonworks.com>
Committed: Tue Oct 3 16:35:56 2017 +0300

----------------------------------------------------------------------
 .../src/app/classes/histogram-options.class.ts  | 36 ++++++++
 .../logs-container.component.html               |  4 +-
 .../logs-container/logs-container.component.ts  | 13 ++-
 .../time-histogram.component.less               | 22 +++--
 .../time-histogram/time-histogram.component.ts  | 94 ++++++++++++++++----
 .../src/app/services/filtering.service.ts       | 23 ++---
 6 files changed, 154 insertions(+), 38 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/1f00c19d/ambari-logsearch/ambari-logsearch-web/src/app/classes/histogram-options.class.ts
----------------------------------------------------------------------
diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/classes/histogram-options.class.ts b/ambari-logsearch/ambari-logsearch-web/src/app/classes/histogram-options.class.ts
new file mode 100644
index 0000000..dee5d98
--- /dev/null
+++ b/ambari-logsearch/ambari-logsearch-web/src/app/classes/histogram-options.class.ts
@@ -0,0 +1,36 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export interface HistogramMarginOptions {
+  top: number;
+  right: number;
+  bottom: number;
+  left: number;
+}
+
+export interface HistogramStyleOptions {
+  margin?: HistogramMarginOptions;
+  height?: number;
+  tickPadding?: number;
+  columnWidth?: number;
+  dragAreaColor?: string;
+}
+
+export interface HistogramOptions extends HistogramStyleOptions {
+  keysWithColors: {[key: string]: string};
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/1f00c19d/ambari-logsearch/ambari-logsearch-web/src/app/components/logs-container/logs-container.component.html
----------------------------------------------------------------------
diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/logs-container/logs-container.component.html b/ambari-logsearch/ambari-logsearch-web/src/app/components/logs-container/logs-container.component.html
index 9c6c336..776bb9a 100644
--- a/ambari-logsearch/ambari-logsearch-web/src/app/components/logs-container/logs-container.component.html
+++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/logs-container/logs-container.component.html
@@ -20,7 +20,9 @@
     {{'filter.capture.triggeringRefresh' | translate: autoRefreshMessageParams}}
   </div>
 </div>
-<time-histogram class="col-md-12" [data]="histogramData" [customOptions]="histogramOptions"></time-histogram>
+<time-histogram class="col-md-12" [data]="histogramData" [customOptions]="histogramOptions"
+                svgId="service-logs-histogram"
+                (selectArea)="setCustomTimeRange($event[0], $event[1])"></time-histogram>
 <dropdown-button *ngIf="!isServiceLogsFileView" class="pull-right" label="logs.columns"
                  [options]="availableColumns | async" [isRightAlign]="true" [isMultipleChoice]="true"
                  action="updateSelectedColumns" [additionalArgs]="logsTypeMapObject.fieldsModel"></dropdown-button>

http://git-wip-us.apache.org/repos/asf/ambari/blob/1f00c19d/ambari-logsearch/ambari-logsearch-web/src/app/components/logs-container/logs-container.component.ts
----------------------------------------------------------------------
diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/logs-container/logs-container.component.ts b/ambari-logsearch/ambari-logsearch-web/src/app/components/logs-container/logs-container.component.ts
index fd3a58b..7345288 100644
--- a/ambari-logsearch/ambari-logsearch-web/src/app/components/logs-container/logs-container.component.ts
+++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/logs-container/logs-container.component.ts
@@ -28,6 +28,7 @@ import {AuditLog} from '@app/models/audit-log.model';
 import {ServiceLog} from '@app/models/service-log.model';
 import {LogField} from '@app/models/log-field.model';
 import {ActiveServiceLogEntry} from '@app/classes/active-service-log-entry.class';
+import {HistogramOptions} from '@app/classes/histogram-options.class';
 
 @Component({
   selector: 'logs-container',
@@ -92,9 +93,9 @@ export class LogsContainerComponent implements OnInit {
 
   displayedColumns: any[] = [];
 
-  histogramData: any;
+  histogramData: {[key: string]: number};
 
-  readonly histogramOptions = {
+  readonly histogramOptions: HistogramOptions = {
     keysWithColors: this.logsContainer.colors
   };
 
@@ -116,9 +117,13 @@ export class LogsContainerComponent implements OnInit {
 
   get isServiceLogsFileView(): boolean {
     return this.logsContainer.isServiceLogsFileView;
-  };
+  }
 
   get activeLog(): ActiveServiceLogEntry | null {
     return this.logsContainer.activeLog;
-  };
+  }
+
+  setCustomTimeRange(startTime: number, endTime: number): void {
+    this.filtering.setCustomTimeRange(startTime, endTime);
+  }
 }

http://git-wip-us.apache.org/repos/asf/ambari/blob/1f00c19d/ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.less
----------------------------------------------------------------------
diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.less b/ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.less
index d891862..1d29c55 100644
--- a/ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.less
+++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.less
@@ -16,14 +16,24 @@
  * limitations under the License.
  */
 
-/deep/ .axis {
-  .domain {
-    display: none;
-  }
+:host {
+  cursor: crosshair;
 
-  .tick {
-    line {
+  /deep/ .axis {
+    .domain {
       display: none;
     }
+
+    .tick {
+      cursor: default;
+
+      line {
+        display: none;
+      }
+    }
+  }
+
+  /deep/ .value {
+    cursor: pointer;
   }
 }

http://git-wip-us.apache.org/repos/asf/ambari/blob/1f00c19d/ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.ts
----------------------------------------------------------------------
diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.ts b/ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.ts
index 7856ecc..c3ec388 100644
--- a/ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.ts
+++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.ts
@@ -16,10 +16,12 @@
  * limitations under the License.
  */
 
-import {Component, OnInit, AfterViewInit, OnChanges, Input, ViewChild, ElementRef} from '@angular/core';
+import {Component, OnInit, AfterViewInit, OnChanges, Input, Output, ViewChild, ElementRef, EventEmitter} from '@angular/core';
+import {ContainerElement, Selection} from 'd3';
 import * as d3 from 'd3';
 import * as moment from 'moment-timezone';
 import {AppSettingsService} from '@app/services/storage/app-settings.service';
+import {HistogramStyleOptions, HistogramOptions} from '@app/classes/histogram-options.class';
 
 @Component({
   selector: 'time-histogram',
@@ -29,14 +31,14 @@ import {AppSettingsService} from '@app/services/storage/app-settings.service';
 export class TimeHistogramComponent implements OnInit, AfterViewInit, OnChanges {
 
   constructor(private appSettings: AppSettingsService) {
-    appSettings.getParameter('timeZone').subscribe(value => {
+    appSettings.getParameter('timeZone').subscribe((value: string): void => {
       this.timeZone = value;
       this.createHistogram();
     });
   }
 
   ngOnInit() {
-    Object.assign(this.options, this.defaultOptions, this.customOptions);
+    this.options = Object.assign({}, this.defaultOptions, this.customOptions);
   }
 
   ngAfterViewInit() {
@@ -52,12 +54,18 @@ export class TimeHistogramComponent implements OnInit, AfterViewInit, OnChanges
   element: ElementRef;
 
   @Input()
-  customOptions: any;
+  svgId: string;
 
   @Input()
-  data: any;
+  customOptions: HistogramOptions;
 
-  private readonly defaultOptions = {
+  @Input()
+  data: {[key: string]: number};
+
+  @Output()
+  selectArea: EventEmitter<number[]> = new EventEmitter();
+
+  private readonly defaultOptions: HistogramStyleOptions = {
     margin: {
       top: 20,
       right: 20,
@@ -66,10 +74,11 @@ export class TimeHistogramComponent implements OnInit, AfterViewInit, OnChanges
     },
     height: 200,
     tickPadding: 10,
-    columnWidth: 20
+    columnWidth: 20,
+    dragAreaColor: '#FFF'
   };
 
-  private options: any = {};
+  private options: HistogramOptions;
 
   private timeZone: string;
 
@@ -77,7 +86,7 @@ export class TimeHistogramComponent implements OnInit, AfterViewInit, OnChanges
 
   private svg;
 
-  private width;
+  private width: number;
 
   private xScale;
 
@@ -91,6 +100,16 @@ export class TimeHistogramComponent implements OnInit, AfterViewInit, OnChanges
 
   private htmlElement: HTMLElement;
 
+  private dragArea: Selection<SVGGraphicsElement, undefined, SVGGraphicsElement, undefined>;
+
+  private dragStartX: number;
+
+  private minDragX: number;
+
+  private maxDragX: number;
+
+  private readonly timeFormat: string = 'MM/DD HH:mm';
+
   histogram: any;
 
   private createHistogram(): void {
@@ -105,7 +124,7 @@ export class TimeHistogramComponent implements OnInit, AfterViewInit, OnChanges
     const margin = this.options.margin,
       keysWithColors = this.options.keysWithColors,
       keys = Object.keys(keysWithColors),
-      colors = keys.reduce((array, key) => [...array, keysWithColors[key]], []);
+      colors = keys.reduce((array: string[], key: string): string[] => [...array, keysWithColors[key]], []);
     this.width = this.htmlElement.clientWidth - margin.left - margin.right;
     this.xScale = d3.scaleTime().range([0, this.width]);
     this.yScale = d3.scaleLinear().range([this.options.height, 0]);
@@ -115,20 +134,20 @@ export class TimeHistogramComponent implements OnInit, AfterViewInit, OnChanges
   private buildSVG(): void {
     const margin = this.options.margin;
     this.host.html('');
-    this.svg = this.host.append('svg').attr('width', this.width + margin.left + margin.right)
+    this.svg = this.host.append('svg').attr('id', this.svgId).attr('width', this.htmlElement.clientWidth)
       .attr('height', this.options.height + margin.top + margin.bottom).append('g')
       .attr('transform', `translate(${margin.left},${margin.top})`);
   }
 
   private drawXAxis(): void {
     this.xAxis = d3.axisBottom(this.xScale)
-      .tickFormat(tick => moment(tick).tz(this.timeZone).format('MM/DD HH:mm'))
+      .tickFormat(tick => moment(tick).tz(this.timeZone).format(this.timeFormat))
       .tickPadding(this.options.tickPadding);
     this.svg.append('g').attr('class', 'axis').attr('transform', `translate(0,${this.options.height})`).call(this.xAxis);
   }
 
   private drawYAxis(): void {
-    this.yAxis = d3.axisLeft(this.yScale).tickFormat((tick: number) => {
+    this.yAxis = d3.axisLeft(this.yScale).tickFormat((tick: number): string | undefined => {
       if (Number.isInteger(tick)) {
         return tick.toFixed(0);
       } else {
@@ -142,20 +161,61 @@ export class TimeHistogramComponent implements OnInit, AfterViewInit, OnChanges
     const keys = Object.keys(this.options.keysWithColors),
       data = this.data,
       timeStamps = Object.keys(data),
-      formattedData = timeStamps.map(timeStamp => Object.assign({
-        timeStamp: timeStamp
+      formattedData = timeStamps.map((timeStamp: string): {[key: string]: number} => Object.assign({
+        timeStamp: Number(timeStamp)
       }, data[timeStamp])),
       layers = (d3.stack().keys(keys)(formattedData)),
       columnWidth = this.options.columnWidth;
     this.xScale.domain(d3.extent(formattedData, item => item.timeStamp));
-    this.yScale.domain([0, d3.max(formattedData, item => keys.reduce((sum, key) => sum + item[key], 0))]);
+    this.yScale.domain([0, d3.max(formattedData, item => keys.reduce((sum: number, key: string): number => sum + item[key], 0))]);
     this.drawXAxis();
     this.drawYAxis();
-    const layer = this.svg.selectAll().data(d3.transpose<any>(layers)).enter().append('g');
+    const layer = this.svg.selectAll().data(d3.transpose<any>(layers)).enter().append('g').attr('class', 'value');
     layer.selectAll().data(item => item).enter().append('rect')
       .attr('x', item => this.xScale(item.data.timeStamp) - columnWidth / 2).attr('y', item => this.yScale(item[1]))
       .attr('height', item => this.yScale(item[0]) - this.yScale(item[1])).attr('width', columnWidth.toString())
       .style('fill', (item, index) => this.colorScale(index));
+    this.setDragBehavior();
+  }
+
+  private setDragBehavior(): void {
+    this.minDragX = this.options.margin.left;
+    this.maxDragX = this.htmlElement.clientWidth;
+    d3.selectAll(`svg#${this.svgId}`).call(d3.drag()
+      .on('start', (datum: undefined, index: number, containers: ContainerElement[]): void => {
+        if (this.dragArea) {
+          this.dragArea.remove();
+        }
+        this.dragStartX = Math.max(0, this.getDragX(containers[0]) - this.options.margin.left);
+        this.dragArea = this.svg.insert('rect', ':first-child').attr('x', this.dragStartX).attr('y', 0).attr('width', 0)
+          .attr('height', this.options.height).style('fill', this.options.dragAreaColor);
+      })
+      .on('drag', (datum: undefined, index: number, containers: ContainerElement[]): void => {
+        const currentX = Math.max(this.getDragX(containers[0]), this.minDragX) - this.options.margin.left,
+          startX = Math.min(currentX, this.dragStartX),
+          currentWidth = Math.abs(currentX - this.dragStartX);
+        this.dragArea.attr('x', startX).attr('width', currentWidth);
+      })
+      .on('end', (): void => {
+        const dragAreaDetails = this.dragArea.node().getBBox(),
+          startX = Math.max(0, dragAreaDetails.x),
+          endX = Math.min(this.width, dragAreaDetails.x + dragAreaDetails.width),
+          xScaleInterval = this.xScale.domain().map((point: Date): number => point.valueOf()),
+          xScaleLength = xScaleInterval[1] - xScaleInterval[0],
+          ratio = xScaleLength / this.width,
+          startTimeStamp = Math.round(xScaleInterval[0] + ratio * startX),
+          endTimeStamp = Math.round(xScaleInterval[0] + ratio * endX);
+        this.selectArea.emit([startTimeStamp, endTimeStamp]);
+        this.dragArea.remove();
+      })
+    );
+    d3.selectAll(`svg#${this.svgId} .value, svg#${this.svgId} .axis`).call(d3.drag().on('start', (): void => {
+      d3.event.sourceEvent.stopPropagation();
+    }));
+  }
+
+  private getDragX(element: ContainerElement): number {
+    return d3.mouse(element)[0];
   }
 
 }

http://git-wip-us.apache.org/repos/asf/ambari/blob/1f00c19d/ambari-logsearch/ambari-logsearch-web/src/app/services/filtering.service.ts
----------------------------------------------------------------------
diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/services/filtering.service.ts b/ambari-logsearch/ambari-logsearch-web/src/app/services/filtering.service.ts
index 6697c54..0fff75d 100644
--- a/ambari-logsearch/ambari-logsearch-web/src/app/services/filtering.service.ts
+++ b/ambari-logsearch/ambari-logsearch-web/src/app/services/filtering.service.ts
@@ -22,7 +22,6 @@ import {Subject} from 'rxjs/Subject';
 import {Observable} from 'rxjs/Observable';
 import 'rxjs/add/observable/timer';
 import 'rxjs/add/operator/takeUntil';
-import {Moment} from 'moment';
 import * as moment from 'moment-timezone';
 import {ListItem} from '@app/classes/list-item.class';
 import {AppSettingsService} from '@app/services/storage/app-settings.service';
@@ -400,29 +399,25 @@ export class FilteringService {
 
   autoRefreshRemainingSeconds: number = 0;
 
-  private startCaptureMoment: Moment;
+  private startCaptureTime: number;
 
-  private stopCaptureMoment: Moment;
+  private stopCaptureTime: number;
 
   startCaptureTimer(): void {
-    this.startCaptureMoment = moment();
+    this.startCaptureTime = new Date().valueOf();
     Observable.timer(0, 1000).takeUntil(this.stopTimer).subscribe(seconds => this.captureSeconds = seconds);
   }
 
   stopCaptureTimer(): void {
     const autoRefreshIntervalSeconds = this.autoRefreshInterval / 1000;
-    this.stopCaptureMoment = moment();
+    this.stopCaptureTime = new Date().valueOf();
     this.captureSeconds = 0;
     this.stopTimer.next();
     Observable.timer(0, 1000).takeUntil(this.stopAutoRefreshCountdown).subscribe(seconds => {
       this.autoRefreshRemainingSeconds = autoRefreshIntervalSeconds - seconds;
       if (!this.autoRefreshRemainingSeconds) {
         this.stopAutoRefreshCountdown.next();
-        this.filtersForm.controls.timeRange.setValue({
-          type: 'CUSTOM',
-          start: this.startCaptureMoment,
-          end: this.stopCaptureMoment
-        });
+        this.setCustomTimeRange(this.startCaptureTime, this.stopCaptureTime);
       }
     });
   }
@@ -457,6 +452,14 @@ export class FilteringService {
     });
   }
 
+  setCustomTimeRange(startTime: number, endTime: number): void {
+    this.filtersForm.controls.timeRange.setValue({
+      type: 'CUSTOM',
+      start: moment(startTime),
+      end: moment(endTime)
+    });
+  }
+
   private getStartTime = (value: any, current: string): string => {
     let time;
     if (value) {