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) {
