You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by ab...@apache.org on 2017/12/08 16:58:22 UTC
ambari git commit: AMBARI-22615 Log Search UI: improve histogram
display. (Istvan Tobias via ababiichuk)
Repository: ambari
Updated Branches:
refs/heads/trunk ee506295d -> b1fd131f7
AMBARI-22615 Log Search UI: improve histogram display. (Istvan Tobias via ababiichuk)
Project: http://git-wip-us.apache.org/repos/asf/ambari/repo
Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/b1fd131f
Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/b1fd131f
Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/b1fd131f
Branch: refs/heads/trunk
Commit: b1fd131f78feda8f9fef6f33dbc8c67e3fdf539f
Parents: ee50629
Author: Istvan Tobias <to...@gmail.com>
Authored: Fri Dec 8 13:32:20 2017 +0200
Committer: ababiichuk <ab...@hortonworks.com>
Committed: Fri Dec 8 19:00:36 2017 +0200
----------------------------------------------------------------------
.../src/app/classes/histogram-options.ts | 3 +-
.../src/app/components/mixins.less | 18 +-
.../time-histogram.component.html | 29 ++
.../time-histogram.component.less | 162 +++++++-
.../time-histogram.component.spec.ts | 138 ++++++-
.../time-histogram/time-histogram.component.ts | 377 ++++++++++++++++---
.../src/app/services/logs-container.service.ts | 4 +-
.../src/assets/i18n/en.json | 13 +-
8 files changed, 669 insertions(+), 75 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/ambari/blob/b1fd131f/ambari-logsearch/ambari-logsearch-web/src/app/classes/histogram-options.ts
----------------------------------------------------------------------
diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/classes/histogram-options.ts b/ambari-logsearch/ambari-logsearch-web/src/app/classes/histogram-options.ts
index dee5d98..15fefde 100644
--- a/ambari-logsearch/ambari-logsearch-web/src/app/classes/histogram-options.ts
+++ b/ambari-logsearch/ambari-logsearch-web/src/app/classes/histogram-options.ts
@@ -27,8 +27,7 @@ export interface HistogramStyleOptions {
margin?: HistogramMarginOptions;
height?: number;
tickPadding?: number;
- columnWidth?: number;
- dragAreaColor?: string;
+ columnWidth?: {[key:string]: number};
}
export interface HistogramOptions extends HistogramStyleOptions {
http://git-wip-us.apache.org/repos/asf/ambari/blob/b1fd131f/ambari-logsearch/ambari-logsearch-web/src/app/components/mixins.less
----------------------------------------------------------------------
diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/mixins.less b/ambari-logsearch/ambari-logsearch-web/src/app/components/mixins.less
index a6e5616..890887a 100644
--- a/ambari-logsearch/ambari-logsearch-web/src/app/components/mixins.less
+++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/mixins.less
@@ -203,25 +203,11 @@
display: inline-block;
width: 0;
height: 0;
- margin-left: @caret-width * .85;
vertical-align: @caret-width * .85;
content: "";
.caret-direction(@caret-width, @direction, @color);
}
// This is the main caret mixin to create the common and the direction related css
-.caret(@caret-width; @direction: down; @color: @base-font-color; @position: before) {
-
- &::before when (@position = before) {
- .caret-style(@caret-width, @direction, @color);
- }
- &::after when (@position = after) {
- .caret-style(@caret-width, @direction, @color);
- }
-
- &:empty::before when (@position = before) {
- margin-left: 0;
- }
- &:empty::after when (@position = after) {
- margin-left: 0;
- }
+.caret-mixin(@caret-width; @direction: down; @color: @base-font-color; @position: before) {
+ .caret-style(@caret-width, @direction, @color);
}
http://git-wip-us.apache.org/repos/asf/ambari/blob/b1fd131f/ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.html
----------------------------------------------------------------------
diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.html b/ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.html
index 299e46e..1193b2e 100644
--- a/ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.html
+++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.html
@@ -15,4 +15,33 @@
limitations under the License.
-->
+<header>
+ <div class="container-fluid">
+ <div class="row">
+ <div *ngIf="chartTimeGap" class="time-gap col-lg-2 col-md-offset-5">
+ {{chartTimeGap.value}} {{chartTimeGap.label | translate}} {{'histogram.gap' | translate}}
+ </div>
+ <div class="legends col-md-5" [class.md-offset-7]="!chartTimeGap">
+ <div *ngFor="let legend of legends" class="legend {{legend.level | lowercase}}">
+ {{ legend.label | translate }}
+ </div>
+ </div>
+ </div>
+ </div>
+</header>
<div #container></div>
+<footer *ngIf="firstDateTick || lastDateTick">
+ <div *ngIf="firstDateTick" class="first-date-tick-label">{{firstDateTick | amTz:timeZone | amDateFormat:historyStartEndTimeFormat}}</div>
+ <div *ngIf="lastDateTick" class="last-date-tick-label">{{lastDateTick | amTz:timeZone | amDateFormat:historyStartEndTimeFormat}}</div>
+</footer>
+<div [ngClass]="{hide: !tooltipInfo, 'tooltip-left': tooltipOnTheLeft, 'tooltip-chart': true}" #tooltipEl
+ [style.top]="tooltipInfo ? (tooltipPosition.top + 'px') : ''" [style.left]="tooltipInfo ? (tooltipPosition.left + 'px') : ''">
+ <ng-container *ngIf="tooltipInfo">
+ <div class="tooltip-chart-date">{{tooltipInfo.timeStamp | amTz:timeZone | amDateFormat:tickTimeFormat}}</div>
+ <div *ngFor="let data of tooltipInfo.data" class="level {{data.level | lowercase}}">
+ <span class="level-label">{{data.levelLabel | translate }}</span>
+ <span class="level-value">{{data.value}}</span>
+ </div>
+ </ng-container>
+</div>
+
http://git-wip-us.apache.org/repos/asf/ambari/blob/b1fd131f/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 e8d3240..1d3766d 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,25 +16,179 @@
* limitations under the License.
*/
+@import '../mixins';
+
:host {
- display: block;
- cursor: crosshair;
+ position: relative;
+ .level-mixin(@level, @size: .8em) {
+ @name: "@{level}-color";
+ border-radius: 100%;
+ content: "";
+ display: inline-block;
+ height: .8em;
+ width: .8em;
+ background-color: @@name;
+ }
+
background: #ECECEC; // TODO add style according to actual design
+ display: block;
+
/deep/ .axis {
.domain {
display: none;
}
-
.tick {
cursor: default;
-
line {
display: none;
}
}
}
+ /deep/ svg {
+ cursor: crosshair;
+ }
+
/deep/ .value {
cursor: pointer;
+ rect {
+ transition: opacity 250ms;
+ opacity: .8;
+ &:hover {
+ opacity: 1;
+ }
+ }
+ }
+
+ /deep/ .tooltip-chart {
+ background: #fff;
+ border-radius: 4px;
+ border: @input-border;
+ display: block;
+ font-size: .8em;
+ margin: 0 1.5em;
+ min-height: 2em;
+ min-width: 5em;
+ padding: .5em;
+ position: absolute;
+ &:empty {
+ display: none;
+ }
+ &::before {
+ .caret-mixin(6px, left, #fff);
+ left: -6px;
+ position: absolute;
+ top: calc(50% - 2px);
+ }
+ &.tooltip-left::before {
+ display: none;
+ }
+ &.tooltip-left::after {
+ .caret-mixin(6px, right, #fff);
+ right: -6px;
+ position: absolute;
+ top: calc(50% - 2px);
+ }
+ .tooltip-chart-date {
+ padding: 0 0 .1em 0;
+ text-align: center;
+ }
+ .level {
+ display: flex;
+ &::before {
+ margin: auto .2em auto 0;
+ }
+ .level-label {
+ flex-grow: 3;
+ padding: 0 2em 0 0;
+ }
+ .level-value {
+ text-align: right;
+ }
+ }
+
+ .fatal::before {
+ .level-mixin('fatal');
+ }
+ .error::before {
+ .level-mixin('error');
+ }
+ .warn::before {
+ .level-mixin('warning');
+ }
+ .info::before {
+ .level-mixin('info');
+ }
+ .trace::before {
+ .level-mixin('trace');
+ }
+ .debug::before {
+ .level-mixin('debug');
+ }
+ .unknown::before {
+ .level-mixin('unknown');
+ }
+ }
+ header {
+ padding: .5rem;
}
+ .legends {
+ text-align: right;
+ .legend {
+ display: inline-block;
+ font-size: 1rem;
+ text-transform: uppercase;
+ padding-right: 1em;
+ }
+ .fatal::before {
+ .level-mixin('fatal');
+ }
+ .error::before {
+ .level-mixin('error');
+ }
+ .warn::before {
+ .level-mixin('warning');
+ }
+ .info::before {
+ .level-mixin('info');
+ }
+ .trace::before {
+ .level-mixin('trace');
+ }
+ .debug::before {
+ .level-mixin('debug');
+ }
+ .unknown::before {
+ .level-mixin('unknown');
+ }
+ }
+
+ .time-gap {
+ color: #666;
+ font-size: 1.2rem;
+ text-align: center;
+ }
+
+ footer {
+ display: flex;
+ div {
+ color: #666;
+ flex-grow: 1;
+ font-size: 1.2rem;
+ padding: 0 1em .5em 1em;
+ }
+ .last-date-tick-label {
+ text-align: right;
+ }
+ }
+
+ /deep/ rect.drag-area {
+ fill: #fff;
+ }
+
+ /deep/ rect.unselected-drag-area {
+ fill: darken(@main-background-color, 10%);
+ opacity: .6;
+ }
+
}
http://git-wip-us.apache.org/repos/asf/ambari/blob/b1fd131f/ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.spec.ts
----------------------------------------------------------------------
diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.spec.ts b/ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.spec.ts
index 9e056be..ee14780 100644
--- a/ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.spec.ts
+++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.spec.ts
@@ -19,35 +19,157 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {StoreModule} from '@ngrx/store';
import {AppSettingsService, appSettings} from '@app/services/storage/app-settings.service';
+import {TranslationModules} from '@app/test-config.spec';
+import {MomentModule} from 'angular2-moment';
+import {MomentTimezoneModule} from 'angular-moment-timezone';
+import {TimeZoneAbbrPipe} from '@app/pipes/timezone-abbr.pipe';
+import {ServiceLogsHistogramDataService} from '@app/services/storage/service-logs-histogram-data.service';
import {TimeHistogramComponent} from './time-histogram.component';
+import {LogsContainerService} from '@app/services/logs-container.service';
+import {HttpClientService} from "@app/services/http-client.service";
+import {AppStateService} from "@app/services/storage/app-state.service";
+import {AuditLogsService} from "@app/services/storage/audit-logs.service";
+import {AuditLogsFieldsService} from "@app/services/storage/audit-logs-fields.service";
+import {ServiceLogsService} from "@app/services/storage/service-logs.service";
+import {ServiceLogsFieldsService} from "@app/services/storage/service-logs-fields.service";
+import {ServiceLogsTruncatedService} from "@app/services/storage/service-logs-truncated.service";
+import {TabsService} from "@app/services/storage/tabs.service";
+import {ClustersService} from "@app/services/storage/clusters.service";
+import {ComponentsService} from "@app/services/storage/components.service";
+import {HostsService} from "@app/services/storage/hosts.service";
describe('TimeHistogramComponent', () => {
let component: TimeHistogramComponent;
let fixture: ComponentFixture<TimeHistogramComponent>;
+ let histogramData: any;
+ let customOptions: any;
beforeEach(async(() => {
+ const httpClient = {
+ get: () => {
+ return {
+ subscribe: () => {}
+ }
+ }
+ };
+ histogramData = {
+ "1512476481940": {
+ "FATAL": 0,
+ "ERROR": 1000,
+ "WARN": 700,
+ "INFO": 0,
+ "DEBUG": 0,
+ "TRACE": 0,
+ "UNKNOWN": 0
+ }, "1512472881940": {"FATAL": 0, "ERROR": 2000, "WARN": 900, "INFO": 0, "DEBUG": 0, "TRACE": 0, "UNKNOWN": 0}
+ };
+ customOptions = {
+ keysWithColors: {
+ FATAL: '#830A0A',
+ ERROR: '#E81D1D',
+ WARN: '#FF8916',
+ INFO: '#2577B5',
+ DEBUG: '#65E8FF',
+ TRACE: '#888',
+ UNKNOWN: '#BDBDBD'
+ }
+ };
TestBed.configureTestingModule({
- declarations: [TimeHistogramComponent],
+ declarations: [TimeHistogramComponent, TimeZoneAbbrPipe],
imports: [
StoreModule.provideStore({
appSettings
- })
+ }),
+ ...TranslationModules,
+ MomentModule,
+ MomentTimezoneModule
],
providers: [
- AppSettingsService
+ AppSettingsService,
+ ServiceLogsHistogramDataService,
+ LogsContainerService,
+ {
+ provide: HttpClientService,
+ useValue: httpClient
+ },
+ AppStateService,
+ AuditLogsService,
+ AuditLogsFieldsService,
+ ServiceLogsService,
+ ServiceLogsFieldsService,
+ ServiceLogsHistogramDataService,
+ ServiceLogsTruncatedService,
+ TabsService,
+ ClustersService,
+ ComponentsService,
+ HostsService
]
})
- .compileComponents();
+ .compileComponents();
}));
beforeEach(() => {
- fixture = TestBed.createComponent(TimeHistogramComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- });
+ fixture = TestBed.createComponent(TimeHistogramComponent);
+ component = fixture.componentInstance;
+ component.customOptions = customOptions;
+ component.svgId = "HistogramSvg";
+ component.data = histogramData;
+ fixture.detectChanges();
+ });
it('should create component', () => {
expect(component).toBeTruthy();
});
+
+ const getTimeGapTestCases = [{
+ startDate: new Date(2017, 0, 1),
+ endDate: new Date(2017, 0, 8),
+ expected: {
+ unit: 'week',
+ value: 1,
+ label: 'histogram.gap.week'
+ }
+ }, {
+ startDate: new Date(2017, 0, 1),
+ endDate: new Date(2017, 0, 2),
+ expected: {
+ unit: 'day',
+ value: 1,
+ label: 'histogram.gap.day'
+ }
+ }, {
+ startDate: new Date(2017, 0, 1, 1),
+ endDate: new Date(2017, 0, 1, 2),
+ expected: {
+ unit: 'hour',
+ value: 1,
+ label: 'histogram.gap.hour'
+ }
+ }, {
+ startDate: new Date(2017, 0, 1, 1, 1),
+ endDate: new Date(2017, 0, 1, 1, 2),
+ expected: {
+ unit: 'minute',
+ value: 1,
+ label: 'histogram.gap.minute'
+ }
+ }, {
+ startDate: new Date(2017, 0, 1, 1, 1, 1),
+ endDate: new Date(2017, 0, 1, 1, 1, 11),
+ expected: {
+ unit: 'second',
+ value: 10,
+ label: 'histogram.gap.seconds'
+ }
+ }];
+
+ getTimeGapTestCases.forEach((test) => {
+ it(`should the getTimeGap return with the proper time gap obj for ${test.expected.value} ${test.expected.unit} difference`, () => {
+ const getTimeGap: (startDate: Date, endDate: Date) => {value: number, unit: string} = component['getTimeGap'];
+ const gap = getTimeGap(test.startDate, test.endDate);
+ expect(gap).toEqual(test.expected);
+ });
+ });
+
});
http://git-wip-us.apache.org/repos/asf/ambari/blob/b1fd131f/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 e255166..fb3092f 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
@@ -30,19 +30,19 @@ import {HistogramStyleOptions, HistogramOptions} from '@app/classes/histogram-op
})
export class TimeHistogramComponent implements OnInit, AfterViewInit, OnChanges {
- constructor(private appSettings: AppSettingsService) {
- appSettings.getParameter('timeZone').subscribe((value: string): void => {
+ constructor(private appSettings: AppSettingsService) {}
+
+ ngOnInit() {
+ this.appSettings.getParameter('timeZone').subscribe((value: string): void => {
this.timeZone = value;
this.createHistogram();
});
- }
-
- ngOnInit() {
this.options = Object.assign({}, this.defaultOptions, this.customOptions);
}
ngAfterViewInit() {
this.htmlElement = this.element.nativeElement;
+ this.tooltipElement = this.tooltipEl.nativeElement;
this.host = d3.select(this.htmlElement);
}
@@ -53,6 +53,9 @@ export class TimeHistogramComponent implements OnInit, AfterViewInit, OnChanges
@ViewChild('container')
element: ElementRef;
+ @ViewChild('tooltipEl')
+ tooltipEl: ElementRef;
+
@Input()
svgId: string;
@@ -67,15 +70,20 @@ export class TimeHistogramComponent implements OnInit, AfterViewInit, OnChanges
private readonly defaultOptions: HistogramStyleOptions = {
margin: {
- top: 20,
- right: 20,
- bottom: 40,
+ top: 5,
+ right: 50,
+ bottom: 30,
left: 50
},
- height: 200,
+ height: 150,
tickPadding: 10,
- columnWidth: 20,
- dragAreaColor: '#FFF'
+ columnWidth: {
+ second: 40,
+ minute: 30,
+ hour: 25,
+ day: 20,
+ base: 20
+ }
};
private options: HistogramOptions;
@@ -99,6 +107,7 @@ export class TimeHistogramComponent implements OnInit, AfterViewInit, OnChanges
private yAxis;
private htmlElement: HTMLElement;
+ private tooltipElement: HTMLElement;
private dragArea: Selection<SVGGraphicsElement, undefined, SVGGraphicsElement, undefined>;
@@ -108,10 +117,68 @@ export class TimeHistogramComponent implements OnInit, AfterViewInit, OnChanges
private maxDragX: number;
- private readonly timeFormat: string = 'MM/DD HH:mm';
+ private readonly tickTimeFormat: string = 'MM/DD HH:mm';
+ private readonly historyStartEndTimeFormat = 'dddd, MMMM DD, YYYY';
histogram: any;
+ /**
+ * This property is to hold the data of the bar where the mouse is over.
+ */
+ private tooltipInfo: {data: object, timeStamp: number};
+ /**
+ * This is the computed position of the tooltip relative to the @htmlElement which is the container of the histogram.
+ * It is set when the mousemoving over the bars in the @handleRectMouseMove method.
+ */
+ private tooltipPosition: {top: number, left: number};
+ /**
+ * This property indicates if the tooltip should be positioned on the left side of the cursor or not.
+ * It should be true when the tooltip is out from the window.
+ * @type {boolean}
+ */
+ private tooltipOnTheLeft: boolean = false;
+ /**
+ * This property holds the data structure describing the gaps between the xAxis ticks.
+ * The unit property can be: second, minute, hour, day
+ * The value is the number of the given unit.
+ */
+ private chartTimeGap: {value: number, unit: string, label: string} | null;
+ /**
+ * This is the rectangle element to represent the unselected time range on the left side of the selected time range
+ */
+ private leftDragArea: Selection<SVGGraphicsElement, undefined, SVGGraphicsElement, undefined>;
+ /**
+ * This is the rectangle element to represent the unselected time range on the right side of the selected time range
+ */
+ private rightDragArea: Selection<SVGGraphicsElement, undefined, SVGGraphicsElement, undefined>;
+ /**
+ * This is a Date object holding the value of the first tick of the xAxis. It is a helper getter for the template.
+ */
+ private get firstDateTick(): Date | undefined {
+ const ticks = this.xScale && this.xScale.ticks();
+ return (ticks && ticks.length && ticks[0]) || undefined;
+ }
+ /**
+ * This is a Date object holding the value of the last tick of the xAxis. It is a helper getter for the template.
+ */
+ private get lastDateTick(): Date | undefined {
+ const ticks = this.xScale && this.xScale.ticks();
+ return (ticks && ticks.length && ticks[ticks.length-1]) || undefined;
+ }
+
+ /**
+ * This will return the information about the used levels and the connected colors and labels.
+ * The goal is to provide an easy property to the template to display the legend of the levels.
+ * @returns {Array<{level: string; label: string; color: string}>}
+ */
+ private get legends(): Array<{level: string, label: string, color: string}> {
+ return Object.keys(this.options.keysWithColors).map(level => Object.assign({},{
+ level,
+ label: `levels.${level.toLowerCase()}`,
+ color: this.options.keysWithColors[level]
+ }));
+ }
+
private createHistogram(): void {
if (this.host) {
this.setup();
@@ -139,13 +206,21 @@ export class TimeHistogramComponent implements OnInit, AfterViewInit, OnChanges
.attr('transform', `translate(${margin.left},${margin.top})`);
}
+ /**
+ * It draws the svg representation of the x axis. The goal is to set the ticks here, add the axis to the svg element
+ * and set the position of the axis.
+ */
private drawXAxis(): void {
this.xAxis = d3.axisBottom(this.xScale)
- .tickFormat(tick => moment(tick).tz(this.timeZone).format(this.timeFormat))
+ .tickFormat(tick => moment(tick).tz(this.timeZone).format(this.tickTimeFormat))
.tickPadding(this.options.tickPadding);
- this.svg.append('g').attr('class', 'axis').attr('transform', `translate(0,${this.options.height})`).call(this.xAxis);
+ this.svg.append('g').attr('class', 'axis axis-x').attr('transform', `translate(0,${this.options.height})`).call(this.xAxis);
}
+ /**
+ * It draws the svg representation of the y axis. The goal is to set the ticks here, add the axis to the svg element
+ * and set the position of the axis.
+ */
private drawYAxis(): void {
this.yAxis = d3.axisLeft(this.yScale).tickFormat((tick: number): string | undefined => {
if (Number.isInteger(tick)) {
@@ -154,30 +229,248 @@ export class TimeHistogramComponent implements OnInit, AfterViewInit, OnChanges
return;
}
}).tickPadding(this.options.tickPadding);
- this.svg.append('g').attr('class', 'axis').call(this.yAxis).append('text');
+ this.svg.append('g').attr('class', 'axis axis-y').call(this.yAxis).append('text');
+ };
+
+ /**
+ * The goal is to handle the mouse over event on the rect svg elements so that we can populate the tooltip info object
+ * and set the initial position of the tooltip. So we call the corresponding methods.
+ * @param d The data for the currently "selected" bar
+ * @param {number} index The index of the current element in the selection
+ * @param elements The selection of the elements
+ */
+ private handleRectMouseOver = (d: any, index: number, elements: any):void => {
+ this.setTooltipDataFromChartData(d);
+ this.setTooltipPosition();
+ };
+
+ /**
+ * The goal is to handle the movement of the mouse over the rect svg elements, so that we can set the position of
+ * the tooltip by calling the @setTooltipPosition method.
+ */
+ private handleRectMouseMove = ():void => {
+ this.setTooltipPosition();
+ };
+
+ /**
+ * The goal is to reset the tooltipInfo object so that the tooltip will be hidden.
+ */
+ private handleRectMouseOut = ():void => {
+ this.tooltipInfo = null;
+ };
+
+ /**
+ * The goal is set the tooltip
+ * @param d
+ */
+ private setTooltipDataFromChartData(d: {data: any, [key: string]: any}): void {
+ let {timeStamp, ...data} = d.data;
+ let levelColors = this.options.keysWithColors;
+ this.tooltipInfo = {
+ data: Object.keys(levelColors).map(key => Object.assign({}, {
+ level: key,
+ levelLabel: `levels.${key.toLowerCase()}`,
+ value: data[key]
+ })),
+ timeStamp
+ };
+ }
+
+ /**
+ * The goal of this function is to set the tooltip position regarding the d3.mouse event relative to the @htmlElement.
+ * Onlty if we have @tooltipInfo
+ */
+ private setTooltipPosition():void {
+ if (this.tooltipInfo) {
+ let tEl = this.tooltipElement;
+ let pos = d3.mouse(this.htmlElement);
+ let left = pos[0];
+ let top = pos[1] - (tEl.offsetHeight / 2);
+ let tooltipWidth = tEl.offsetWidth;
+ let windowSize = window.innerWidth;
+ if (left + tooltipWidth > windowSize) {
+ left = pos[0] - (tooltipWidth + 25);
+ }
+ this.tooltipOnTheLeft = left < pos[0];
+ this.tooltipPosition = {left, top};
+ }
+ };
+
+ /**
+ * The goal is to calculate the time gap between the given dates. It will return an object representing the unit and
+ * the value in the given unit. Eg.: {unit: 'minute', value: 5}
+ * @param {Date} startDate
+ * @param {Date} endDate
+ * @returns {{value: number; unit: string, label: string}}
+ */
+ private getTimeGap(startDate: Date, endDate: Date): {value: number, unit: string, label: string} {
+ const startDateMoment = moment(startDate);
+ const endDateMoment = moment(endDate);
+ const diffInWeek: number = endDateMoment.diff(startDateMoment, 'weeks');
+ const diffInDay: number = endDateMoment.diff(startDateMoment, 'days');
+ const diffInHour: number = endDateMoment.diff(startDateMoment, 'hours');
+ const diffInMin: number = endDateMoment.diff(startDateMoment, 'minutes');
+ const diffInSec: number = endDateMoment.diff(startDateMoment, 'seconds');
+ const value = diffInWeek >= 1 ? diffInWeek : (
+ diffInDay >= 1 ? diffInDay : (
+ diffInHour >= 1 ? diffInHour : (diffInMin >= 1 ? diffInMin : diffInSec)
+ )
+ );
+ const unit: string = diffInWeek >= 1 ? 'week' : (
+ diffInDay >= 1 ? `day` : (
+ diffInHour >= 1 ? `hour` : (diffInMin >= 1 ? `minute` : `second`)
+ )
+ );
+ const label = `histogram.gap.${unit}${value>1 ? 's' : ''}`;
+ return {
+ value,
+ unit,
+ label
+ };
+ }
+
+ /**
+ * The goal is to have a simple function to set the time gap corresponding to the xScale ticks.
+ * It will reset the time gap if the xScale is not set or there are no ticks.
+ */
+ private setChartTimeGapByXScale() {
+ let ticks = this.xScale && this.xScale.ticks();
+ if (ticks && ticks.length) {
+ this.setChartTimeGap(ticks[0], ticks[1] || ticks[0]);
+ } else {
+ this.resetChartTimeGap();
+ }
+ }
+
+ /**
+ * Simply reset the time gap property to null.
+ */
+ private resetChartTimeGap(): void {
+ this.chartTimeGap = null;
+ }
+
+ /**
+ * The goal is to have a single point where we set the chartTimeGap property corresponding the given timerange.
+ * @param {Date} startDate
+ * @param {Date} endDate
+ */
+ private setChartTimeGap(startDate: Date, endDate: Date): void {
+ this.chartTimeGap = this.getTimeGap(startDate, endDate);
+ }
+
+ /**
+ * Set the domain for the y scale regarding the given data. The maximum value of the data is the sum of the log level
+ * values.
+ * An example data: [{timeStamp: 1233455677, WARN: 12, ERROR: 123}]
+ * @param {Array<{timeStamp: number; [p: string]: number}>} data
+ */
+ private setYScaleDomain(data: Array<{timeStamp: number, [key: string]: number}>): void {
+ const keys = Object.keys(this.options.keysWithColors);
+ const maxYValue = d3.max(data, item => keys.reduce((sum: number, key: string): number => sum + item[key], 0));
+ this.yScale.domain([0, maxYValue]);
+ }
+
+ /**
+ * Set the domain values for the x scale regarding the given data.
+ * An example data: [{timeStamp: 1233455677, WARN: 12, ERROR: 123}]
+ * @param {Array<{timeStamp: number; [p: string]: any}>} data
+ */
+ private setXScaleDomain(data: Array<{timeStamp: number, [key: string]: any}>): void {
+ this.xScale.domain(d3.extent(data, item => item.timeStamp)).nice();
}
private populate(): void {
- const keys = Object.keys(this.options.keysWithColors),
- data = this.data,
- timeStamps = Object.keys(data),
- formattedData = timeStamps.map((timeStamp: string): {[key: string]: number} => Object.assign({
+ const keys = Object.keys(this.options.keysWithColors);
+ const data = this.data;
+ const timeStamps = Object.keys(data);
+ // we create a more consumable data structure for d3
+ const formattedData = timeStamps.map((timeStamp: string): {timeStamp: number, [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: number, key: string): number => sum + item[key], 0))]);
+ }, data[timeStamp]));
+ const layers = (d3.stack().keys(keys)(formattedData));
+
+ // after we have the data we set the domain values both scales
+ this.setXScaleDomain(formattedData);
+ this.setYScaleDomain(formattedData);
+
+ // Setting the timegap label above the chart
+ this.setChartTimeGapByXScale();
+
+ let unitD3TimeProp = this.chartTimeGap.unit.charAt(0).toUpperCase() + this.chartTimeGap.unit.slice(1);
+ this.xScale.nice(d3[`time${unitD3TimeProp}`], 2);
+
+ let columnWidth = this.options.columnWidth[this.chartTimeGap.unit] || this.options.columnWidth.base;
+
+ // drawing the axis
this.drawXAxis();
this.drawYAxis();
- 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));
+
+ // populate the data and drawing the bars
+ const layer = this.svg.selectAll('.value').data(d3.transpose<any>(layers))
+ .attr('class', 'value')
+ .enter().append('g')
+ .attr('class', 'value');
+ layer.selectAll('.value rect').data(item => item)
+ .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))
+ .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))
+ .on('mouseover', this.handleRectMouseOver)
+ .on('mousemove', this.handleRectMouseMove)
+ .on('mouseout', this.handleRectMouseOut);
this.setDragBehavior();
}
+ private getTimeRangeByXRanges(startX: number, endX:number): [number, number] {
+ const xScaleInterval = this.xScale.domain().map((point: Date): number => point.valueOf());
+ const xScaleLength = xScaleInterval[1] - xScaleInterval[0];
+ const ratio = xScaleLength / this.width;
+ return [Math.round(xScaleInterval[0] + ratio * startX), Math.round(xScaleInterval[0] + ratio * endX)];
+ }
+
+ /**
+ * The goal is to create the two shadow rectangle beside the selected area. Actually we blurout the not selected
+ * timeranges
+ * @param {number} startX This is the starting position of the drag event withing the container
+ * @param {number} currentX This is the ending point of the drag within the container
+ */
+ private createInvertDragArea(startX: number, currentX: number): void {
+ const height: number = this.options.height + this.options.margin.top + this.options.margin.bottom;
+ this.leftDragArea = this.svg.insert('rect').attr('height', height).attr('class', 'unselected-drag-area');
+ this.rightDragArea = this.svg.insert('rect').attr('height', height).attr('class', 'unselected-drag-area');
+ this.setInvertDragArea(startX, currentX);
+ }
+
+ /**
+ * Set the position and the width of the blur/shadow rectangles of the unselected area(s).
+ * @param {number} startX The start point of the selected area.
+ * @param {number} currentX The end point of the selected area.
+ */
+ private setInvertDragArea(startX: number, currentX: number): void {
+ const left: number = Math.min(startX, currentX);
+ const right: number = Math.max(startX, currentX);
+ let rightAreaWidth: number = this.width - right;
+ rightAreaWidth = rightAreaWidth > 0 ? rightAreaWidth : 0;
+ let leftAreaWidth: number = left > 0 ? left : 0;
+ this.leftDragArea.attr('x', 0).attr('width', leftAreaWidth);
+ this.rightDragArea.attr('x', right).attr('width', rightAreaWidth);
+ }
+
+ /**
+ * The goal is to have a single point where we remove the rectangles of the blur/shadow, unselected time range(s)
+ */
+ private clearInvertDragArea(): void {
+ this.leftDragArea.remove();
+ this.rightDragArea.remove();
+ }
+
private setDragBehavior(): void {
this.minDragX = this.options.margin.left;
this.maxDragX = this.htmlElement.clientWidth;
@@ -188,25 +481,25 @@ export class TimeHistogramComponent implements OnInit, AfterViewInit, OnChanges
}
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);
+ .attr('height', this.options.height).attr('class', 'drag-area');
})
.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);
+ const mousePos = this.getDragX(containers[0]);
+ const currentX = Math.max(mousePos, this.minDragX) - this.options.margin.left;
+ const startX = Math.min(currentX, this.dragStartX);
+ const currentWidth = Math.abs(currentX - this.dragStartX);
this.dragArea.attr('x', startX).attr('width', currentWidth);
+ let timeRange = this.getTimeRangeByXRanges(startX, startX + currentWidth);
+ this.setChartTimeGap(new Date(timeRange[0]), new Date(timeRange[1]));
})
.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]);
+ const dragAreaDetails = this.dragArea.node().getBBox();
+ const startX = Math.max(0, dragAreaDetails.x);
+ const endX = Math.min(this.width, dragAreaDetails.x + dragAreaDetails.width);
+ const dateRange: [number, number] = this.getTimeRangeByXRanges(startX, endX);
+ this.selectArea.emit(dateRange);
this.dragArea.remove();
+ this.setChartTimeGap(new Date(dateRange[0]), new Date(dateRange[1]));
})
);
d3.selectAll(`svg#${this.svgId} .value, svg#${this.svgId} .axis`).call(d3.drag().on('start', (): void => {
http://git-wip-us.apache.org/repos/asf/ambari/blob/b1fd131f/ambari-logsearch/ambari-logsearch-web/src/app/services/logs-container.service.ts
----------------------------------------------------------------------
diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/services/logs-container.service.ts b/ambari-logsearch/ambari-logsearch-web/src/app/services/logs-container.service.ts
index e754aa4..d719893 100644
--- a/ambari-logsearch/ambari-logsearch-web/src/app/services/logs-container.service.ts
+++ b/ambari-logsearch/ambari-logsearch-web/src/app/services/logs-container.service.ts
@@ -476,9 +476,9 @@ export class LogsContainerService {
};
readonly colors = {
- WARN: '#FF8916',
- ERROR: '#E81D1D',
FATAL: '#830A0A',
+ ERROR: '#E81D1D',
+ WARN: '#FF8916',
INFO: '#2577B5',
DEBUG: '#65E8FF',
TRACE: '#888',
http://git-wip-us.apache.org/repos/asf/ambari/blob/b1fd131f/ambari-logsearch/ambari-logsearch-web/src/assets/i18n/en.json
----------------------------------------------------------------------
diff --git a/ambari-logsearch/ambari-logsearch-web/src/assets/i18n/en.json b/ambari-logsearch/ambari-logsearch-web/src/assets/i18n/en.json
index 6c916aa..3f4f5c8 100644
--- a/ambari-logsearch/ambari-logsearch-web/src/assets/i18n/en.json
+++ b/ambari-logsearch/ambari-logsearch-web/src/assets/i18n/en.json
@@ -158,5 +158,16 @@
"logs.totalEventFound": "{{totalCount}} events found",
"logs.noEventFound": "No event found",
"logs.hideGraph": "Hide Graph",
- "logs.showGraph": "Show Graph"
+ "logs.showGraph": "Show Graph",
+
+ "histogram.gap": "gap",
+ "histogram.gaps": "gaps",
+ "histogram.gap.second": "second",
+ "histogram.gap.seconds": "seconds",
+ "histogram.gap.minute": "minute",
+ "histogram.gap.minutes": "minutes",
+ "histogram.gap.hour": "hour",
+ "histogram.gap.hours": "hours",
+ "histogram.gap.day": "day",
+ "histogram.gap.days": "days"
}