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-14714
Commit: 1f00c19d09ffe9889a6fe59df28c2905a09c1333
Parents: c28b797
Author: ababiichuk <[email protected]>
Authored: Tue Oct 3 16:02:56 2017 +0300
Committer: ababiichuk <[email protected]>
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) {

Reply via email to