AMBARI-22714 Log Search UI: implement Summary tab for Access Logs page. (ababiichuk)
authorababiichuk <ababiichuk@hortonworks.com>
Tue, 2 Jan 2018 13:08:44 +0000 (15:08 +0200)
committerababiichuk <ababiichuk@hortonworks.com>
Tue, 2 Jan 2018 13:43:32 +0000 (15:43 +0200)
67 files changed:
ambari-logsearch/ambari-logsearch-web/package.json
ambari-logsearch/ambari-logsearch-web/src/app/app.module.ts
ambari-logsearch/ambari-logsearch-web/src/app/classes/components/graph/graph.component.less [new file with mode: 0644]
ambari-logsearch/ambari-logsearch-web/src/app/classes/components/graph/graph.component.ts [new file with mode: 0644]
ambari-logsearch/ambari-logsearch-web/src/app/classes/components/logs-table/logs-table-component.spec.ts [moved from ambari-logsearch/ambari-logsearch-web/src/app/classes/components/logs-table-component.spec.ts with 100% similarity]
ambari-logsearch/ambari-logsearch-web/src/app/classes/components/logs-table/logs-table-component.ts [moved from ambari-logsearch/ambari-logsearch-web/src/app/classes/components/logs-table-component.ts with 100% similarity]
ambari-logsearch/ambari-logsearch-web/src/app/classes/graph.ts [moved from ambari-logsearch/ambari-logsearch-web/src/app/classes/histogram-options.ts with 72% similarity]
ambari-logsearch/ambari-logsearch-web/src/app/classes/models/tab.ts
ambari-logsearch/ambari-logsearch-web/src/app/classes/object.ts [new file with mode: 0644]
ambari-logsearch/ambari-logsearch-web/src/app/classes/queries/audit-logs-query-params.ts
ambari-logsearch/ambari-logsearch-web/src/app/classes/queries/audit-logs-top-resources-query-params.ts [new file with mode: 0644]
ambari-logsearch/ambari-logsearch-web/src/app/classes/queries/service-logs-query-params.ts
ambari-logsearch/ambari-logsearch-web/src/app/classes/service-injector.ts [new file with mode: 0644]
ambari-logsearch/ambari-logsearch-web/src/app/components/audit-logs-entries/audit-logs-entries.component.html [new file with mode: 0644]
ambari-logsearch/ambari-logsearch-web/src/app/components/audit-logs-entries/audit-logs-entries.component.spec.ts [new file with mode: 0644]
ambari-logsearch/ambari-logsearch-web/src/app/components/audit-logs-entries/audit-logs-entries.component.ts [new file with mode: 0644]
ambari-logsearch/ambari-logsearch-web/src/app/components/audit-logs-table/audit-logs-table.component.ts
ambari-logsearch/ambari-logsearch-web/src/app/components/collapsible-panel/collapsible-panel.component.html
ambari-logsearch/ambari-logsearch-web/src/app/components/collapsible-panel/collapsible-panel.component.ts
ambari-logsearch/ambari-logsearch-web/src/app/components/dropdown-button/dropdown-button.component.spec.ts
ambari-logsearch/ambari-logsearch-web/src/app/components/dropdown-button/dropdown-button.component.ts
ambari-logsearch/ambari-logsearch-web/src/app/components/filter-button/filter-button.component.spec.ts
ambari-logsearch/ambari-logsearch-web/src/app/components/filter-button/filter-button.component.ts
ambari-logsearch/ambari-logsearch-web/src/app/components/filter-dropdown/filter-dropdown.component.spec.ts
ambari-logsearch/ambari-logsearch-web/src/app/components/filter-dropdown/filter-dropdown.component.ts
ambari-logsearch/ambari-logsearch-web/src/app/components/filters-panel/filters-panel.component.ts
ambari-logsearch/ambari-logsearch-web/src/app/components/graph-legend-item/graph-legend-item.component.html [new file with mode: 0644]
ambari-logsearch/ambari-logsearch-web/src/app/components/graph-legend-item/graph-legend-item.component.less [new file with mode: 0644]
ambari-logsearch/ambari-logsearch-web/src/app/components/graph-legend-item/graph-legend-item.component.spec.ts [new file with mode: 0644]
ambari-logsearch/ambari-logsearch-web/src/app/components/graph-legend-item/graph-legend-item.component.ts [new file with mode: 0644]
ambari-logsearch/ambari-logsearch-web/src/app/components/graph-legend/graph-legend.component.html [new file with mode: 0644]
ambari-logsearch/ambari-logsearch-web/src/app/components/graph-legend/graph-legend.component.spec.ts [new file with mode: 0644]
ambari-logsearch/ambari-logsearch-web/src/app/components/graph-legend/graph-legend.component.ts [new file with mode: 0644]
ambari-logsearch/ambari-logsearch-web/src/app/components/graph-tooltip/graph-tooltip.component.html [new file with mode: 0644]
ambari-logsearch/ambari-logsearch-web/src/app/components/graph-tooltip/graph-tooltip.component.less [new file with mode: 0644]
ambari-logsearch/ambari-logsearch-web/src/app/components/graph-tooltip/graph-tooltip.component.spec.ts [new file with mode: 0644]
ambari-logsearch/ambari-logsearch-web/src/app/components/graph-tooltip/graph-tooltip.component.ts [new file with mode: 0644]
ambari-logsearch/ambari-logsearch-web/src/app/components/horizontal-histogram/horizontal-histogram.component.html [new file with mode: 0644]
ambari-logsearch/ambari-logsearch-web/src/app/components/horizontal-histogram/horizontal-histogram.component.less [new file with mode: 0644]
ambari-logsearch/ambari-logsearch-web/src/app/components/horizontal-histogram/horizontal-histogram.component.spec.ts [new file with mode: 0644]
ambari-logsearch/ambari-logsearch-web/src/app/components/horizontal-histogram/horizontal-histogram.component.ts [new file with mode: 0644]
ambari-logsearch/ambari-logsearch-web/src/app/components/logs-container/logs-container.component.html
ambari-logsearch/ambari-logsearch-web/src/app/components/logs-container/logs-container.component.ts
ambari-logsearch/ambari-logsearch-web/src/app/components/menu-button/menu-button.component.spec.ts
ambari-logsearch/ambari-logsearch-web/src/app/components/menu-button/menu-button.component.ts
ambari-logsearch/ambari-logsearch-web/src/app/components/search-box/search-box.component.ts
ambari-logsearch/ambari-logsearch-web/src/app/components/service-logs-table/service-logs-table.component.ts
ambari-logsearch/ambari-logsearch-web/src/app/components/tabs/tabs.component.spec.ts
ambari-logsearch/ambari-logsearch-web/src/app/components/tabs/tabs.component.ts
ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.html
ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.less
ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.spec.ts
ambari-logsearch/ambari-logsearch-web/src/app/components/time-histogram/time-histogram.component.ts
ambari-logsearch/ambari-logsearch-web/src/app/components/top-menu/top-menu.component.ts
ambari-logsearch/ambari-logsearch-web/src/app/components/variables.less
ambari-logsearch/ambari-logsearch-web/src/app/mock-data.ts
ambari-logsearch/ambari-logsearch-web/src/app/services/component-actions.service.ts
ambari-logsearch/ambari-logsearch-web/src/app/services/http-client.service.ts
ambari-logsearch/ambari-logsearch-web/src/app/services/logs-container.service.spec.ts
ambari-logsearch/ambari-logsearch-web/src/app/services/logs-container.service.ts
ambari-logsearch/ambari-logsearch-web/src/app/services/utils.service.spec.ts
ambari-logsearch/ambari-logsearch-web/src/app/services/utils.service.ts
ambari-logsearch/ambari-logsearch-web/src/assets/i18n/en.json
ambari-logsearch/ambari-logsearch-web/src/styles.less
ambari-logsearch/ambari-logsearch-web/src/vendor/css/bootstrap-logsearch.min.css
ambari-logsearch/ambari-logsearch-web/src/vendor/js/bootstrap-logsearch.min.js
ambari-logsearch/ambari-logsearch-web/yarn.lock

index b9ee179..2a3df23 100644 (file)
@@ -30,6 +30,7 @@
     "bootstrap": "^3.3.7",
     "core-js": "^2.4.1",
     "d3": "^4.10.0",
+    "d3-scale-chromatic": "^1.1.1",
     "font-awesome": "^4.7.0",
     "jquery": "^1.12.4",
     "moment": "^2.18.1",
@@ -43,6 +44,7 @@
     "@angular/compiler-cli": "^4.0.0",
     "@ngtools/webpack": "^1.7.1",
     "@types/d3": "^4.10.0",
+    "@types/d3-scale-chromatic": "^1.1.0",
     "@types/jasmine": "2.5.38",
     "@types/jquery": "^1.10.33",
     "@types/moment": "^2.13.0",
index b76de20..da78a71 100644 (file)
@@ -30,6 +30,8 @@ import {MomentTimezoneModule} from 'angular-moment-timezone';
 
 import {environment} from '@envs/environment';
 
+import {ServiceInjector} from '@app/classes/service-injector';
+
 import {mockApiDataService} from '@app/services/mock-api-data.service'
 import {HttpClientService} from '@app/services/http-client.service';
 import {ComponentActionsService} from '@app/services/component-actions.service';
@@ -85,6 +87,11 @@ import {LogFileEntryComponent} from '@app/components/log-file-entry/log-file-ent
 import {TabsComponent} from '@app/components/tabs/tabs.component';
 import {ServiceLogsTableComponent} from '@app/components/service-logs-table/service-logs-table.component';
 import {AuditLogsTableComponent} from '@app/components/audit-logs-table/audit-logs-table.component';
+import {AuditLogsEntriesComponent} from '@app/components/audit-logs-entries/audit-logs-entries.component';
+import {GraphLegendComponent} from '@app/components/graph-legend/graph-legend.component';
+import {HorizontalHistogramComponent} from '@app/components/horizontal-histogram/horizontal-histogram.component';
+import {GraphTooltipComponent} from '@app/components/graph-tooltip/graph-tooltip.component';
+import {GraphLegendItemComponent} from '@app/components/graph-legend-item/graph-legend-item.component';
 
 import {TimeZoneAbbrPipe} from '@app/pipes/timezone-abbr.pipe';
 import {TimerSecondsPipe} from '@app/pipes/timer-seconds.pipe';
@@ -141,6 +148,11 @@ export function getXHRBackend(injector: Injector, browser: BrowserXhr, xsrf: XSR
     TabsComponent,
     ServiceLogsTableComponent,
     AuditLogsTableComponent,
+    AuditLogsEntriesComponent,
+    GraphLegendComponent,
+    HorizontalHistogramComponent,
+    GraphTooltipComponent,
+    GraphLegendItemComponent,
     TimeZoneAbbrPipe,
     TimerSecondsPipe
   ],
@@ -194,4 +206,7 @@ export function getXHRBackend(injector: Injector, browser: BrowserXhr, xsrf: XSR
   schemas: [CUSTOM_ELEMENTS_SCHEMA]
 })
 export class AppModule {
+  constructor(private injector: Injector) {
+    ServiceInjector.injector = this.injector;
+  }
 }
diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/classes/components/graph/graph.component.less b/ambari-logsearch/ambari-logsearch-web/src/app/classes/components/graph/graph.component.less
new file mode 100644 (file)
index 0000000..0830193
--- /dev/null
@@ -0,0 +1,48 @@
+/**
+ * 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.
+ */
+
+@import '../../../components/variables';
+
+:host {
+  display: block;
+
+  /deep/ .axis {
+    .domain {
+      stroke: @base-font-color;
+    }
+    .tick {
+      line {
+        display: none;
+      }
+    }
+  }
+
+  /deep/ .value {
+    cursor: pointer;
+    rect {
+      transition: opacity 250ms;
+      opacity: .8;
+      &:hover {
+        opacity: 1;
+      }
+    }
+  }
+
+  graph-legend {
+    font-size: 1rem;
+  }
+}
diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/classes/components/graph/graph.component.ts b/ambari-logsearch/ambari-logsearch-web/src/app/classes/components/graph/graph.component.ts
new file mode 100644 (file)
index 0000000..1bb7f92
--- /dev/null
@@ -0,0 +1,355 @@
+/**
+ * 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.
+ */
+
+import {AfterViewInit, OnChanges, SimpleChanges, ViewChild, ElementRef, Input} from '@angular/core';
+import * as d3 from 'd3';
+import * as d3sc from 'd3-scale-chromatic';
+import {GraphPositionOptions, GraphMarginOptions, GraphTooltipInfo, LegendItem} from '@app/classes/graph';
+import {HomogeneousObject} from '@app/classes/object';
+import {ServiceInjector} from '@app/classes/service-injector';
+import {UtilsService} from '@app/services/utils.service';
+
+export class GraphComponent implements AfterViewInit, OnChanges {
+
+  constructor() {
+    this.utils = ServiceInjector.injector.get(UtilsService);
+  }
+
+  ngAfterViewInit() {
+    this.graphContainer = this.graphContainerRef.nativeElement;
+    this.tooltip = this.tooltipRef.nativeElement;
+    this.host = d3.select(this.graphContainer);
+  }
+
+  ngOnChanges(changes: SimpleChanges) {
+    const dataChange = changes.data;
+    if (dataChange && dataChange.currentValue && !this.utils.isEmptyObject(dataChange.currentValue)
+      && (!dataChange.previousValue || this.utils.isEmptyObject(dataChange.previousValue))
+      && this.utils.isEmptyObject(this.labels)) {
+      this.setDefaultLabels();
+    }
+    this.createGraph();
+  }
+
+  @Input()
+  data: HomogeneousObject<HomogeneousObject<number>> = {};
+
+  @Input()
+  svgId: string = 'graph-svg';
+
+  @Input()
+  margin: GraphMarginOptions = {
+    top: 5,
+    right: 50,
+    bottom: 30,
+    left: 50
+  };
+
+  @Input()
+  width: number;
+
+  @Input()
+  height: number = 150;
+
+  @Input()
+  tickPadding: number = 10;
+
+  @Input()
+  colors: HomogeneousObject<string> = {};
+
+  @Input()
+  labels: HomogeneousObject<string> = {};
+
+  /**
+   * Indicates whether the graph represents dependency on time
+   * @type {boolean}
+   */
+  @Input()
+  isTimeGraph: boolean = false;
+
+  /**
+   * Indicates whether X axis direction is right to left
+   * @type {boolean}
+   */
+  @Input()
+  reverseXRange: boolean = false;
+
+  /**
+   * Indicates whether Y axis direction is top to bottom
+   * @type {boolean}
+   */
+  @Input()
+  reverseYRange: boolean = false;
+
+  @ViewChild('graphContainer')
+  graphContainerRef: ElementRef;
+
+  @ViewChild('tooltip', {
+    read: ElementRef
+  })
+  tooltipRef: ElementRef;
+
+  protected utils: UtilsService;
+
+  protected graphContainer: HTMLElement;
+
+  private tooltip: HTMLElement;
+
+  protected host;
+
+  protected svg;
+
+  protected xScale;
+
+  protected yScale;
+
+  protected xAxis;
+
+  protected yAxis;
+
+  /**
+   * Ordered array of color strings for data representation
+   * @type {string[]}
+   */
+  protected orderedColors: string[];
+
+  /**
+   * This property is to hold the data of the bar where the mouse is over.
+   */
+  protected tooltipInfo: GraphTooltipInfo | {} = {};
+
+  /**
+   * This is the computed position of the tooltip relative to the @graphContainer which is the container of the histogram.
+   * It is set when the mousemoving over the bars in the @handleRectMouseMove method.
+   */
+  private tooltipPosition: GraphPositionOptions;
+
+  /**
+   * 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 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 {LegendItem[]}
+   */
+  get legendItems(): LegendItem[] {
+    return Object.keys(this.labels).map((key: string) => Object.assign({}, {
+      label: this.labels[key],
+      color: this.colors[key]
+    }));
+  }
+
+  protected createGraph(): void {
+    if (this.host && !this.utils.isEmptyObject(this.labels)) {
+      this.setup();
+      this.buildSVG();
+      this.populate();
+    }
+  }
+
+  /**
+   * Method that sets default labels map object based on data if no custom one is specified
+   */
+  protected setDefaultLabels() {
+    const data = this.data,
+      keys = Object.keys(data),
+      labels = keys.reduce((keys: HomogeneousObject<string>, dataKey: string): HomogeneousObject<string> => {
+        const newKeys = Object.keys(data[dataKey]),
+          newKeysObj = newKeys.reduce((subKeys: HomogeneousObject<string>, key: string): HomogeneousObject<string> => {
+            return Object.assign(subKeys, {
+              [key]: key
+            });
+        }, {});
+        return Object.assign(keys, newKeysObj);
+      }, {});
+    this.labels = labels;
+  }
+
+  protected setup(): void {
+    const margin = this.margin;
+    if (this.utils.isEmptyObject(this.colors)) {
+      // set default color scheme for different values if no custom colors specified
+      const keys = Object.keys(this.labels),
+        keysCount = keys.length,
+        specterLength = keysCount > 2 ? keysCount : 3; // length of minimal available spectral scheme is 3
+      let colorsArray;
+      if (keysCount > 2) {
+        colorsArray = Array.from(d3sc.schemeSpectral[keysCount]);
+      } else {
+        const minimalColorScheme = Array.from(d3sc.schemeSpectral[specterLength]);
+        colorsArray = minimalColorScheme.slice(0, keysCount);
+      }
+      this.orderedColors = colorsArray;
+      this.colors = keys.reduce((currentObject: HomogeneousObject<string>, currentKey: string, index: number) => {
+        return Object.assign(currentObject, {
+          [currentKey]: colorsArray[index]
+        });
+      }, {});
+    } else {
+      const keysWithColors = this.colors,
+        keys = Object.keys(keysWithColors);
+      this.orderedColors = keys.reduce((array: string[], key: string): string[] => [...array, keysWithColors[key]], []);
+    }
+    if (!this.width) {
+      this.width = this.graphContainer.clientWidth - margin.left - margin.right;
+    }
+    const xScale = this.isTimeGraph ? d3.scaleTime() : d3.scaleLinear();
+    const yScale = d3.scaleLinear();
+    const xScaleWithRange = this.reverseXRange ? xScale.range([this.width, 0]) : xScale.range([0, this.width]);
+    const yScaleWithRange = this.reverseYRange ? yScale.range([0, this.height]) : yScale.range([this.height, 0]);
+    this.xScale = xScaleWithRange;
+    this.yScale = yScaleWithRange;
+  }
+
+  protected buildSVG(): void {
+    const margin = this.margin;
+    this.host.html('');
+    this.svg = this.host.append('svg').attr('id', this.svgId).attr('width', this.graphContainer.clientWidth)
+      .attr('height', this.height + margin.top + margin.bottom).append('g')
+      .attr('transform', `translate(${margin.left},${margin.top})`);
+  }
+
+  protected populate(): void {}
+
+  protected setXScaleDomain(formattedData?: any): void {}
+
+  protected setYScaleDomain(formattedData?: any): void {}
+
+  /**
+   * 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.
+   * @param {number} ticksCount - optional parameter which sets number of ticks explicitly
+   */
+  protected drawXAxis(ticksCount?: number): void {
+    const axis = d3.axisBottom(this.xScale).tickFormat(this.xAxisTickFormatter).tickPadding(this.tickPadding);
+    if (ticksCount) {
+      axis.ticks(ticksCount);
+    }
+    this.xAxis = axis;
+    this.svg.append('g').attr('class', 'axis axis-x').attr('transform', `translate(0,${this.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.
+   * @param {number} ticksCount - optional parameter which sets number of ticks explicitly
+   */
+  protected drawYAxis(ticksCount?: number): void {
+    const axis = d3.axisLeft(this.yScale).tickFormat(this.yAxisTickFormatter).tickPadding(this.tickPadding);
+    if (ticksCount) {
+      axis.ticks(ticksCount);
+    }
+    this.yAxis = axis;
+    this.svg.append('g').attr('class', 'axis axis-y').call(this.yAxis).append('text');
+  };
+
+  /**
+   * Function that formats the labels for X axis ticks.
+   * Returns simple toString() conversion as default, can be overridden in ancestors.
+   * undefined value is returned for ticks to be skipped.
+   * @param tick
+   * @param {number} index
+   * @returns {string|undefined}
+   */
+  protected xAxisTickFormatter = (tick: any, index: number): string | undefined => {
+    return tick.toString();
+  };
+
+  /**
+   * Function that formats the labels for Y axis ticks.
+   * Returns simple toString() conversion as default, can be overridden in ancestors.
+   * undefined value is returned for ticks to be skipped.
+   * @param tick
+   * @param {number} index
+   * @returns {string|undefined}
+   */
+  protected yAxisTickFormatter = (tick: any, index: number): string | undefined => {
+    return tick.toString();
+  };
+
+  /**
+   * 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
+   */
+  protected handleRectMouseOver = (d: {data: any, [key: string]: 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.
+   */
+  protected handleRectMouseMove = (): void => {
+    this.setTooltipPosition();
+  };
+
+  /**
+   * The goal is to reset the tooltipInfo object so that the tooltip will be hidden.
+   */
+  protected handleRectMouseOut = (): void => {
+    this.tooltipInfo = {};
+  };
+
+  /**
+   * The goal is set the tooltip
+   * @param d
+   */
+  protected setTooltipDataFromChartData(d: {data: any, [key: string]: any}): void {
+    let {tick, ...data} = d.data;
+    let levelColors = this.colors;
+    this.tooltipInfo = {
+      data: Object.keys(levelColors).filter((key: string): boolean => data[key] > 0).map((key: string): object => Object.assign({}, {
+        color: this.colors[key],
+        label: this.labels[key],
+        value: data[key]
+      })),
+      title: tick
+    };
+  }
+
+  /**
+   * The goal of this function is to set the tooltip position regarding the d3.mouse event relative to the @graphContainer.
+   * Only if we have @tooltipInfo
+   */
+  protected setTooltipPosition(): void {
+    if (this.tooltipInfo.hasOwnProperty('data')) {
+      const tooltip = this.tooltip,
+        relativeMousePosition = d3.mouse(this.graphContainer),
+        absoluteMousePosition = d3.mouse(document.body),
+        absoluteMouseLeft = absoluteMousePosition[0],
+        top = relativeMousePosition[1] - (tooltip.offsetHeight / 2),
+        tooltipWidth = tooltip.offsetWidth,
+        windowSize = window.innerWidth;
+      let left = relativeMousePosition[0];
+      if (absoluteMouseLeft + tooltipWidth > windowSize) {
+        left = relativeMousePosition[0] - (tooltipWidth + 25);
+      }
+      this.tooltipOnTheLeft = left < relativeMousePosition[0];
+      this.tooltipPosition = {left, top};
+    }
+  };
+
+}
  * limitations under the License.
  */
 
-export interface HistogramMarginOptions {
+export interface GraphPositionOptions {
   top: number;
+  left: number;
+}
+
+export interface GraphMarginOptions extends GraphPositionOptions {
   right: number;
   bottom: number;
-  left: number;
 }
 
-export interface HistogramStyleOptions {
-  margin?: HistogramMarginOptions;
-  height?: number;
-  tickPadding?: number;
-  columnWidth?: {[key:string]: number};
+export interface GraphTooltipInfo {
+  data: object[];
+  title: string | number;
+}
+
+export interface LegendItem {
+  label: string;
+  color: string;
 }
 
-export interface HistogramOptions extends HistogramStyleOptions {
-  keysWithColors: {[key: string]: string};
+export interface GraphScaleItem {
+  tick: number;
+  [key: string]: number;
 }
index 05ea59d..718adf3 100644 (file)
  * limitations under the License.
  */
 
+import {HomogeneousObject} from '@app/classes/object';
+
 export interface Tab {
   id: string;
-  type: string;
   isActive?: boolean;
   isCloseable?: boolean;
   label: string;
-  appState?: object;
+  appState?: HomogeneousObject<any>;
 }
 
 export const initialTabs: Tab[] = [
   {
     id: 'serviceLogs',
-    type: 'serviceLogs',
     isActive: true,
     label: 'common.serviceLogs',
     appState: {
@@ -38,7 +38,6 @@ export const initialTabs: Tab[] = [
   },
   {
     id: 'auditLogs',
-    type: 'auditLogs',
     isActive: false,
     label: 'common.auditLogs',
     appState: {
diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/classes/object.ts b/ambari-logsearch/ambari-logsearch-web/src/app/classes/object.ts
new file mode 100644 (file)
index 0000000..4d0c7f6
--- /dev/null
@@ -0,0 +1,19 @@
+/**
+ * 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 type HomogeneousObject<T> = {[key: string]: T};
index 3b38a03..dc82b9e 100644 (file)
@@ -25,7 +25,17 @@ export const defaultParams = {
 };
 
 export class AuditLogsQueryParams extends QueryParams {
-  constructor(options: AuditLogsQueryParams) {
+  clusters?: string;
+  mustBe?: string;
+  mustNot?: string;
+  includeQuery?: string;
+  excludeQuery?: string;
+  from?: string;
+  to?: string;
+}
+
+export class AuditLogsListQueryParams extends AuditLogsQueryParams {
+  constructor(options: AuditLogsListQueryParams) {
     let finalParams = Object.assign({}, defaultParams, options);
     const page = parseInt(finalParams.page),
       pageSize = parseInt(finalParams.pageSize);
@@ -37,11 +47,4 @@ export class AuditLogsQueryParams extends QueryParams {
   startIndex: string;
   sortBy?: string;
   sortType?: SortingType;
-  clusters?: string;
-  mustBe?: string;
-  mustNot?: string;
-  includeQuery?: string;
-  excludeQuery?: string;
-  from?: string;
-  to?: string;
 }
diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/classes/queries/audit-logs-top-resources-query-params.ts b/ambari-logsearch/ambari-logsearch-web/src/app/classes/queries/audit-logs-top-resources-query-params.ts
new file mode 100644 (file)
index 0000000..0d12539
--- /dev/null
@@ -0,0 +1,23 @@
+/**
+ * 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.
+ */
+
+import {AuditLogsQueryParams} from '@app/classes/queries/audit-logs-query-params';
+
+export class AuditLogsTopResourcesQueryParams extends AuditLogsQueryParams {
+  field: string;
+}
index 0700a98..60c3d5c 100644 (file)
@@ -16,9 +16,9 @@
  * limitations under the License.
  */
 
-import {AuditLogsQueryParams} from '@app/classes/queries/audit-logs-query-params';
+import {AuditLogsListQueryParams} from '@app/classes/queries/audit-logs-query-params';
 
-export class ServiceLogsQueryParams extends AuditLogsQueryParams {
+export class ServiceLogsQueryParams extends AuditLogsListQueryParams {
   level?: string;
   file_name?: string;
   bundle_id?: string;
diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/classes/service-injector.ts b/ambari-logsearch/ambari-logsearch-web/src/app/classes/service-injector.ts
new file mode 100644 (file)
index 0000000..6db65cd
--- /dev/null
@@ -0,0 +1,23 @@
+/**
+ * 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.
+ */
+
+import {Injector} from '@angular/core';
+
+export class ServiceInjector {
+  static injector: Injector;
+}
diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/audit-logs-entries/audit-logs-entries.component.html b/ambari-logsearch/ambari-logsearch-web/src/app/components/audit-logs-entries/audit-logs-entries.component.html
new file mode 100644 (file)
index 0000000..3c5852a
--- /dev/null
@@ -0,0 +1,30 @@
+<!--
+  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.
+-->
+
+<tabs [items]="tabs" (tabSwitched)="setActiveTab($event)"></tabs>
+<ng-container [ngSwitch]="activeTab">
+  <audit-logs-table *ngSwitchCase="'logs'" [totalCount]="totalCount" [logs]="logs" [columns]="columns"
+                    [filtersForm]="filtersForm"></audit-logs-table>
+  <div *ngSwitchCase="'summary'" class="row">
+    <collapsible-panel title="{{'logs.topUsers' | translate: usersGraphTitleParams}}" class="col-md-6">
+      <horizontal-histogram [data]="topUsersGraphData"></horizontal-histogram>
+    </collapsible-panel>
+    <collapsible-panel title="{{'logs.topResources' | translate: resourcesGraphTitleParams}}" class="col-md-6">
+      <horizontal-histogram [data]="topResourcesGraphData"></horizontal-histogram>
+    </collapsible-panel>
+  </div>
+</ng-container>
diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/audit-logs-entries/audit-logs-entries.component.spec.ts b/ambari-logsearch/ambari-logsearch-web/src/app/components/audit-logs-entries/audit-logs-entries.component.spec.ts
new file mode 100644 (file)
index 0000000..260b383
--- /dev/null
@@ -0,0 +1,110 @@
+/**
+ * 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.
+ */
+
+import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
+import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {TranslationModules} from '@app/test-config.spec';
+import {StoreModule} from '@ngrx/store';
+import {AuditLogsService, auditLogs} from '@app/services/storage/audit-logs.service';
+import {ServiceLogsService, serviceLogs} from '@app/services/storage/service-logs.service';
+import {AuditLogsFieldsService, auditLogsFields} from '@app/services/storage/audit-logs-fields.service';
+import {ServiceLogsFieldsService, serviceLogsFields} from '@app/services/storage/service-logs-fields.service';
+import {
+  ServiceLogsHistogramDataService, serviceLogsHistogramData
+} from '@app/services/storage/service-logs-histogram-data.service';
+import {AppSettingsService, appSettings} from '@app/services/storage/app-settings.service';
+import {AppStateService, appState} from '@app/services/storage/app-state.service';
+import {ClustersService, clusters} from '@app/services/storage/clusters.service';
+import {ComponentsService, components} from '@app/services/storage/components.service';
+import {HostsService, hosts} from '@app/services/storage/hosts.service';
+import {ServiceLogsTruncatedService, serviceLogsTruncated} from '@app/services/storage/service-logs-truncated.service';
+import {TabsService, tabs} from '@app/services/storage/tabs.service';
+import {TabsComponent} from '@app/components/tabs/tabs.component';
+import {LogsContainerService} from '@app/services/logs-container.service';
+import {HttpClientService} from '@app/services/http-client.service';
+
+import {AuditLogsEntriesComponent} from './audit-logs-entries.component';
+
+describe('AuditLogsEntriesComponent', () => {
+  let component: AuditLogsEntriesComponent;
+  let fixture: ComponentFixture<AuditLogsEntriesComponent>;
+
+  beforeEach(async(() => {
+    const httpClient = {
+      get: () => {
+        return {
+          subscribe: () => {
+          }
+        }
+      }
+    };
+    TestBed.configureTestingModule({
+      declarations: [
+        AuditLogsEntriesComponent,
+        TabsComponent
+      ],
+      imports: [
+        ...TranslationModules,
+        StoreModule.provideStore({
+          auditLogs,
+          serviceLogs,
+          auditLogsFields,
+          serviceLogsFields,
+          serviceLogsHistogramData,
+          appSettings,
+          appState,
+          clusters,
+          components,
+          hosts,
+          serviceLogsTruncated,
+          tabs
+        }),
+      ],
+      providers: [
+        LogsContainerService,
+        {
+          provide: HttpClientService,
+          useValue: httpClient
+        },
+        AuditLogsService,
+        ServiceLogsService,
+        AuditLogsFieldsService,
+        ServiceLogsFieldsService,
+        ServiceLogsHistogramDataService,
+        AppSettingsService,
+        AppStateService,
+        ClustersService,
+        ComponentsService,
+        HostsService,
+        ServiceLogsTruncatedService,
+        TabsService
+      ],
+      schemas: [CUSTOM_ELEMENTS_SCHEMA]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(AuditLogsEntriesComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create component', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/audit-logs-entries/audit-logs-entries.component.ts b/ambari-logsearch/ambari-logsearch-web/src/app/components/audit-logs-entries/audit-logs-entries.component.ts
new file mode 100644 (file)
index 0000000..44786f1
--- /dev/null
@@ -0,0 +1,86 @@
+/**
+ * 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.
+ */
+
+import {Component, Input} from '@angular/core';
+import {FormGroup} from '@angular/forms';
+import {ListItem} from '@app/classes/list-item';
+import {HomogeneousObject} from '@app/classes/object';
+import {AuditLog} from '@app/classes/models/audit-log';
+import {Tab} from '@app/classes/models/tab';
+import {LogsContainerService} from '@app/services/logs-container.service';
+
+@Component({
+  selector: 'audit-logs-entries',
+  templateUrl: './audit-logs-entries.component.html'
+})
+export class AuditLogsEntriesComponent {
+
+  constructor(private logsContainer: LogsContainerService) {
+  }
+
+  @Input()
+  logs: AuditLog[] = [];
+
+  @Input()
+  columns: ListItem[] = [];
+
+  @Input()
+  filtersForm: FormGroup;
+
+  @Input()
+  totalCount: number = 0;
+
+  tabs: Tab[] = [
+    {
+      id: 'summary',
+      isActive: true,
+      label: 'common.summary'
+    },
+    {
+      id: 'logs',
+      isActive: false,
+      label: 'common.logs'
+    }
+  ];
+
+  /**
+   * Id of currently active tab (Summary or Logs)
+   * @type {string}
+   */
+  activeTab: string = 'summary';
+
+  readonly usersGraphTitleParams = {
+    number: this.logsContainer.topUsersCount
+  };
+
+  readonly resourcesGraphTitleParams = {
+    number: this.logsContainer.topResourcesCount
+  };
+
+  get topResourcesGraphData(): HomogeneousObject<HomogeneousObject<number>> {
+    return this.logsContainer.topResourcesGraphData;
+  }
+
+  get topUsersGraphData(): HomogeneousObject<HomogeneousObject<number>> {
+    return this.logsContainer.topUsersGraphData;
+  }
+
+  setActiveTab(tab: Tab): void {
+    this.activeTab = tab.id;
+  }
+
+}
index 0e578ab..deca936 100644 (file)
@@ -18,7 +18,7 @@
 
 import {Component} from '@angular/core';
 import {ListItem} from '@app/classes/list-item';
-import {LogsTableComponent} from '@app/classes/components/logs-table-component';
+import {LogsTableComponent} from '@app/classes/components/logs-table/logs-table-component';
 import {LogsContainerService} from '@app/services/logs-container.service';
 
 @Component({
index cbc0dd7..f82823a 100644 (file)
@@ -46,14 +46,14 @@ export class CollapsiblePanelComponent {
    * @type {string}
    */
   @Input()
-  openTitle: string = 'common.hide';
+  openTitle?: string;
 
   /**
    * The panel's title fo the closed/collapsed state
    * @type {string}
    */
   @Input()
-  collapsedTitle: string = 'common.show';
+  collapsedTitle?: string;
 
   /**
    * This property indicates the position of the caret. It can be 'left' or 'right'
index fc42e3c..9f2bb16 100644 (file)
  * limitations under the License.
  */
 
-import {NO_ERRORS_SCHEMA} from '@angular/core';
-import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {NO_ERRORS_SCHEMA, Injector} from '@angular/core';
+import {async, ComponentFixture, TestBed, inject} from '@angular/core/testing';
 import {TranslationModules} from '@app/test-config.spec';
 import {StoreModule} from '@ngrx/store';
+import {ServiceInjector} from '@app/classes/service-injector';
 import {AppSettingsService, appSettings} from '@app/services/storage/app-settings.service';
 import {ClustersService, clusters} from '@app/services/storage/clusters.service';
 import {ComponentsService, components} from '@app/services/storage/components.service';
@@ -99,11 +100,12 @@ describe('DropdownButtonComponent', () => {
     .compileComponents();
   }));
 
-  beforeEach(() => {
+  beforeEach(inject([Injector], (injector: Injector) => {
+    ServiceInjector.injector = injector;
     fixture = TestBed.createComponent(DropdownButtonComponent);
     component = fixture.componentInstance;
     fixture.detectChanges();
-  });
+  }));
 
   it('should create component', () => {
     expect(component).toBeTruthy();
index a8037d0..ead9e1a 100644 (file)
@@ -18,6 +18,7 @@
 
 import {Component, Input} from '@angular/core';
 import {ListItem} from '@app/classes/list-item';
+import {ServiceInjector} from '@app/classes/service-injector';
 import {ComponentActionsService} from '@app/services/component-actions.service';
 import {UtilsService} from '@app/services/utils.service';
 
@@ -28,7 +29,8 @@ import {UtilsService} from '@app/services/utils.service';
 })
 export class DropdownButtonComponent {
 
-  constructor(protected actions: ComponentActionsService, protected utils: UtilsService) {
+  constructor(protected utils: UtilsService) {
+    this.actions = ServiceInjector.injector.get(ComponentActionsService);
   }
   
   @Input()
@@ -64,6 +66,8 @@ export class DropdownButtonComponent {
   @Input()
   isDropup: boolean = false;
 
+  private actions: ComponentActionsService;
+
   protected selectedItems?: ListItem[] = [];
 
   get selection(): ListItem[] {
index 6a9aca5..082082c 100644 (file)
  * limitations under the License.
  */
 
-import {NO_ERRORS_SCHEMA} from '@angular/core';
-import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {NO_ERRORS_SCHEMA, Injector} from '@angular/core';
+import {async, ComponentFixture, TestBed, inject} from '@angular/core/testing';
 import {TranslationModules} from '@app/test-config.spec';
 import {StoreModule} from '@ngrx/store';
+import {ServiceInjector} from '@app/classes/service-injector';
 import {AppSettingsService, appSettings} from '@app/services/storage/app-settings.service';
 import {ClustersService, clusters} from '@app/services/storage/clusters.service';
 import {ComponentsService, components} from '@app/services/storage/components.service';
@@ -99,11 +100,12 @@ describe('FilterButtonComponent', () => {
     .compileComponents();
   }));
 
-  beforeEach(() => {
+  beforeEach(inject([Injector], (injector: Injector) => {
+    ServiceInjector.injector = injector;
     fixture = TestBed.createComponent(FilterButtonComponent);
     component = fixture.componentInstance;
     fixture.detectChanges();
-  });
+  }));
 
   it('should create component', () => {
     expect(component).toBeTruthy();
index 2dcecd1..e1787a2 100644 (file)
  * limitations under the License.
  */
 
-import {Component, Input, forwardRef} from '@angular/core';
+import {Component, forwardRef} from '@angular/core';
 import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
 import {ListItem} from "@app/classes/list-item";
-import {ComponentActionsService} from '@app/services/component-actions.service';
 import {UtilsService} from '@app/services/utils.service';
 import {MenuButtonComponent} from '@app/components/menu-button/menu-button.component';
 
@@ -37,8 +36,8 @@ import {MenuButtonComponent} from '@app/components/menu-button/menu-button.compo
 })
 export class FilterButtonComponent extends MenuButtonComponent implements ControlValueAccessor {
 
-  constructor(protected actions: ComponentActionsService, private utils: UtilsService) {
-    super(actions);
+  constructor(private utils: UtilsService) {
+    super();
   }
 
   private selectedItems: ListItem[] = [];
index 8293ba0..d085f3e 100644 (file)
  * limitations under the License.
  */
 
-import {NO_ERRORS_SCHEMA} from '@angular/core';
-import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {NO_ERRORS_SCHEMA, Injector} from '@angular/core';
+import {async, ComponentFixture, TestBed, inject} from '@angular/core/testing';
 import {TranslationModules} from '@app/test-config.spec';
 import {StoreModule} from '@ngrx/store';
+import {ServiceInjector} from '@app/classes/service-injector';
 import {AppSettingsService, appSettings} from '@app/services/storage/app-settings.service';
 import {AppStateService, appState} from '@app/services/storage/app-state.service';
 import {AuditLogsService, auditLogs} from '@app/services/storage/audit-logs.service';
 import {AuditLogsFieldsService, auditLogsFields} from '@app/services/storage/audit-logs-fields.service';
 import {ServiceLogsService, serviceLogs} from '@app/services/storage/service-logs.service';
 import {ServiceLogsFieldsService, serviceLogsFields} from '@app/services/storage/service-logs-fields.service';
-import {ServiceLogsHistogramDataService, serviceLogsHistogramData} from '@app/services/storage/service-logs-histogram-data.service';
+import {
+  ServiceLogsHistogramDataService, serviceLogsHistogramData
+} from '@app/services/storage/service-logs-histogram-data.service';
 import {ServiceLogsTruncatedService, serviceLogsTruncated} from '@app/services/storage/service-logs-truncated.service';
 import {TabsService, tabs} from '@app/services/storage/tabs.service';
 import {ClustersService, clusters} from '@app/services/storage/clusters.service';
@@ -118,11 +121,12 @@ describe('FilterDropdownComponent', () => {
     .compileComponents();
   }));
 
-  beforeEach(() => {
+  beforeEach(inject([Injector], (injector: Injector) => {
+    ServiceInjector.injector = injector;
     fixture = TestBed.createComponent(FilterDropdownComponent);
     component = fixture.componentInstance;
     fixture.detectChanges();
-  });
+  }));
 
   it('should create component', () => {
     expect(component).toBeTruthy();
index d677d81..665386b 100644 (file)
@@ -17,7 +17,6 @@
 
 import {Component, forwardRef} from '@angular/core';
 import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
-import {ComponentActionsService} from '@app/services/component-actions.service';
 import {UtilsService} from '@app/services/utils.service';
 import {DropdownButtonComponent} from '@app/components/dropdown-button/dropdown-button.component';
 import {ListItem} from '@app/classes/list-item';
@@ -36,8 +35,8 @@ import {ListItem} from '@app/classes/list-item';
 })
 export class FilterDropdownComponent extends DropdownButtonComponent implements ControlValueAccessor {
 
-  constructor(protected actions: ComponentActionsService, protected utils: UtilsService) {
-    super(actions, utils);
+  constructor(protected utils: UtilsService) {
+    super(utils);
   }
 
   private onChange: (fn: any) => void;
index 480706a..cd372ec 100644 (file)
@@ -23,6 +23,7 @@ import {Subject} from 'rxjs/Subject';
 import 'rxjs/add/observable/from';
 import {FilterCondition, SearchBoxParameter, SearchBoxParameterTriggered} from '@app/classes/filtering';
 import {ListItem} from '@app/classes/list-item';
+import {HomogeneousObject} from '@app/classes/object';
 import {LogsType} from '@app/classes/string';
 import {LogsContainerService} from '@app/services/logs-container.service';
 
@@ -66,15 +67,15 @@ export class FiltersPanelComponent implements OnChanges {
     return this.viewContainerRef.element.nativeElement;
   }
 
-  get filters(): {[key: string]: FilterCondition} {
+  get filters(): HomogeneousObject<FilterCondition> {
     return this.logsContainer.filters;
   }
 
   /**
    * Object with options for search box parameter values
-   * @returns {[key: string]: ListItem[]}
+   * @returns HomogeneousObject<ListItem[]>
    */
-  get options(): {[key: string]: ListItem[]} {
+  get options(): HomogeneousObject<ListItem[]> {
     return Object.keys(this.filters).filter((key: string): boolean => {
       const condition = this.filters[key];
       return Boolean(condition.fieldName && condition.options);
diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/graph-legend-item/graph-legend-item.component.html b/ambari-logsearch/ambari-logsearch-web/src/app/components/graph-legend-item/graph-legend-item.component.html
new file mode 100644 (file)
index 0000000..e1ebb70
--- /dev/null
@@ -0,0 +1,19 @@
+<!--
+  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.
+-->
+
+<span class="color" [style.background-color]="color"></span>
+{{label}}
diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/graph-legend-item/graph-legend-item.component.less b/ambari-logsearch/ambari-logsearch-web/src/app/components/graph-legend-item/graph-legend-item.component.less
new file mode 100644 (file)
index 0000000..dc20dca
--- /dev/null
@@ -0,0 +1,27 @@
+/**
+ * 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.
+ */
+
+:host {
+  padding-right: 1em;
+
+  .color {
+    border-radius: 100%;
+    display: inline-block;
+    height: .8em;
+    width: .8em;
+  }
+}
diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/graph-legend-item/graph-legend-item.component.spec.ts b/ambari-logsearch/ambari-logsearch-web/src/app/components/graph-legend-item/graph-legend-item.component.spec.ts
new file mode 100644 (file)
index 0000000..f8a4beb
--- /dev/null
@@ -0,0 +1,42 @@
+/**
+ * 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.
+ */
+
+import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+
+import {GraphLegendItemComponent} from './graph-legend-item.component';
+
+describe('GraphLegendItemComponent', () => {
+  let component: GraphLegendItemComponent;
+  let fixture: ComponentFixture<GraphLegendItemComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [GraphLegendItemComponent]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(GraphLegendItemComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create component', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/graph-legend-item/graph-legend-item.component.ts b/ambari-logsearch/ambari-logsearch-web/src/app/components/graph-legend-item/graph-legend-item.component.ts
new file mode 100644 (file)
index 0000000..127eb8d
--- /dev/null
@@ -0,0 +1,37 @@
+/**
+ * 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.
+ */
+
+import {Component, Input} from '@angular/core';
+
+@Component({
+  selector: 'graph-legend-item',
+  templateUrl: './graph-legend-item.component.html',
+  styleUrls: ['./graph-legend-item.component.less']
+})
+export class GraphLegendItemComponent {
+
+  /**
+   * Color of the corresponding graph item. Should be string in any CSS allowable format.
+   * @type {string}
+   */
+  @Input()
+  color: string;
+
+  @Input()
+  label: string;
+
+}
diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/graph-legend/graph-legend.component.html b/ambari-logsearch/ambari-logsearch-web/src/app/components/graph-legend/graph-legend.component.html
new file mode 100644 (file)
index 0000000..e756af6
--- /dev/null
@@ -0,0 +1,19 @@
+<!--
+  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.
+-->
+
+<graph-legend-item *ngFor="let item of items" label="{{item.label | translate}}"
+                   color="{{item.color}}"></graph-legend-item>
diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/graph-legend/graph-legend.component.spec.ts b/ambari-logsearch/ambari-logsearch-web/src/app/components/graph-legend/graph-legend.component.spec.ts
new file mode 100644 (file)
index 0000000..e297e14
--- /dev/null
@@ -0,0 +1,50 @@
+/**
+ * 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.
+ */
+
+import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
+import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {TranslationModules} from '@app/test-config.spec';
+import {GraphLegendItemComponent} from '@app/components/graph-legend-item/graph-legend-item.component';
+
+import {GraphLegendComponent} from './graph-legend.component';
+
+describe('GraphLegendComponent', () => {
+  let component: GraphLegendComponent;
+  let fixture: ComponentFixture<GraphLegendComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [
+        GraphLegendComponent,
+        GraphLegendItemComponent
+      ],
+      imports: TranslationModules,
+      schemas: [CUSTOM_ELEMENTS_SCHEMA]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(GraphLegendComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create component', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/graph-legend/graph-legend.component.ts b/ambari-logsearch/ambari-logsearch-web/src/app/components/graph-legend/graph-legend.component.ts
new file mode 100644 (file)
index 0000000..e273d4e
--- /dev/null
@@ -0,0 +1,32 @@
+/**
+ * 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.
+ */
+
+import {Component, Input} from '@angular/core';
+
+@Component({
+  selector: 'graph-legend',
+  templateUrl: './graph-legend.component.html'
+})
+export class GraphLegendComponent {
+
+  @Input()
+  items = [];
+
+  @Input()
+  labelClass: string = 'initial-color';
+
+}
diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/graph-tooltip/graph-tooltip.component.html b/ambari-logsearch/ambari-logsearch-web/src/app/components/graph-tooltip/graph-tooltip.component.html
new file mode 100644 (file)
index 0000000..1711ffc
--- /dev/null
@@ -0,0 +1,22 @@
+<!--
+  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.
+-->
+
+<div class="title">{{title}}</div>
+<div *ngFor="let item of data" class="data-item">
+  <graph-legend-item label="{{item.label | translate}}" color="{{item.color}}"></graph-legend-item>
+  <span class="item-value">{{item.value}}</span>
+</div>
diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/graph-tooltip/graph-tooltip.component.less b/ambari-logsearch/ambari-logsearch-web/src/app/components/graph-tooltip/graph-tooltip.component.less
new file mode 100644 (file)
index 0000000..ec750e1
--- /dev/null
@@ -0,0 +1,69 @@
+/**
+ * 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.
+ */
+
+@import '../mixins';
+
+:host {
+  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;
+    }
+
+    &::after {
+      .caret-mixin(6px, right, #fff);
+      right: -6px;
+      position: absolute;
+      top: calc(50% - 2px);
+    }
+  }
+
+  .title {
+    padding: 0 0 .1em 0;
+    text-align: center;
+  }
+
+  .data-item {
+    display: flex;
+    justify-content: space-between;
+
+    graph-legend-item {
+      flex-grow: 3;
+    }
+  }
+}
diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/graph-tooltip/graph-tooltip.component.spec.ts b/ambari-logsearch/ambari-logsearch-web/src/app/components/graph-tooltip/graph-tooltip.component.spec.ts
new file mode 100644 (file)
index 0000000..14fa60e
--- /dev/null
@@ -0,0 +1,50 @@
+/**
+ * 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.
+ */
+
+import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
+import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {TranslationModules} from '@app/test-config.spec';
+import {GraphLegendItemComponent} from '@app/components/graph-legend-item/graph-legend-item.component';
+
+import {GraphTooltipComponent} from './graph-tooltip.component';
+
+describe('GraphTooltipComponent', () => {
+  let component: GraphTooltipComponent;
+  let fixture: ComponentFixture<GraphTooltipComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [
+        GraphTooltipComponent,
+        GraphLegendItemComponent
+      ],
+      imports: TranslationModules,
+      schemas: [CUSTOM_ELEMENTS_SCHEMA]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(GraphTooltipComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create component', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/graph-tooltip/graph-tooltip.component.ts b/ambari-logsearch/ambari-logsearch-web/src/app/components/graph-tooltip/graph-tooltip.component.ts
new file mode 100644 (file)
index 0000000..9d26a2e
--- /dev/null
@@ -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.
+ */
+
+import {Component, Input} from '@angular/core';
+
+@Component({
+  selector: 'graph-tooltip',
+  templateUrl: './graph-tooltip.component.html',
+  styleUrls: ['./graph-tooltip.component.less']
+})
+export class GraphTooltipComponent {
+
+  @Input()
+  title: string | number = '';
+
+  @Input()
+  data = [];
+
+  @Input()
+  labelClass: string = 'initial-color';
+
+}
diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/horizontal-histogram/horizontal-histogram.component.html b/ambari-logsearch/ambari-logsearch-web/src/app/components/horizontal-histogram/horizontal-histogram.component.html
new file mode 100644 (file)
index 0000000..015013f
--- /dev/null
@@ -0,0 +1,22 @@
+<!--
+  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.
+-->
+
+<div #graphContainer></div>
+<graph-legend class="col-md-12" [items]="legendItems"></graph-legend>
+<graph-tooltip #tooltip [data]="tooltipInfo.data" [title]="tooltipInfo.title" [ngClass]="{'hide': !tooltipInfo.data}"
+               [style.top]="tooltipInfo.data ? tooltipPosition.top + 'px' : ''"
+               [style.left]="tooltipInfo.data ? tooltipPosition.left + 'px' : ''"></graph-tooltip>
diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/horizontal-histogram/horizontal-histogram.component.less b/ambari-logsearch/ambari-logsearch-web/src/app/components/horizontal-histogram/horizontal-histogram.component.less
new file mode 100644 (file)
index 0000000..395737a
--- /dev/null
@@ -0,0 +1,22 @@
+/**
+ * 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.
+ */
+
+@import '../variables';
+
+:host {
+  padding-top: @graph-padding;
+}
diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/horizontal-histogram/horizontal-histogram.component.spec.ts b/ambari-logsearch/ambari-logsearch-web/src/app/components/horizontal-histogram/horizontal-histogram.component.spec.ts
new file mode 100644 (file)
index 0000000..2c59916
--- /dev/null
@@ -0,0 +1,61 @@
+/**
+ * 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.
+ */
+
+import {Injector} from '@angular/core';
+import {async, ComponentFixture, TestBed, inject} from '@angular/core/testing';
+import {TranslationModules} from '@app/test-config.spec';
+import {ServiceInjector} from '@app/classes/service-injector';
+import {GraphLegendComponent} from '@app/components/graph-legend/graph-legend.component';
+import {GraphLegendItemComponent} from '@app/components/graph-legend-item/graph-legend-item.component';
+import {GraphTooltipComponent} from '@app/components/graph-tooltip/graph-tooltip.component';
+import {UtilsService} from '@app/services/utils.service';
+
+import {HorizontalHistogramComponent} from './horizontal-histogram.component';
+
+describe('HorizontalHistogramComponent', () => {
+  let component: HorizontalHistogramComponent;
+  let fixture: ComponentFixture<HorizontalHistogramComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [
+        HorizontalHistogramComponent,
+        GraphLegendComponent,
+        GraphLegendItemComponent,
+        GraphTooltipComponent
+      ],
+      imports: [
+        ...TranslationModules
+      ],
+      providers: [
+        UtilsService
+      ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(inject([Injector], (injector: Injector) => {
+    ServiceInjector.injector = injector;
+    fixture = TestBed.createComponent(HorizontalHistogramComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  }));
+
+  it('should create component', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/horizontal-histogram/horizontal-histogram.component.ts b/ambari-logsearch/ambari-logsearch-web/src/app/components/horizontal-histogram/horizontal-histogram.component.ts
new file mode 100644 (file)
index 0000000..9553e2e
--- /dev/null
@@ -0,0 +1,114 @@
+/**
+ * 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.
+ */
+
+import {Component, Input} from '@angular/core';
+import * as d3 from 'd3';
+import {GraphComponent} from '@app/classes/components/graph/graph.component';
+import {HomogeneousObject} from '@app/classes/object';
+
+@Component({
+  selector: 'horizontal-histogram',
+  templateUrl: './horizontal-histogram.component.html',
+  styleUrls: ['../../classes/components/graph/graph.component.less', './horizontal-histogram.component.less']
+})
+export class HorizontalHistogramComponent extends GraphComponent {
+
+  /**
+   * Thickness of horizontal bar o the graph
+   * @type {number}
+   */
+  @Input()
+  barSize: number = 5;
+
+  rowsCount: number;
+
+  readonly reverseYRange: boolean = true;
+
+  protected populate(): void {
+    const barSize = this.barSize,
+      data = this.data,
+      yValues = Object.keys(data),
+      keys = Object.keys(this.labels),
+      rowsCount = yValues.reduce((currentCount: number, currentKey: string): number => {
+        return currentCount + Object.keys(this.data[currentKey]).length;
+      }, 0),
+      formattedData = yValues.reduce((currentData, currentKey: string) => {
+        const currentValues = data[currentKey],
+          currentObjects = keys.map((key: string): HomogeneousObject<number> => {
+            return {
+              [key]: currentValues[key] || 0
+            };
+          });
+        return [...currentData, Object.assign({
+          tick: currentKey
+        }, ...currentObjects)];
+      }, []),
+      layers = d3.stack().keys(keys)(formattedData),
+      formattedLayers = d3.transpose<any>(layers);
+
+    this.rowsCount = rowsCount;
+
+    this.setXScaleDomain();
+    this.setYScaleDomain();
+
+    // drawing the axis
+    this.drawXAxis();
+    this.drawYAxis(rowsCount);
+
+    let i = 0;
+
+    // populate the data and drawing the bars
+    this.svg.selectAll().data(formattedLayers).enter().append('g').attr('class', 'value')
+      .selectAll().data(item => item).enter().append('rect')
+      .attr('x', item => this.xScale(0) + 1).attr('y', item => {
+        if (item [0] !== item[1]) {
+          return this.yScale(i++) - this.barSize / 2;
+        }
+      }).attr('height', item => item[0] === item[1] ? '0' : barSize.toString())
+      .attr('width', item => this.xScale(item[1]) - this.xScale(item[0]))
+      .style('fill', (item, index) => this.orderedColors[index])
+      .on('mouseover', this.handleRectMouseOver)
+      .on('mousemove', this.handleRectMouseMove)
+      .on('mouseout', this.handleRectMouseOut);
+  }
+
+  protected setXScaleDomain(): void {
+    const keys = Object.keys(this.data),
+      maxValues = keys.map((currentKey: string): number => this.utils.getMaxNumberInObject(this.data[currentKey]), 0),
+      maximum = Math.max(...maxValues);
+    this.xScale.domain([0, maximum]);
+  }
+
+  protected setYScaleDomain(): void {
+    this.yScale.domain([0, this.rowsCount]);
+  }
+
+  protected yAxisTickFormatter = (tick: any, index: number): string | undefined => {
+    const data = this.data,
+      keys = Object.keys(data);
+    let currentIndex = 0;
+    for (let i = 0; i < keys.length && i <= index; i++) {
+      const currentKey = keys[i];
+      if (currentIndex === index) {
+        return currentKey;
+      } else {
+        currentIndex += Object.keys(data[currentKey]).length;
+      }
+    }
+  };
+
+}
index d1b11e6..8e507ae 100644 (file)
     }}</div>
   </div>
   <collapsible-panel openTitle="logs.hideGraph" collapsedTitle="logs.showGraph">
-    <time-histogram [data]="histogramData" [customOptions]="histogramOptions" svgId="service-logs-histogram"
+    <time-histogram [data]="histogramData" [colors]="serviceLogsHistogramColors" svgId="service-logs-histogram"
                     (selectArea)="setCustomTimeRange($event[0], $event[1])"></time-histogram>
   </collapsible-panel>
   <ng-container [ngSwitch]="logsType">
     <service-logs-table *ngSwitchCase="'serviceLogs'" [totalCount]="totalCount" [logs]="serviceLogs | async"
                         [columns]="serviceLogsColumns | async" [filtersForm]="filtersForm"></service-logs-table>
-    <audit-logs-table *ngSwitchCase="'auditLogs'" [totalCount]="totalCount" [logs]="auditLogs | async"
-                      [columns]="auditLogsColumns | async" [filtersForm]="filtersForm"></audit-logs-table>
+    <audit-logs-entries *ngSwitchCase="'auditLogs'" [totalCount]="totalCount" [logs]="auditLogs | async"
+                        [columns]="auditLogsColumns | async" [filtersForm]="filtersForm"></audit-logs-entries>
   </ng-container>
   <log-context *ngIf="isServiceLogContextView" [id]="activeLog.id" [hostName]="activeLog.host_name"
                [componentName]="activeLog.component_name"></log-context>
index cf28a8b..6d50a17 100644 (file)
@@ -28,8 +28,8 @@ import {ServiceLog} from '@app/classes/models/service-log';
 import {Tab} from '@app/classes/models/tab';
 import {BarGraph} from '@app/classes/models/bar-graph';
 import {ActiveServiceLogEntry} from '@app/classes/active-service-log-entry';
-import {HistogramOptions} from '@app/classes/histogram-options';
 import {ListItem} from '@app/classes/list-item';
+import {HomogeneousObject} from '@app/classes/object';
 import {LogsType} from '@app/classes/string';
 import {FiltersPanelComponent} from "@app/components/filters-panel/filters-panel.component";
 
@@ -74,11 +74,11 @@ export class LogsContainerComponent {
     return this.logsContainer.totalCount;
   }
 
-  histogramData: {[key: string]: number};
+  histogramData: HomogeneousObject<HomogeneousObject<number>>;
 
-  readonly histogramOptions: HistogramOptions = {
-    keysWithColors: this.logsContainer.colors
-  };
+  get serviceLogsHistogramColors(): HomogeneousObject<string> {
+    return this.logsContainer.colors;
+  }
 
   get autoRefreshRemainingSeconds(): number {
     return this.logsContainer.autoRefreshRemainingSeconds;
index 3836e7a..67d9423 100644 (file)
  * limitations under the License.
  */
 
-import {NO_ERRORS_SCHEMA} from '@angular/core';
-import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {NO_ERRORS_SCHEMA, Injector} from '@angular/core';
+import {async, ComponentFixture, TestBed, inject} from '@angular/core/testing';
 import {TranslationModules} from '@app/test-config.spec';
+import {ServiceInjector} from '@app/classes/service-injector';
 import {StoreModule} from '@ngrx/store';
 import {AppSettingsService, appSettings} from '@app/services/storage/app-settings.service';
 import {AppStateService, appState} from '@app/services/storage/app-state.service';
@@ -97,11 +98,12 @@ describe('MenuButtonComponent', () => {
     .compileComponents();
   }));
 
-  beforeEach(() => {
+  beforeEach(inject([Injector], (injector: Injector) => {
+    ServiceInjector.injector = injector;
     fixture = TestBed.createComponent(MenuButtonComponent);
     component = fixture.componentInstance;
     fixture.detectChanges();
-  });
+  }));
 
   it('should create component', () => {
     expect(component).toBeTruthy();
index ca89935..12da4ac 100644 (file)
@@ -18,6 +18,7 @@
 
 import {Component, Input, ViewChild, ElementRef} from '@angular/core';
 import {ListItem} from '@app/classes/list-item';
+import {ServiceInjector} from '@app/classes/service-injector';
 import {ComponentActionsService} from '@app/services/component-actions.service';
 
 @Component({
@@ -27,7 +28,8 @@ import {ComponentActionsService} from '@app/services/component-actions.service';
 })
 export class MenuButtonComponent {
 
-  constructor(protected actions: ComponentActionsService) {
+  constructor() {
+    this.actions = ServiceInjector.injector.get(ComponentActionsService);
   }
 
   @ViewChild('dropdown')
@@ -82,6 +84,8 @@ export class MenuButtonComponent {
   @Input()
   maxLongClickDelay: number = 0;
 
+  private actions: ComponentActionsService;
+
   /**
    * This is a private property to indicate the mousedown timestamp, so that we can check it when teh click event
    * has been triggered.
index b2136f4..9dd8e26 100644 (file)
@@ -21,6 +21,7 @@ import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
 import {Subject} from 'rxjs/Subject';
 import {SearchBoxParameter, SearchBoxParameterProcessed, SearchBoxParameterTriggered} from '@app/classes/filtering';
 import {ListItem} from '@app/classes/list-item';
+import {HomogeneousObject} from '@app/classes/object';
 import {UtilsService} from '@app/services/utils.service';
 
 @Component({
@@ -88,7 +89,7 @@ export class SearchBoxComponent implements OnInit, OnDestroy, ControlValueAccess
   items: ListItem[] = [];
 
   @Input()
-  itemsOptions: {[key: string]: ListItem[]} = {};
+  itemsOptions: HomogeneousObject<ListItem[]> = {};
 
   /**
    * Name of parameter to be used if there are no matching values
index 9f38371..84a59b9 100644 (file)
@@ -18,7 +18,7 @@
 
 import {Component, AfterViewInit, ViewChild, ElementRef} from '@angular/core';
 import {ListItem} from '@app/classes/list-item';
-import {LogsTableComponent} from '@app/classes/components/logs-table-component';
+import {LogsTableComponent} from '@app/classes/components/logs-table/logs-table-component';
 import {LogsContainerService} from '@app/services/logs-container.service';
 import {UtilsService} from '@app/services/utils.service';
 
index 2df5090..7eef0e1 100644 (file)
@@ -47,7 +47,6 @@ describe('TabsComponent', () => {
     let activeTab;
     const tab = {
       id: 'tab0',
-      type: '',
       isActive: true,
       label: '',
       appState: null
@@ -64,21 +63,18 @@ describe('TabsComponent', () => {
     const items = [
         {
           id: 'serviceLogs',
-          type: '',
           isActive: false,
           label: '',
           appState: null
         },
         {
           id: 'auditLogs',
-          type: '',
           isActive: false,
           label: '',
           appState: null
         },
         {
           id: 'newTab',
-          type: '',
           isActive: false,
           label: '',
           appState: null
index ef941e6..e202c2d 100644 (file)
@@ -35,6 +35,7 @@ export class TabsComponent {
   tabClosed: EventEmitter<Tab[]> = new EventEmitter();
 
   switchTab(tab: Tab): void {
+    this.items.forEach((item: Tab) => item.isActive = item.id === tab.id);
     this.tabSwitched.emit(tab);
   }
 
index 1193b2e..720f55e 100644 (file)
 <header>
   <div class="container-fluid">
     <div class="row">
-      <div *ngIf="chartTimeGap" class="time-gap col-lg-2 col-md-offset-5">
+      <div *ngIf="isTimeGraph && chartTimeGap" class="time-gap col-md-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>
+      <graph-legend [ngClass]="{'col-md-5 text-right': true, 'md-offset-7': !chartTimeGap}"
+                    [items]="legendItems"></graph-legend>
     </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>
+<div #graphContainer></div>
+<footer *ngIf="isTimeGraph && (firstDateTick || lastDateTick)">
+  <div *ngIf="firstDateTick">{{firstDateTick | amTz: timeZone | amDateFormat: historyStartEndTimeFormat}}</div>
+  <div *ngIf="lastDateTick">{{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>
-
+<graph-tooltip #tooltip [data]="tooltipInfo.data" [ngClass]="{'hide': !tooltipInfo.data}"
+               [style.top]="tooltipInfo.data ? tooltipPosition.top + 'px' : ''"
+               [style.left]="tooltipInfo.data ? tooltipPosition.left + 'px' : ''"
+               [title]="tooltipInfo.title | amTz: timeZone | amDateFormat: tickTimeFormat"></graph-tooltip>
index 1d3766d..364517c 100644 (file)
 
 :host {
   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;
-      }
     }
   }
 
     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');
-    }
+    padding: @graph-padding;
   }
 
   .time-gap {
-    color: #666;
+    color: @base-font-color;
     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;
-    }
+    .default-flex;
+    font-size: 1.2rem;
+    color: @base-font-color;
+    padding: 0 1em .5em;
   }
 
   /deep/ rect.drag-area {
index ee14780..09cd5d8 100644 (file)
  * limitations under the License.
  */
 
-import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {Injector} from '@angular/core';
+import {async, ComponentFixture, TestBed, inject} 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 {ServiceInjector} from '@app/classes/service-injector';
 import {TimeZoneAbbrPipe} from '@app/pipes/timezone-abbr.pipe';
+import {GraphLegendComponent} from '@app/components/graph-legend/graph-legend.component';
+import {GraphLegendItemComponent} from '@app/components/graph-legend-item/graph-legend-item.component';
+import {GraphTooltipComponent} from '@app/components/graph-tooltip/graph-tooltip.component';
 
 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";
+import {HttpClientService} from '@app/services/http-client.service';
+import {UtilsService} from '@app/services/utils.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';
+import {HomogeneousObject} from '@app/classes/object';
 
 describe('TimeHistogramComponent', () => {
   let component: TimeHistogramComponent;
   let fixture: ComponentFixture<TimeHistogramComponent>;
   let histogramData: any;
-  let customOptions: any;
+  let colors: HomogeneousObject<string>;
 
   beforeEach(async(() => {
     const httpClient = {
@@ -54,29 +61,42 @@ describe('TimeHistogramComponent', () => {
       }
     };
     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'
+      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
       }
     };
+    colors = {
+      FATAL: '#830A0A',
+      ERROR: '#E81D1D',
+      WARN: '#FF8916',
+      INFO: '#2577B5',
+      DEBUG: '#65E8FF',
+      TRACE: '#888',
+      UNKNOWN: '#BDBDBD'
+    };
     TestBed.configureTestingModule({
-      declarations: [TimeHistogramComponent, TimeZoneAbbrPipe],
+      declarations: [
+        TimeHistogramComponent,
+        GraphLegendComponent,
+        GraphLegendItemComponent,
+        GraphTooltipComponent,
+        TimeZoneAbbrPipe
+      ],
       imports: [
         StoreModule.provideStore({
           appSettings
@@ -93,6 +113,7 @@ describe('TimeHistogramComponent', () => {
           provide: HttpClientService,
           useValue: httpClient
         },
+        UtilsService,
         AppStateService,
         AuditLogsService,
         AuditLogsFieldsService,
@@ -109,14 +130,15 @@ describe('TimeHistogramComponent', () => {
       .compileComponents();
   }));
 
-  beforeEach(() => {
-      fixture = TestBed.createComponent(TimeHistogramComponent);
-      component = fixture.componentInstance;
-      component.customOptions = customOptions;
-      component.svgId = "HistogramSvg";
-      component.data = histogramData;
-      fixture.detectChanges();
-    });
+  beforeEach(inject([Injector], (injector: Injector) => {
+    ServiceInjector.injector = injector;
+    fixture = TestBed.createComponent(TimeHistogramComponent);
+    component = fixture.componentInstance;
+    component.colors = colors;
+    component.svgId = 'HistogramSvg';
+    component.data = histogramData;
+    fixture.detectChanges();
+  }));
 
   it('should create component', () => {
     expect(component).toBeTruthy();
index fb3092f..8544677 100644 (file)
  * limitations under the License.
  */
 
-import {Component, OnInit, AfterViewInit, OnChanges, Input, Output, ViewChild, ElementRef, EventEmitter} from '@angular/core';
-import {ContainerElement, Selection} from 'd3';
+import {Component, OnInit, Input, Output, EventEmitter} from '@angular/core';
 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';
+import {GraphComponent} from '@app/classes/components/graph/graph.component';
+import {GraphScaleItem} from '@app/classes/graph';
 
 @Component({
   selector: 'time-histogram',
   templateUrl: './time-histogram.component.html',
-  styleUrls: ['./time-histogram.component.less']
+  styleUrls: ['../../classes/components/graph/graph.component.less', './time-histogram.component.less']
 })
-export class TimeHistogramComponent implements OnInit, AfterViewInit, OnChanges {
+export class TimeHistogramComponent extends GraphComponent implements OnInit {
 
-  constructor(private appSettings: AppSettingsService) {}
+  constructor(private appSettings: AppSettingsService) {
+    super();
+  }
 
   ngOnInit() {
     this.appSettings.getParameter('timeZone').subscribe((value: string): void => {
       this.timeZone = value;
-      this.createHistogram();
+      this.createGraph();
     });
-    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);
-  }
-
-  ngOnChanges() {
-    this.createHistogram();
   }
 
-  @ViewChild('container')
-  element: ElementRef;
-
-  @ViewChild('tooltipEl')
-  tooltipEl: ElementRef;
-
-  @Input()
-  svgId: string;
-
   @Input()
-  customOptions: HistogramOptions;
-
-  @Input()
-  data: {[key: string]: number};
+  columnWidth = {
+    second: 40,
+    minute: 30,
+    hour: 25,
+    day: 20,
+    base: 20
+  };
 
   @Output()
   selectArea: EventEmitter<number[]> = new EventEmitter();
 
-  private readonly defaultOptions: HistogramStyleOptions = {
-    margin: {
-      top: 5,
-      right: 50,
-      bottom: 30,
-      left: 50
-    },
-    height: 150,
-    tickPadding: 10,
-    columnWidth: {
-      second: 40,
-      minute: 30,
-      hour: 25,
-      day: 20,
-      base: 20
-    }
-  };
-
-  private options: HistogramOptions;
+  readonly isTimeGraph: boolean = true;
 
   private timeZone: string;
 
-  private host;
-
-  private svg;
-
-  private width: number;
-
-  private xScale;
-
-  private yScale;
-
-  private colorScale;
-
-  private xAxis;
-
-  private yAxis;
-
-  private htmlElement: HTMLElement;
-  private tooltipElement: HTMLElement;
-
-  private dragArea: Selection<SVGGraphicsElement, undefined, SVGGraphicsElement, undefined>;
+  private dragArea: d3.Selection<SVGGraphicsElement, undefined, SVGGraphicsElement, undefined>;
 
   private dragStartX: number;
 
@@ -118,26 +66,12 @@ export class TimeHistogramComponent implements OnInit, AfterViewInit, OnChanges
   private maxDragX: number;
 
   private readonly tickTimeFormat: string = 'MM/DD HH:mm';
-  private readonly historyStartEndTimeFormat = 'dddd, MMMM DD, YYYY';
+
+  private readonly historyStartEndTimeFormat: string = '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.
@@ -146,11 +80,11 @@ export class TimeHistogramComponent implements OnInit, AfterViewInit, OnChanges
   /**
    * 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>;
+  private leftDragArea: d3.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>;
+  private rightDragArea: d3.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.
    */
@@ -166,134 +100,12 @@ export class TimeHistogramComponent implements OnInit, AfterViewInit, OnChanges
     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();
-      this.buildSVG();
-      this.populate();
-    }
-  }
-
-  private setup(): void {
-    const margin = this.options.margin,
-      keysWithColors = this.options.keysWithColors,
-      keys = Object.keys(keysWithColors),
-      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]);
-    this.colorScale = d3.scaleOrdinal(colors);
-  }
-
-  private buildSVG(): void {
-    const margin = this.options.margin;
-    this.host.html('');
-    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})`);
-  }
-
-  /**
-   * 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.tickTimeFormat))
-      .tickPadding(this.options.tickPadding);
-    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)) {
-        return tick.toFixed(0);
-      } else {
-        return;
-      }
-    }).tickPadding(this.options.tickPadding);
-    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();
+  protected xAxisTickFormatter = (tick: Date): string => {
+    return moment(tick).tz(this.timeZone).format(this.tickTimeFormat);
   };
 
-  /**
-   * 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};
-    }
+  protected yAxisTickFormatter = (tick: number): string | undefined => {
+    return Number.isInteger(tick) ? tick.toFixed(0) : undefined;
   };
 
   /**
@@ -361,33 +173,33 @@ export class TimeHistogramComponent implements OnInit, AfterViewInit, OnChanges
   /**
    * 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
+   * An example data: [{tick: 1233455677, WARN: 12, ERROR: 123}]
+   * @param {GraphScaleItem[]} data
    */
-  private setYScaleDomain(data: Array<{timeStamp: number, [key: string]: number}>): void {
-    const keys = Object.keys(this.options.keysWithColors);
+  protected setYScaleDomain(data: GraphScaleItem[]): void {
+    const keys = Object.keys(this.labels);
     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
+   * An example data: [{tick: 1233455677, WARN: 12, ERROR: 123}]
+   * @param {GraphScaleItem[]} data
    */
-  private setXScaleDomain(data: Array<{timeStamp: number, [key: string]: any}>): void {
-    this.xScale.domain(d3.extent(data, item => item.timeStamp)).nice();
+  protected setXScaleDomain(data: GraphScaleItem[]): void {
+    this.xScale.domain(d3.extent(data, item => item.tick)).nice();
   }
 
-  private populate(): void {
-    const keys = Object.keys(this.options.keysWithColors);
+  protected populate(): void {
+    const keys = Object.keys(this.colors);
     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)
+    const formattedData = timeStamps.map((timeStamp: string): {tick: number, [key: string]: number} => Object.assign({
+        tick: Number(timeStamp)
       }, data[timeStamp]));
-    const layers = (d3.stack().keys(keys)(formattedData));
+    const layers = d3.stack().keys(keys)(formattedData);
 
     // after we have the data we set the domain values both scales
     this.setXScaleDomain(formattedData);
@@ -396,39 +208,32 @@ export class TimeHistogramComponent implements OnInit, AfterViewInit, OnChanges
     // Setting the timegap label above the chart
     this.setChartTimeGapByXScale();
 
-    let unitD3TimeProp = this.chartTimeGap.unit.charAt(0).toUpperCase() + this.chartTimeGap.unit.slice(1);
+    const 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;
+    const columnWidth = this.columnWidth[this.chartTimeGap.unit] || this.columnWidth.base;
 
     // drawing the axis
     this.drawXAxis();
     this.drawYAxis();
 
     // 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)
+    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.tick) - 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))
+        .style('fill', (item, index) => this.orderedColors[index])
         .on('mouseover', this.handleRectMouseOver)
         .on('mousemove', this.handleRectMouseMove)
         .on('mouseout', this.handleRectMouseOut);
     this.setDragBehavior();
   }
 
-  private getTimeRangeByXRanges(startX: number, endX:number): [number, number] {
+  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;
@@ -442,7 +247,7 @@ export class TimeHistogramComponent implements OnInit, AfterViewInit, OnChanges
    * @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;
+    const height: number = this.height + this.margin.top + this.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);
@@ -456,9 +261,8 @@ export class TimeHistogramComponent implements OnInit, AfterViewInit, OnChanges
   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;
+    const rightAreaWidth: number = Math.max(0, this.width - right);
+    const leftAreaWidth: number = Math.max(0, left);
     this.leftDragArea.attr('x', 0).attr('width', leftAreaWidth);
     this.rightDragArea.attr('x', right).attr('width', rightAreaWidth);
   }
@@ -472,20 +276,20 @@ export class TimeHistogramComponent implements OnInit, AfterViewInit, OnChanges
   }
 
   private setDragBehavior(): void {
-    this.minDragX = this.options.margin.left;
-    this.maxDragX = this.htmlElement.clientWidth;
+    this.minDragX = this.margin.left;
+    this.maxDragX = this.graphContainer.clientWidth;
     d3.selectAll(`svg#${this.svgId}`).call(d3.drag()
-      .on('start', (datum: undefined, index: number, containers: ContainerElement[]): void => {
+      .on('start', (datum: undefined, index: number, containers: d3.ContainerElement[]): void => {
         if (this.dragArea) {
           this.dragArea.remove();
         }
-        this.dragStartX = Math.max(0, this.getDragX(containers[0]) - this.options.margin.left);
+        this.dragStartX = Math.max(0, this.getDragX(containers[0]) - this.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).attr('class', 'drag-area');
+          .attr('height', this.height).attr('class', 'drag-area');
       })
-      .on('drag', (datum: undefined, index: number, containers: ContainerElement[]): void => {
+      .on('drag', (datum: undefined, index: number, containers: d3.ContainerElement[]): void => {
         const mousePos = this.getDragX(containers[0]);
-        const currentX = Math.max(mousePos, this.minDragX) - this.options.margin.left;
+        const currentX = Math.max(mousePos, this.minDragX) - this.margin.left;
         const startX = Math.min(currentX, this.dragStartX);
         const currentWidth = Math.abs(currentX - this.dragStartX);
         this.dragArea.attr('x', startX).attr('width', currentWidth);
@@ -507,7 +311,7 @@ export class TimeHistogramComponent implements OnInit, AfterViewInit, OnChanges
     }));
   }
 
-  private getDragX(element: ContainerElement): number {
+  private getDragX(element: d3.ContainerElement): number {
     return d3.mouse(element)[0];
   }
 
index 6df7ab3..8d739ec 100644 (file)
@@ -20,6 +20,7 @@ import {Component} from '@angular/core';
 import {FormGroup} from '@angular/forms';
 import {FilterCondition, TimeUnitListItem} from '@app/classes/filtering';
 import {ListItem} from '@app/classes/list-item';
+import {HomogeneousObject} from '@app/classes/object';
 import {LogsContainerService} from '@app/services/logs-container.service';
 
 @Component({
@@ -36,7 +37,7 @@ export class TopMenuComponent {
     return this.logsContainer.filtersForm;
   };
 
-  get filters(): {[key: string]: FilterCondition} {
+  get filters(): HomogeneousObject<FilterCondition> {
     return this.logsContainer.filters;
   };
 
index 7578867..aa86f4f 100644 (file)
@@ -18,6 +18,8 @@
 
 import * as moment from 'moment';
 
+const currentTime = moment();
+
 export const mockData = {
   login: {},
   logout: {},
@@ -153,34 +155,82 @@ export const mockData = {
           },
           components: {},
           resources: {
-            graphData: [
-              {
-                dataCount: [
-                  {
-                    name: 'n16',
-                    value: 800
-                  },
-                  {
-                    name: 'n17',
-                    value: 400
-                  }
-                ],
-                name: 'graph8'
-              },
-              {
-                dataCount: [
-                  {
-                    name: 'n18',
-                    value: 600
-                  },
-                  {
-                    name: 'n19',
-                    value: 300
-                  }
-                ],
-                name: 'graph9'
-              }
-            ]
+            6: {
+              graphData: [
+                {
+                  dataCount: [
+                    {
+                      name: 'hdfs',
+                      value: 800
+                    },
+                    {
+                      name: 'zookeeper',
+                      value: 400
+                    },
+                    {
+                      name: 'ambari_metrics',
+                      value: 200
+                    }
+                  ],
+                  name: 'admin'
+                },
+                {
+                  dataCount: [
+                    {
+                      name: 'ambari_agent',
+                      value: 400
+                    },
+                    {
+                      name: 'hdfs',
+                      value: 600
+                    },
+                    {
+                      name: 'ambari_metrics',
+                      value: 300
+                    }
+                  ],
+                  name: 'user'
+                }
+              ]
+            },
+            10: {
+              graphData: [
+                {
+                  dataCount: [
+                    {
+                      name: 'ambari',
+                      value: 800
+                    },
+                    {
+                      name: 'hdfs',
+                      value: 400
+                    },
+                    {
+                      name: 'hbase',
+                      value: 200
+                    },
+                  ],
+                  name: '/user'
+                },
+                {
+                  dataCount: [
+                    {
+                      name: 'hdfs',
+                      value: 400
+                    },
+                    {
+                      name: 'hbase',
+                      value: 600
+                    },
+                    {
+                      name: 'kafka',
+                      value: 300
+                    }
+                  ],
+                  name: '/root'
+                }
+              ]
+            }
           },
           schema: {
             fields: {
@@ -299,7 +349,7 @@ export const mockData = {
               path: '/var/log/ambari-metrics-collector/ambari-metrics-collector.log',
               host: 'h0',
               level: 'WARN',
-              logtime: moment().valueOf(),
+              logtime: currentTime.valueOf(),
               ip: '192.168.0.1',
               logfile_line_number: 8,
               type: 'ams_collector',
@@ -316,14 +366,14 @@ export const mockData = {
               event_md5: '1908755391',
               event_dur_ms: 200,
               _ttl_: '+5DAYS',
-              _expire_at_: moment().add(5, 'd').valueOf(),
+              _expire_at_: currentTime.clone().add(5, 'd').valueOf(),
               _router_field_: 20
             },
             {
               path: '/var/log/ambari-metrics-collector/ambari-metrics-collector.log',
               host: 'h1',
               level: 'ERROR',
-              logtime: moment().subtract(2, 'd').valueOf(),
+              logtime: currentTime.clone().subtract(2, 'd').valueOf(),
               ip: '192.168.0.2',
               type: 'ams_collector',
               _version_: 14,
@@ -340,14 +390,14 @@ export const mockData = {
               event_md5: '1029384756',
               event_dur_ms: 700,
               _ttl_: '+5DAYS',
-              _expire_at_: moment().add(3, 'd').valueOf(),
+              _expire_at_: currentTime.clone().add(3, 'd').valueOf(),
               _router_field_: 5
             },
             {
               path: '/var/log/ambari-metrics-collector/ambari-metrics-collector.log',
               host: 'h1',
               level: 'FATAL',
-              logtime: moment().subtract(10, 'd').valueOf(),
+              logtime: currentTime.clone().subtract(10, 'd').valueOf(),
               ip: '192.168.0.3',
               type: 'ambari_agent',
               _version_: 14,
@@ -364,14 +414,14 @@ export const mockData = {
               event_md5: '67589403',
               event_dur_ms: 100,
               _ttl_: '+5DAYS',
-              _expire_at_: moment().subtract(5, 'd').valueOf(),
+              _expire_at_: currentTime.clone().subtract(5, 'd').valueOf(),
               _router_field_: 45
             },
             {
               path: '/var/log/ambari-metrics-collector/zookeeper-server.log',
               host: 'h1',
               level: 'INFO',
-              logtime: moment().subtract(25, 'h').valueOf(),
+              logtime: currentTime.clone().subtract(25, 'h').valueOf(),
               ip: '192.168.0.4',
               type: 'zookeeper_server',
               _version_: 14,
@@ -388,14 +438,14 @@ export const mockData = {
               event_md5: '67589403',
               event_dur_ms: 1000,
               _ttl_: '+5DAYS',
-              _expire_at_: moment().subtract(25, 'h').add(5, 'd').valueOf(),
+              _expire_at_: currentTime.clone().subtract(25, 'h').add(5, 'd').valueOf(),
               _router_field_: 55
             },
             {
               path: '/var/log/ambari-metrics-collector/zookeeper-server.log',
               host: 'h1',
               level: 'DEBUG',
-              logtime: moment().subtract(25, 'd').valueOf(),
+              logtime: currentTime.clone().subtract(25, 'd').valueOf(),
               ip: '192.168.0.4',
               type: 'zookeeper_server',
               _version_: 14,
@@ -412,14 +462,14 @@ export const mockData = {
               event_md5: '67589403',
               event_dur_ms: 1000,
               _ttl_: '+5DAYS',
-              _expire_at_: moment().subtract(20, 'd').valueOf(),
+              _expire_at_: currentTime.clone().subtract(20, 'd').valueOf(),
               _router_field_: 55
             },
             {
               path: '/var/log/ambari-metrics-collector/zookeeper-client.log',
               host: 'h1',
               level: 'TRACE',
-              logtime: moment().subtract(2, 'h').valueOf(),
+              logtime: currentTime.clone().subtract(2, 'h').valueOf(),
               ip: '192.168.0.4',
               type: 'zookeeper_client',
               _version_: 14,
@@ -436,14 +486,14 @@ export const mockData = {
               event_md5: '67589403',
               event_dur_ms: 1000,
               _ttl_: '+5DAYS',
-              _expire_at_: moment().subtract(2, 'h').add(5, 'd').valueOf(),
+              _expire_at_: currentTime.clone().subtract(2, 'h').add(5, 'd').valueOf(),
               _router_field_: 55
             },
             {
               path: '/var/log/ambari-metrics-collector/zookeeper-client.log',
               host: 'h1',
               level: 'UNKNOWN',
-              logtime: moment().subtract(31, 'd').valueOf(),
+              logtime: currentTime.clone().subtract(31, 'd').valueOf(),
               ip: '192.168.0.4',
               type: 'zookeeper_client',
               _version_: 14,
@@ -460,7 +510,7 @@ export const mockData = {
               event_md5: '67589403',
               event_dur_ms: 1000,
               _ttl_: '+5DAYS',
-              _expire_at_: moment().subtract(26, 'd').valueOf(),
+              _expire_at_: currentTime.clone().subtract(26, 'd').valueOf(),
               _router_field_: 55
             }
           ],
@@ -637,11 +687,11 @@ export const mockData = {
               {
                 dataCount: [
                   {
-                    name: moment().toISOString(),
+                    name: currentTime.toISOString(),
                     value: '1000'
                   },
                   {
-                    name: moment().subtract(1, 'h').toISOString(),
+                    name: currentTime.clone().subtract(1, 'h').toISOString(),
                     value: '2000'
                   }
                 ],
@@ -650,11 +700,11 @@ export const mockData = {
               {
                 dataCount: [
                   {
-                    name: moment().toISOString(),
+                    name: currentTime.toISOString(),
                     value: '700'
                   },
                   {
-                    name: moment().subtract(1, 'h').toISOString(),
+                    name: currentTime.clone().subtract(1, 'h').toISOString(),
                     value: '900'
                   }
                 ],
index 51b0c0b..36c4d8d 100644 (file)
@@ -83,7 +83,6 @@ export class ComponentActionsService {
   openLog(log: ServiceLog): void {
     const tab = {
       id: log.id,
-      type: 'serviceLogs',
       isCloseable: true,
       label: `${log.host} >> ${log.type}`,
       appState: {
index 9b61bf6..6f98bf5 100644 (file)
 import {Injectable} from '@angular/core';
 import {Observable} from 'rxjs/Observable';
 import 'rxjs/add/operator/first';
-import {Http, XHRBackend, Request, RequestOptions, RequestOptionsArgs, Response, Headers, URLSearchParams} from '@angular/http';
-import {AuditLogsQueryParams} from '@app/classes/queries/audit-logs-query-params';
+import {
+  Http, XHRBackend, Request, RequestOptions, RequestOptionsArgs, Response, Headers, URLSearchParams
+} from '@angular/http';
+import {HomogeneousObject} from '@app/classes/object';
+import {AuditLogsListQueryParams} from '@app/classes/queries/audit-logs-query-params';
+import {AuditLogsTopResourcesQueryParams} from '@app/classes/queries/audit-logs-top-resources-query-params';
 import {ServiceLogsQueryParams} from '@app/classes/queries/service-logs-query-params';
 import {ServiceLogsHistogramQueryParams} from '@app/classes/queries/service-logs-histogram-query-params';
 import {ServiceLogsTruncatedQueryParams} from '@app/classes/queries/service-logs-truncated-query-params';
@@ -41,7 +45,7 @@ export class HttpClientService extends Http {
     },
     auditLogs: {
       url: 'audit/logs',
-      params: opts => new AuditLogsQueryParams(opts)
+      params: opts => new AuditLogsListQueryParams(opts)
     },
     auditLogsFields: {
       url: 'audit/logs/schema/fields'
@@ -69,14 +73,31 @@ export class HttpClientService extends Http {
     },
     hosts: {
       url: 'service/logs/tree'
+    },
+    topAuditLogsResources: {
+      url: variables => `audit/logs/resources/${variables.number}`,
+      params: opts => new AuditLogsTopResourcesQueryParams(opts)
     }
   };
 
   private readonly unauthorizedStatuses = [401, 403, 419];
 
-  private generateUrlString(url: string): string {
+  private generateUrlString(url: string, urlVariables?: HomogeneousObject<string>): string {
     const preset = this.endPoints[url];
-    return preset ? `${this.apiPrefix}${preset.url}` : url;
+    let generatedUrl: string;
+    if (preset) {
+      const urlExpression = preset.url;
+      let path: string;
+      if (typeof urlExpression === 'function') {
+        path = preset.url(urlVariables);
+      } else if (typeof urlExpression === 'string') {
+        path = preset.url;
+      }
+      generatedUrl = `${this.apiPrefix}${path}`;
+    } else {
+      generatedUrl = url;
+    }
+    return generatedUrl;
   }
 
   private generateUrl(request: string | Request): string | Request {
@@ -89,7 +110,7 @@ export class HttpClientService extends Http {
     }
   }
 
-  private generateOptions(url: string, params: {[key: string]: string}): RequestOptionsArgs {
+  private generateOptions(url: string, params: HomogeneousObject<string>): RequestOptionsArgs {
     const preset = this.endPoints[url],
       rawParams = preset && preset.params ? preset.params(params) : params;
     if (rawParams) {
@@ -122,11 +143,11 @@ export class HttpClientService extends Http {
     return req;
   }
 
-  get(url, params?: {[key: string]: string}): Observable<Response> {
-    return super.get(this.generateUrlString(url), this.generateOptions(url, params));
+  get(url, params?: HomogeneousObject<string>, urlVariables?: HomogeneousObject<string>): Observable<Response> {
+    return super.get(this.generateUrlString(url, urlVariables), this.generateOptions(url, params));
   }
 
-  postFormData(url: string, params: {[key: string]: string}, options?: RequestOptionsArgs): Observable<Response> {
+  postFormData(url: string, params: HomogeneousObject<string>, options?: RequestOptionsArgs): Observable<Response> {
     const encodedParams = this.generateOptions(url, params).params;
     let body;
     if (encodedParams && encodedParams instanceof URLSearchParams) {
index 870058b..acdc0c2 100644 (file)
@@ -23,7 +23,9 @@ import {AuditLogsService, auditLogs} from '@app/services/storage/audit-logs.serv
 import {ServiceLogsService, serviceLogs} from '@app/services/storage/service-logs.service';
 import {AuditLogsFieldsService, auditLogsFields} from '@app/services/storage/audit-logs-fields.service';
 import {ServiceLogsFieldsService, serviceLogsFields} from '@app/services/storage/service-logs-fields.service';
-import {ServiceLogsHistogramDataService, serviceLogsHistogramData} from '@app/services/storage/service-logs-histogram-data.service';
+import {
+  ServiceLogsHistogramDataService, serviceLogsHistogramData
+} from '@app/services/storage/service-logs-histogram-data.service';
 import {AppSettingsService, appSettings} from '@app/services/storage/app-settings.service';
 import {AppStateService, appState} from '@app/services/storage/app-state.service';
 import {ClustersService, clusters} from '@app/services/storage/clusters.service';
index d719893..78bcdb1 100644 (file)
@@ -27,7 +27,6 @@ import 'rxjs/add/operator/first';
 import 'rxjs/add/operator/map';
 import 'rxjs/add/operator/takeUntil';
 import * as moment from 'moment-timezone';
-import {TranslateService} from '@ngx-translate/core';
 import {HttpClientService} from '@app/services/http-client.service';
 import {AuditLogsService} from '@app/services/storage/audit-logs.service';
 import {AuditLogsFieldsService} from '@app/services/storage/audit-logs-fields.service';
@@ -46,6 +45,7 @@ import {
   FilterCondition, TimeUnitListItem, SortingListItem, SearchBoxParameter, SearchBoxParameterTriggered
 } from '@app/classes/filtering';
 import {ListItem} from '@app/classes/list-item';
+import {HomogeneousObject} from '@app/classes/object';
 import {LogsType, ScrollType, SortingType} from '@app/classes/string';
 import {Tab} from '@app/classes/models/tab';
 import {LogField} from '@app/classes/models/log-field';
@@ -61,15 +61,15 @@ import {CommonEntry} from '@app/classes/models/common-entry';
 export class LogsContainerService {
 
   constructor(
-    private translate: TranslateService, private httpClient: HttpClientService,
-    private auditLogsStorage: AuditLogsService, private auditLogsFieldsStorage: AuditLogsFieldsService,
-    private serviceLogsStorage: ServiceLogsService, private serviceLogsFieldsStorage: ServiceLogsFieldsService,
-    private serviceLogsHistogramStorage: ServiceLogsHistogramDataService,
-    private serviceLogsTruncatedStorage: ServiceLogsTruncatedService, private appState: AppStateService,
-    private appSettings: AppSettingsService, private tabsStorage: TabsService, private clustersStorage: ClustersService,
-    private componentsStorage: ComponentsService, private hostsStorage: HostsService
+    private httpClient: HttpClientService, private appState: AppStateService,
+    private appSettings: AppSettingsService, private auditLogsStorage: AuditLogsService,
+    private auditLogsFieldsStorage: AuditLogsFieldsService, private serviceLogsStorage: ServiceLogsService,
+    private serviceLogsFieldsStorage: ServiceLogsFieldsService, private tabsStorage: TabsService,
+    private serviceLogsHistogramStorage: ServiceLogsHistogramDataService, private clustersStorage: ClustersService,
+    private componentsStorage: ComponentsService, private hostsStorage: HostsService,
+    private serviceLogsTruncatedStorage: ServiceLogsTruncatedService
   ) {
-    const formItems = Object.keys(this.filters).reduce((currentObject: any, key: string): {[key: string]: FormControl} => {
+    const formItems = Object.keys(this.filters).reduce((currentObject: any, key: string): HomogeneousObject<FormControl> => {
       let formControl = new FormControl(),
         item = {
           [key]: formControl
@@ -88,7 +88,7 @@ export class LogsContainerService {
     tabsStorage.mapCollection((tab: Tab): Tab => {
       let currentAppState = tab.appState || {};
       const appState = Object.assign({}, currentAppState, {
-        activeFilters: this.getFiltersData(tab.type)
+        activeFilters: this.getFiltersData(tab.appState.activeLogsType)
       });
       return Object.assign({}, tab, {
         appState
@@ -120,7 +120,7 @@ export class LogsContainerService {
 
   private readonly paginationOptions: string[] = ['10', '25', '50', '100'];
 
-  filters: {[key: string]: FilterCondition} = {
+  filters: HomogeneousObject<FilterCondition> = {
     clusters: {
       label: 'filter.clusters',
       options: [],
@@ -507,18 +507,25 @@ export class LogsContainerService {
     query: ['includeQuery', 'excludeQuery']
   };
 
+  readonly topResourcesCount: string = '10';
+
+  readonly topUsersCount: string = '6';
+
   readonly logsTypeMap = {
     auditLogs: {
       logsModel: this.auditLogsStorage,
       fieldsModel: this.auditLogsFieldsStorage,
       // TODO add all the required fields
       listFilters: ['clusters', 'timeRange', 'auditLogsSorting', 'pageSize', 'page', 'query'],
+      topResourcesFilters: ['clusters', 'timeRange', 'query'],
       histogramFilters: ['clusters', 'timeRange', 'query']
     },
     serviceLogs: {
       logsModel: this.serviceLogsStorage,
       fieldsModel: this.serviceLogsFieldsStorage,
-      listFilters: ['clusters', 'timeRange', 'components', 'levels', 'hosts', 'serviceLogsSorting', 'pageSize', 'page', 'query'],
+      listFilters: [
+        'clusters', 'timeRange', 'components', 'levels', 'hosts', 'serviceLogsSorting', 'pageSize', 'page', 'query'
+      ],
       histogramFilters: ['clusters', 'timeRange', 'components', 'levels', 'hosts', 'query']
     }
   };
@@ -620,6 +627,10 @@ export class LogsContainerService {
 
   private stopCaptureTime: number;
 
+  topUsersGraphData: HomogeneousObject<HomogeneousObject<number>> = {};
+
+  topResourcesGraphData: HomogeneousObject<HomogeneousObject<number>> = {};
+
   loadLogs = (logsType: LogsType = this.activeLogsType): void => {
     this.httpClient.get(logsType, this.getParams('listFilters')).subscribe((response: Response): void => {
       const jsonResponse = response.json(),
@@ -647,6 +658,34 @@ export class LogsContainerService {
         }
       });
     }
+    if (logsType === 'auditLogs') {
+      this.httpClient.get('topAuditLogsResources', this.getParams('topResourcesFilters', {
+        field: 'resource'
+      }), {
+        number: this.topResourcesCount
+      }).subscribe((response: Response): void => {
+        const jsonResponse = response.json();
+        if (jsonResponse) {
+          const data = jsonResponse.graphData;
+          if (data) {
+            this.topResourcesGraphData = this.parseAuditLogsTopData(data);
+          }
+        }
+      });
+      this.httpClient.get('topAuditLogsResources', this.getParams('topResourcesFilters', {
+        field: 'reqUser'
+      }), {
+        number: this.topUsersCount
+      }).subscribe((response: Response): void => {
+        const jsonResponse = response.json();
+        if (jsonResponse) {
+          const data = jsonResponse.graphData;
+          if (data) {
+            this.topUsersGraphData = this.parseAuditLogsTopData(data);
+          }
+        }
+      });
+    }
   };
 
   loadLogContext(id: string, hostName: string, componentName: string, scrollType: ScrollType = ''): void {
@@ -680,7 +719,19 @@ export class LogsContainerService {
     });
   }
 
-  private getParams(filtersMapName: string, logsType: LogsType = this.activeLogsType): {[key: string]: string} {
+  private parseAuditLogsTopData(data: BarGraph[]): HomogeneousObject<HomogeneousObject<number>> {
+    return data.reduce((currentObject: HomogeneousObject<HomogeneousObject<number>>, currentItem: BarGraph): HomogeneousObject<HomogeneousObject<number>> => Object.assign(currentObject, {
+      [currentItem.name]: currentItem.dataCount.reduce((currentDataObject: HomogeneousObject<number>, currentDataItem: CommonEntry): HomogeneousObject<number> => {
+        return Object.assign(currentDataObject, {
+          [currentDataItem.name]: currentDataItem.value
+        });
+      }, {})
+    }), {});
+  }
+
+  private getParams(
+    filtersMapName: string, additionalParams: HomogeneousObject<string> = {}, logsType: LogsType = this.activeLogsType
+  ): HomogeneousObject<string> {
     let params = {};
     this.logsTypeMap[logsType][filtersMapName].forEach((key: string): void => {
       const inputValue = this.filtersForm.getRawValue()[key],
@@ -698,10 +749,10 @@ export class LogsContainerService {
         }
       });
     }, this);
-    return params;
+    return Object.assign({}, params, additionalParams);
   }
 
-  getHistogramData(data: BarGraph[]): {[key: string]: number} {
+  getHistogramData(data: BarGraph[]): HomogeneousObject<HomogeneousObject<number>> {
     let histogramData = {};
     data.forEach(type => {
       const name = type.name;
@@ -800,7 +851,9 @@ export class LogsContainerService {
     return (value: SearchBoxParameter[]): string => {
       let parameters;
       if (value && value.length) {
-        parameters = value.filter((item: SearchBoxParameter): boolean => item.isExclude === isExclude).map((parameter: SearchBoxParameter): {[key: string]: string} => {
+        parameters = value.filter((item: SearchBoxParameter): boolean => {
+          return item.isExclude === isExclude;
+        }).map((parameter: SearchBoxParameter): HomogeneousObject<string> => {
           return {
             [parameter.name]: parameter.value.replace(/\s/g, '+')
           };
index 23d3726..f89462d 100644 (file)
@@ -300,4 +300,121 @@ describe('UtilsService', () => {
       });
     });
   });
+
+  describe('#isEmptyObject()', () => {
+    const cases = [
+      {
+        obj: {},
+        result: true,
+        title: 'empty object'
+      },
+      {
+        obj: {
+          p: 'v'
+        },
+        result: false,
+        title: 'not empty object'
+      },
+      {
+        obj: null,
+        result: false,
+        title: 'null'
+      },
+      {
+        obj: undefined,
+        result: false,
+        title: 'undefined'
+      },
+      {
+        obj: '',
+        result: false,
+        title: 'empty string'
+      },
+      {
+        obj: 0,
+        result: false,
+        title: 'zero'
+      },
+      {
+        obj: false,
+        result: false,
+        title: 'false'
+      },
+      {
+        obj: NaN,
+        result: false,
+        title: 'NaN'
+      },
+      {
+        obj: [],
+        result: false,
+        title: 'empty array'
+      },
+      {
+        obj: '123',
+        result: false,
+        title: 'not empty primitive'
+      }
+    ];
+
+    cases.forEach(test => {
+      it(test.title, inject([UtilsService], (service: UtilsService) => {
+        expect(service.isEmptyObject(test.obj)).toEqual(test.result);
+      }));
+    });
+  });
+
+  describe('#getMaxNumberInObject()', () => {
+    const cases = [
+      {
+        obj: {
+          a: 1,
+          b: -1,
+          c: 0
+        },
+        max: 1,
+        title: 'basic case'
+      },
+      {
+        obj: {
+          a: 1
+        },
+        max: 1,
+        title: 'single-item object'
+      },
+      {
+        obj: {
+          a: -Infinity,
+          b: 0,
+          c: 1
+        },
+        max: 1,
+        title: 'object with -Infinity'
+      },
+      {
+        obj: {
+          a: Infinity,
+          b: 0,
+          c: 1
+        },
+        max: Infinity,
+        title: 'object with Infinity'
+      },
+      {
+        obj: {
+          a: NaN,
+          b: 0,
+          c: 1
+        },
+        max: 1,
+        title: 'object with NaN'
+      }
+    ];
+
+    cases.forEach(test => {
+      it(test.title, inject([UtilsService], (service: UtilsService) => {
+        expect(service.getMaxNumberInObject(test.obj)).toEqual(test.max);
+      }));
+    });
+  });
 });
index dd9075c..514837c 100644 (file)
@@ -18,6 +18,7 @@
 
 import {Injectable} from '@angular/core';
 import * as moment from 'moment-timezone';
+import {HomogeneousObject} from '@app/classes/object';
 
 @Injectable()
 export class UtilsService {
@@ -93,4 +94,15 @@ export class UtilsService {
     });
   }
 
+  isEmptyObject(obj: any): boolean {
+    return this.isEqual(obj, {});
+  }
+
+  getMaxNumberInObject(obj: HomogeneousObject<number>): number {
+    const keys = Object.keys(obj);
+    return keys.reduce((currentMax: number, currentKey: string): number => {
+      return isNaN(obj[currentKey]) ? currentMax : Math.max(currentMax, obj[currentKey]);
+    }, 0);
+  }
+
 }
index 9561ca0..48c1d88 100644 (file)
@@ -1,9 +1,9 @@
 {
   "common.title": "Log Search",
-  "common.hide": "Hide",
-  "common.show": "Show",
   "common.serviceLogs": "Service Logs",
   "common.auditLogs": "Audit Logs",
+  "common.summary": "Summary",
+  "common.logs": "Logs",
 
   "modal.submit": "OK",
   "modal.cancel": "Cancel",
   "logs.noEventFound": "No event found",
   "logs.hideGraph": "Hide Graph",
   "logs.showGraph": "Show Graph",
+  "logs.topUsers": "Top {{number}} Users",
+  "logs.topResources": "Top {{number}} Resources",
 
   "histogram.gap": "gap",
   "histogram.gaps": "gaps",
index 5d5f1e3..d58b1eb 100644 (file)
@@ -15,3 +15,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+.initial-color {
+  color: initial;
+}
index 177d7df..1ceb968 100644 (file)
@@ -15,4 +15,4 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-.btn-group.open .dropdown-menu,.dropdown.open .dropdown-menu,.font-mixin{font-family:Roboto,sans-serif;font-style:normal;font-weight:400}.pagination-block .pagination-block-item a,.pagination-block .pagination-block-item a:focus,.pagination-block .pagination-block-item a:visited,.table.table-hover .action:hover,a,a:focus,a:visited{text-decoration:none}@font-face{font-family:Roboto;font-weight:400;font-style:normal;src:url(fonts/Roboto-Regular-webfont.eot);src:url(fonts/Roboto-Regular-webfont.eot?#iefix) format('embedded-opentype'),url(fonts/Roboto-Regular-webfont.woff) format('woff'),url(fonts/Roboto-Regular-webfont.ttf) format('truetype'),url(fonts/Roboto-Regular-webfont.svg#robotoregular) format('svg')}  .font-mixin{line-height:1;color:#333}  .btn,.btn:focus{outline:0;font-family:Roboto,sans-serif;text-transform:uppercase;height:34px;font-size:14px;padding:10px 20px;line-height:14px}  .btn .glyphicon,.btn:focus .glyphicon{top:-1px;float:left}  .box-shadow{box-shadow:0 0 2px 0 #1391c1}  .btn-default-disabled,.btn-disabled{box-shadow:none;opacity:.6}  .btn-default-disabled{color:#FFF;background-color:#808793;border:none}  .btn-default,.btn-default:focus{color:#666;background-color:#FFF;border:1px solid #CFD3D7}  .btn-default:focus:hover,.btn-default:hover{color:#FFF;background-color:#808793}  .btn-default:active,.btn-default:focus:active{color:#666;background-color:#FFF;border:1px solid #CFD3D7;box-shadow:0 0 2px 0 #1391c1}  .btn-default.disabled,.btn-default.disabled.active,.btn-default.disabled:active,.btn-default.disabled:hover,.btn-default:focus.disabled,.btn-default:focus.disabled.active,.btn-default:focus.disabled:active,.btn-default:focus.disabled:hover,.btn-default:focus[disabled],.btn-default:focus[disabled].active,.btn-default:focus[disabled]:active,.btn-default:focus[disabled]:hover,.btn-default[disabled],.btn-default[disabled].active,.btn-default[disabled]:active,.btn-default[disabled]:hover{opacity:.6;box-shadow:none;color:#FFF;background-color:#808793;border:none}  .btn-primary-disabled{opacity:.6;box-shadow:none;color:#D1E8D1;background-color:#3FAE2A;border:1px solid #3FAE2A}  .btn-primary,.btn-primary:focus{color:#FFF;background-color:#3FAE2A;border:1px solid #3FAE2A}  .btn-primary:focus:hover,.btn-primary:hover{color:#FFF;background-color:#429929;border:1px solid #429929}  .btn-primary.active,.btn-primary:active,.btn-primary:focus.active,.btn-primary:focus:active{color:#FFF;background-color:#3FAE2A;border:1px solid #3FAE2A;box-shadow:0 0 2px 0 #1391c1}  .btn-primary.disabled,.btn-primary.disabled.active,.btn-primary.disabled:active,.btn-primary.disabled:hover,.btn-primary:focus.disabled,.btn-primary:focus.disabled.active,.btn-primary:focus.disabled:active,.btn-primary:focus.disabled:hover,.btn-primary:focus[disabled],.btn-primary:focus[disabled].active,.btn-primary:focus[disabled]:active,.btn-primary:focus[disabled]:hover,.btn-primary[disabled],.btn-primary[disabled].active,.btn-primary[disabled]:active,.btn-primary[disabled]:hover{opacity:.6;box-shadow:none;color:#D1E8D1;background-color:#3FAE2A;border:1px solid #3FAE2A}  .btn-secondary-disabled{opacity:.6;box-shadow:none;color:#D1E8D1;background-color:#429929;border:1px solid #3FAE2A}  .btn-secondary,.btn-secondary:focus{color:#429929;background-color:#FFF;border:1px solid #3FAE2A}  .btn-secondary:focus:hover,.btn-secondary:hover{color:#FFF;background-color:#429929}  .btn-secondary:active,.btn-secondary:focus:active{color:#429929;background-color:#FFF;box-shadow:0 0 2px 0 #1391c1}  .btn-secondary.disabled,.btn-secondary.disabled.active,.btn-secondary.disabled:active,.btn-secondary.disabled:hover,.btn-secondary:focus.disabled,.btn-secondary:focus.disabled.active,.btn-secondary:focus.disabled:active,.btn-secondary:focus.disabled:hover,.btn-secondary:focus[disabled],.btn-secondary:focus[disabled].active,.btn-secondary:focus[disabled]:active,.btn-secondary:focus[disabled]:hover,.btn-secondary[disabled],.btn-secondary[disabled].active,.btn-secondary[disabled]:active,.btn-secondary[disabled]:hover{opacity:.6;box-shadow:none;color:#D1E8D1;background-color:#429929;border:1px solid #3FAE2A}  .btn-success{border:none}  .btn-regular-default-state{background-color:#FFF;color:#666;border:1px solid #cfd3d7}  .btn-primary-default-state{background-color:#3FAE2A;border:1px solid #3FAE2A;color:#FFF}  .btn-group.open .btn.dropdown-toggle,.dropdown.open .btn.dropdown-toggle{box-shadow:inset 0 0 3px 0 #1391c1}  .btn-group.open .btn.dropdown-toggle,.btn-group.open .btn.dropdown-toggle.btn-default,.btn-group.open .btn.dropdown-toggle.btn-default:hover,.btn-group.open .btn.dropdown-toggle:hover,.dropdown.open .btn.dropdown-toggle,.dropdown.open .btn.dropdown-toggle.btn-default,.dropdown.open .btn.dropdown-toggle.btn-default:hover,.dropdown.open .btn.dropdown-toggle:hover{background-color:#FFF;color:#666;border:1px solid #cfd3d7}  .btn-group.open .btn.dropdown-toggle+.dropdown-menu>li>a:hover,.btn-group.open .btn.dropdown-toggle.btn-default+.dropdown-menu>li>a:hover,.dropdown.open .btn.dropdown-toggle+.dropdown-menu>li>a:hover,.dropdown.open .btn.dropdown-toggle.btn-default+.dropdown-menu>li>a:hover{background-color:#808793;color:#FFF}  .btn-group.open .btn.dropdown-toggle.btn-primary,.btn-group.open .btn.dropdown-toggle.btn-primary:hover,.dropdown.open .btn.dropdown-toggle.btn-primary,.dropdown.open .btn.dropdown-toggle.btn-primary:hover{background-color:#3FAE2A;border:1px solid #3FAE2A;color:#FFF}  .btn-group.open .btn.dropdown-toggle.btn-primary+.dropdown-menu>li>a:hover,.dropdown.open .btn.dropdown-toggle.btn-primary+.dropdown-menu>li>a:hover{background-color:#429929;color:#FFF}  .btn-group.open .dropdown-menu,.dropdown.open .dropdown-menu{line-height:1;border-radius:2px;font-size:14px;min-width:200px;background:#FFF;color:#666;border:1px solid #cfd3d7}  .btn-group.open .dropdown-menu>li,.dropdown.open .dropdown-menu>li{margin-bottom:1px}  .btn-group.open .dropdown-menu>li>a,.dropdown.open .dropdown-menu>li>a{height:24px}  .btn-group .btn.dropdown-toggle:first-child,.dropdown .btn.dropdown-toggle:first-child{min-width:80px}  .btn-group .btn.dropdown-toggle.disabled,.btn-group .btn.dropdown-toggle[disabled],.dropdown .btn.dropdown-toggle.disabled,.dropdown .btn.dropdown-toggle[disabled]{opacity:.6}  input.form-control{font-size:14px;border-radius:2px;color:#666;border:1px solid #CFD3D7;height:34px;padding:10px}  input.form-control:focus{border-color:#1291c1;box-shadow:none}  .help-block{color:#999;font-size:14px}  .help-block.validation-block{color:#999;margin-top:10px}  .help-block.validation-block::before{position:relative;top:2px;margin-right:5px;font-family:'Glyphicons Halflings'}  .wizard .wizard-body .wizard-content .step-description,.wizard .wizard-body .wizard-content .step-title,.wizard .wizard-header h3,h2.table-title{font-family:Roboto,sans-serif;font-style:normal}  .has-success input.form-control{color:#666;border:1px solid #1EB475}  .has-success input.form-control:focus{border-color:#1EB475;box-shadow:none}  .has-success .help-block.validation-block::before{content:'\e084';color:#1EB475}  .has-error input.form-control{color:#666;border:1px solid #EF6162}  .has-error input.form-control:focus{border-color:#EF6162;box-shadow:none}  .has-error .help-block.validation-block::before{content:'\e083';color:#EF6162}  .has-warning input.form-control{color:#666;border:1px solid #E98A40}  .has-warning input.form-control:focus{border-color:#E98A40;box-shadow:none}  .has-warning .help-block.validation-block::before{content:'\e101';color:#E98A40}  .form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{color:#999;border-color:#ccc;background-color:#ddd}  h2.table-title{font-weight:400;line-height:1;color:#333;margin-top:10px;font-size:20px}  .table{color:#666;font-size:13px}  .table tfoot,.table thead{color:#999}  .table input[type=checkbox]+label{position:relative;line-height:1.3em;font-size:initial;top:4px;margin-bottom:0}  .table thead>tr>th{border-bottom-color:#EEE}  .table tfoot>tr:first-of-type>td{border-top-width:2px;border-top-color:#EEE}  .table>tbody>tr>td{border-top-color:#EEE}  .table>tbody>tr.active,.table>tbody>tr.active>td{background-color:#EEE}  .table.table-hover .action{visibility:hidden;padding:0;line-height:1}  .table.table-hover>tbody>tr{border-width:0 1px 1px;border-style:solid;border-color:#EEE transparent}  .table.table-hover>tbody>tr>td{border-width:0}  .table.table-hover>tbody>tr:hover{border-color:#A7DFF2;background-color:#E7F6FC}  .table.table-hover>tbody>tr:hover>td{border-top:1px solid #A7DFF2;background-color:#E7F6FC}  .table.table-hover>tbody>tr:hover>td .action{visibility:visible}  .table.table-hover>tbody>tr:first-of-type>td{border-top:1px solid transparent}  .table.table-hover>tbody>tr:first-of-type:hover>td{border-color:#A7DFF2}  .pagination-block .pagination-block-item{float:left;padding:0 5px}  .pagination-block .pagination-block-item select{border:none;background-color:transparent;color:#1491C1}  .nav.nav-tabs{border:none;margin-bottom:20px}  .nav.nav-tabs li a{border-width:0;border-radius:0;border-bottom:2px solid transparent;color:#666;text-transform:uppercase}  .nav.nav-tabs li a:active,.nav.nav-tabs li a:focus,.nav.nav-tabs li a:hover{color:#333;border-top-width:0;border-left-width:0;border-right-width:0;background:0 0}  .nav.nav-tabs li a .badge.badge-important{display:inline}  .nav.nav-tabs li.active a{color:#333;border-bottom:2px solid #3FAE2A}  .nav-tabs-left li,.nav-tabs-right li{float:none;margin-bottom:2px}  .nav-tabs-left li a,.nav-tabs-right li a{margin-right:0}  .nav-tabs-left li{margin-right:-1px}  .nav-tabs-left li a{border:2px solid transparent!important}  .nav-tabs-left li.active a,.nav-tabs-left li.active a:active,.nav-tabs-left li.active a:focus,.nav-tabs-left li.active a:hover{border-right:2px solid #3FAE2A!important}  .nav-tabs-right li{margin-left:-1px}  .nav-tabs-right li a{border:2px solid transparent!important}  .nav-tabs-right li.active a,.nav-tabs-right li.active a:active,.nav-tabs-right li.active a:focus,.nav-tabs-right li.active a:hover{border-left:2px solid #3FAE2A!important}  .wizard{border:2px solid #ebecf1}  .wizard .wizard-header h3{font-weight:400;line-height:1;font-size:20px;color:#333;margin:15px 20px}  .wizard .wizard-body{overflow:hidden;margin:0}  .wizard .wizard-body .wizard-content{background:#ebecf1;padding-top:25px;float:left;margin-bottom:-99999px;padding-bottom:99999px}  .wizard .wizard-body .wizard-content .step-title{line-height:1;font-weight:700;font-size:18px;color:#666}  .wizard .wizard-body .wizard-content .step-description{font-weight:400;font-size:14px;line-height:1.4;color:#999}  .wizard .wizard-body .wizard-content .panel.panel-default{border:none;box-shadow:none;margin-top:20px}  .wizard .wizard-body .wizard-content .panel.panel-default .panel-body{padding:30px 20px}  .wizard .wizard-body .wizard-nav{min-height:550px;padding-top:25px;background-color:#323544;float:left;margin-bottom:-99999px;padding-bottom:99999px}  .wizard .wizard-body .wizard-nav .nav li{padding:0 15px}  .wizard .wizard-body .wizard-nav .nav li a{height:48px;padding:0 5px;display:table-cell;vertical-align:middle}  .wizard .wizard-body .wizard-nav .nav li .step-marker{position:absolute;top:9px;line-height:16px;text-align:center;width:20px;height:20px;border:2px solid #1EB475;border-radius:50%;font-size:12px;font-style:inherit;color:#1EB475;background-color:#323544}  .wizard .wizard-body .wizard-nav .nav li .step-description,.wizard .wizard-body .wizard-nav .nav li .step-name{font-family:Roboto,sans-serif;font-weight:400;color:#999;margin-left:30px;font-style:normal}  .wizard .wizard-body .wizard-nav .nav li .step-name{line-height:1;font-size:14px}  .wizard .wizard-body .wizard-nav .nav li .step-index{line-height:18px}  .wizard .wizard-body .wizard-nav .nav li .step-description{line-height:1;font-size:12px}  .wizard .wizard-body .wizard-nav .nav li.completed .step-marker{background-color:#1EB475;color:#fff;font-size:10px;padding-left:2px}  .wizard .wizard-body .wizard-nav .nav li.completed .step-marker .step-index{display:none}  .wizard .wizard-body .wizard-nav .nav li.completed .step-marker:after{font-family:"Glyphicons Halflings";content:"\e013";position:relative;top:1px;left:-1px}  .wizard .wizard-body .wizard-nav .nav li.completed:after{width:2px;height:100%;position:absolute;background-color:#1EB475;content:"";top:25px;left:29px}  .wizard .wizard-body .wizard-nav .nav li.completed:last-child:after{content:none}  .wizard .wizard-body .wizard-nav .nav li.active .step-name{font-weight:700}  .wizard .wizard-body .wizard-nav .nav li.disabled .step-marker{color:#666;border-color:#666}  .wizard .wizard-body .wizard-nav .nav li.disabled .step-description,.wizard .wizard-body .wizard-nav .nav li.disabled .step-name{color:#666}  .wizard .wizard-body .wizard-nav .nav li.disabled.completed .step-marker{background-color:#1EB475;border:2px solid #1EB475;color:#fff}  .wizard .wizard-body .wizard-nav .nav-pills>li.active>a,.wizard .wizard-body .wizard-nav .nav-pills>li.active>a:focus,.wizard .wizard-body .wizard-nav .nav-pills>li.active>a:hover,.wizard .wizard-body .wizard-nav .nav>li>a:focus,.wizard .wizard-body .wizard-nav .nav>li>a:hover{background-color:inherit}  .wizard .wizard-body .wizard-footer{background:#fff;padding:15px 20px}  .wizard .wizard-body .wizard-footer button{margin:0 10px}  .checkbox-disabled-style{background-color:#b2b8c1;border-color:#b2b8c1}  input[type=radio]:checked,input[type=radio]:not(:checked),input[type=checkbox]:checked,input[type=checkbox]:not(:checked){display:none}  input[type=radio]:checked+label,input[type=radio]:not(:checked)+label,input[type=checkbox]:checked+label,input[type=checkbox]:not(:checked)+label{position:relative;padding-left:20px}  input[type=radio]:checked+label:hover:before,input[type=radio]:not(:checked)+label:hover:before,input[type=checkbox]:checked+label:hover:before,input[type=checkbox]:not(:checked)+label:hover:before{border-color:#1491C1;background-color:#1491C1}  input[type=radio]:checked+label:before,input[type=checkbox]:checked+label:before{background-color:#1491C1;border-color:#1491C1}  input[type=radio].disabled+label:before,input[type=radio].disabled+label:hover:before,input[type=radio][disabled]+label:before,input[type=radio][disabled]+label:hover:before,input[type=checkbox].disabled+label:before,input[type=checkbox].disabled+label:hover:before,input[type=checkbox][disabled]+label:before,input[type=checkbox][disabled]+label:hover:before{background-color:#b2b8c1;border-color:#b2b8c1}  input[type=checkbox]+label:before{content:'';position:absolute;left:0;top:4px;width:10px;height:10px;-moz-box-sizing:border-box;box-sizing:border-box;border-radius:2px;border-width:1px;border-style:solid;border-color:#ddd}  input[type=checkbox]:checked+label:after{content:'\2714';color:#FFF;position:absolute;top:0;left:2px;font-size:9px}  input.radio+label:before,input[type=radio]+label:before{content:'';position:absolute;left:0;top:0;width:12px;height:12px;-moz-box-sizing:border-box;box-sizing:border-box;border-radius:12px;border-width:1px;border-style:solid;border-color:#ddd}  input.radio:checked+label:after,input[type=radio]:checked+label:after{content:'';background-color:#FFF;position:absolute;top:3px;left:3px;width:6px;height:6px;border-radius:6px}  .navigation-bar-container,.navigation-bar-container ul.nav.side-nav-header{width:230px;transition:width .5s ease-out}  .navigation-bar-container{height:auto;background-color:#323544;padding:0;-ms-overflow-style:none;-webkit-font-smoothing:antialiased}  .navigation-bar-container ul.nav.side-nav-header li.navigation-header{background:#313d54;padding:15px 5px 15px 25px;height:55px}  .navigation-bar-container ul.nav.side-nav-header li.navigation-header>a.ambari-logo{padding:0}  .navigation-bar-container ul.nav.side-nav-header li.navigation-header>a.ambari-logo>img{height:25px;float:left;margin-left:-3px}  .navigation-bar-container ul.nav.side-nav-header li.navigation-header .btn-group{cursor:pointer;margin-top:3px}  .navigation-bar-container ul.nav.side-nav-header li.navigation-header .btn-group:hover span.ambari-header,.navigation-bar-container ul.nav.side-nav-header li.navigation-header .btn-group:hover span.toggle-icon{color:#fff}  .navigation-bar-container ul.nav.side-nav-header li.navigation-header .btn-group span.ambari-header{font-family:Roboto,sans-serif;font-weight:400;font-style:normal;line-height:1;font-size:20px;width:55px;display:inline;color:#b8bec4;padding:0 8px 0 10px}  .navigation-bar-container ul.nav.side-nav-header li.navigation-header .btn-group span.toggle-icon{margin-bottom:5px;font-size:13px;display:inline-block;vertical-align:middle;color:#b8bec4}  .navigation-bar-container ul.nav.side-nav-header li.navigation-header .btn-group.open .dropdown-toggle{box-shadow:none}  .navigation-bar-container ul.nav.side-nav-header li.navigation-header ul.dropdown-menu{top:30px}  .navigation-bar-container ul.nav.side-nav-header li.navigation-header ul.dropdown-menu li>a{font-family:Roboto,sans-serif;font-weight:400;font-style:normal;font-size:14px;color:#666;line-height:1.42;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}  .navigation-bar-container ul.nav.side-nav-header li.navigation-header ul.dropdown-menu li>a:hover{background:#f5f5f5}  .navigation-bar-container ul.nav.side-nav-footer,.navigation-bar-container ul.nav.side-nav-menu{background-color:#323544;width:230px;transition:width .5s ease-out}  .navigation-bar-container ul.nav.side-nav-footer li,.navigation-bar-container ul.nav.side-nav-menu li{padding:0;margin:0}  .navigation-bar-container ul.nav.side-nav-footer li.mainmenu-li>a,.navigation-bar-container ul.nav.side-nav-footer li.navigation-footer>a,.navigation-bar-container ul.nav.side-nav-footer li.submenu-li>a,.navigation-bar-container ul.nav.side-nav-menu li.mainmenu-li>a,.navigation-bar-container ul.nav.side-nav-menu li.navigation-footer>a,.navigation-bar-container ul.nav.side-nav-menu li.submenu-li>a{display:table-cell;vertical-align:middle;width:230px;border-radius:0;-moz-border-radius:0;-webkit-border-radius:0;white-space:nowrap}  .navigation-bar-container ul.nav.side-nav-footer li.mainmenu-li>a .navigation-menu-item,.navigation-bar-container ul.nav.side-nav-footer li.navigation-footer>a .navigation-menu-item,.navigation-bar-container ul.nav.side-nav-footer li.submenu-li>a .navigation-menu-item,.navigation-bar-container ul.nav.side-nav-menu li.mainmenu-li>a .navigation-menu-item,.navigation-bar-container ul.nav.side-nav-menu li.navigation-footer>a .navigation-menu-item,.navigation-bar-container ul.nav.side-nav-menu li.submenu-li>a .navigation-menu-item{font-family:Roboto,sans-serif;font-weight:400;font-style:normal;line-height:1;font-size:14px;color:#b8bec4;padding-left:8px}  .navigation-bar-container ul.nav.side-nav-footer li.mainmenu-li>a .navigation-icon,.navigation-bar-container ul.nav.side-nav-footer li.navigation-footer>a .navigation-icon,.navigation-bar-container ul.nav.side-nav-footer li.submenu-li>a .navigation-icon,.navigation-bar-container ul.nav.side-nav-menu li.mainmenu-li>a .navigation-icon,.navigation-bar-container ul.nav.side-nav-menu li.navigation-footer>a .navigation-icon,.navigation-bar-container ul.nav.side-nav-menu li.submenu-li>a .navigation-icon{line-height:18px;font-size:18px;color:#b8bec4}  .navigation-bar-container ul.nav.side-nav-footer li.mainmenu-li>a .toggle-icon,.navigation-bar-container ul.nav.side-nav-footer li.navigation-footer>a .toggle-icon,.navigation-bar-container ul.nav.side-nav-footer li.submenu-li>a .toggle-icon,.navigation-bar-container ul.nav.side-nav-menu li.mainmenu-li>a .toggle-icon,.navigation-bar-container ul.nav.side-nav-menu li.navigation-footer>a .toggle-icon,.navigation-bar-container ul.nav.side-nav-menu li.submenu-li>a .toggle-icon{line-height:14px;font-size:14px;color:#b8bec4;padding:3px 5px 3px 10px}  .navigation-bar-container ul.nav.side-nav-footer li.mainmenu-li>a,.navigation-bar-container ul.nav.side-nav-footer li.navigation-footer>a,.navigation-bar-container ul.nav.side-nav-menu li.mainmenu-li>a,.navigation-bar-container ul.nav.side-nav-menu li.navigation-footer>a{padding:10px 5px 10px 20px}  .navigation-bar-container ul.nav.side-nav-footer li.submenu-li>a,.navigation-bar-container ul.nav.side-nav-menu li.submenu-li>a{padding:10px 5px 10px 25px}  .navigation-bar-container ul.nav.side-nav-footer li.navigation-footer,.navigation-bar-container ul.nav.side-nav-menu li.navigation-footer{background:#313d54;height:50px}  .navigation-bar-container ul.nav.side-nav-footer li.navigation-footer a .navigation-icon,.navigation-bar-container ul.nav.side-nav-menu li.navigation-footer a .navigation-icon{color:#31823a;font-size:20px;position:relative;padding:0 15px;left:calc(30%)}  .navigation-bar-container ul.nav.side-nav-footer li.navigation-footer a .navigation-icon:hover,.navigation-bar-container ul.nav.side-nav-menu li.navigation-footer a .navigation-icon:hover{color:#fff}  .navigation-bar-container ul.nav.side-nav-footer li>ul>li,.navigation-bar-container ul.nav.side-nav-menu li>ul>li{background-color:#323544}  .navigation-bar-container ul.nav.side-nav-footer li>ul>li a,.navigation-bar-container ul.nav.side-nav-menu li>ul>li a{font-family:Roboto,sans-serif;font-weight:400;font-style:normal;line-height:1;font-size:14px;color:#999}  .navigation-bar-container ul.nav.side-nav-footer li>ul>li a .submenu-icon,.navigation-bar-container ul.nav.side-nav-menu li>ul>li a .submenu-icon{line-height:14px;font-size:14px}  .navigation-bar-container ul.nav.side-nav-footer li>a:hover,.navigation-bar-container ul.nav.side-nav-footer li>ul>li>a:hover,.navigation-bar-container ul.nav.side-nav-menu li>a:hover,.navigation-bar-container ul.nav.side-nav-menu li>ul>li>a:hover{background:#404351;cursor:pointer;color:#fff}  .navigation-bar-container ul.nav.side-nav-footer li>a:hover .navigation-icon,.navigation-bar-container ul.nav.side-nav-footer li>a:hover .navigation-menu-item,.navigation-bar-container ul.nav.side-nav-footer li>a:hover .submenu-item,.navigation-bar-container ul.nav.side-nav-footer li>a:hover .toggle-icon,.navigation-bar-container ul.nav.side-nav-footer li>ul>li>a:hover .navigation-icon,.navigation-bar-container ul.nav.side-nav-footer li>ul>li>a:hover .navigation-menu-item,.navigation-bar-container ul.nav.side-nav-footer li>ul>li>a:hover .submenu-item,.navigation-bar-container ul.nav.side-nav-footer li>ul>li>a:hover .toggle-icon,.navigation-bar-container ul.nav.side-nav-menu li>a:hover .navigation-icon,.navigation-bar-container ul.nav.side-nav-menu li>a:hover .navigation-menu-item,.navigation-bar-container ul.nav.side-nav-menu li>a:hover .submenu-item,.navigation-bar-container ul.nav.side-nav-menu li>a:hover .toggle-icon,.navigation-bar-container ul.nav.side-nav-menu li>ul>li>a:hover .navigation-icon,.navigation-bar-container ul.nav.side-nav-menu li>ul>li>a:hover .navigation-menu-item,.navigation-bar-container ul.nav.side-nav-menu li>ul>li>a:hover .submenu-item,.navigation-bar-container ul.nav.side-nav-menu li>ul>li>a:hover .toggle-icon{color:#fff}  .navigation-bar-container ul.nav.side-nav-footer li.active.collapsed,.navigation-bar-container ul.nav.side-nav-footer li.active:not(.has-sub-menu),.navigation-bar-container ul.nav.side-nav-menu li.active.collapsed,.navigation-bar-container ul.nav.side-nav-menu li.active:not(.has-sub-menu){background:#404351;cursor:pointer}  .navigation-bar-container ul.nav.side-nav-footer li.active.collapsed>a,.navigation-bar-container ul.nav.side-nav-footer li.active:not(.has-sub-menu)>a,.navigation-bar-container ul.nav.side-nav-menu li.active.collapsed>a,.navigation-bar-container ul.nav.side-nav-menu li.active:not(.has-sub-menu)>a{color:#fff}  .navigation-bar-container ul.nav.side-nav-footer li.active.collapsed>a .navigation-icon,.navigation-bar-container ul.nav.side-nav-footer li.active.collapsed>a .navigation-menu-item,.navigation-bar-container ul.nav.side-nav-footer li.active.collapsed>a .submenu-item,.navigation-bar-container ul.nav.side-nav-footer li.active.collapsed>a .toggle-icon,.navigation-bar-container ul.nav.side-nav-footer li.active:not(.has-sub-menu)>a .navigation-icon,.navigation-bar-container ul.nav.side-nav-footer li.active:not(.has-sub-menu)>a .navigation-menu-item,.navigation-bar-container ul.nav.side-nav-footer li.active:not(.has-sub-menu)>a .submenu-item,.navigation-bar-container ul.nav.side-nav-footer li.active:not(.has-sub-menu)>a .toggle-icon,.navigation-bar-container ul.nav.side-nav-menu li.active.collapsed>a .navigation-icon,.navigation-bar-container ul.nav.side-nav-menu li.active.collapsed>a .navigation-menu-item,.navigation-bar-container ul.nav.side-nav-menu li.active.collapsed>a .submenu-item,.navigation-bar-container ul.nav.side-nav-menu li.active.collapsed>a .toggle-icon,.navigation-bar-container ul.nav.side-nav-menu li.active:not(.has-sub-menu)>a .navigation-icon,.navigation-bar-container ul.nav.side-nav-menu li.active:not(.has-sub-menu)>a .navigation-menu-item,.navigation-bar-container ul.nav.side-nav-menu li.active:not(.has-sub-menu)>a .submenu-item,.navigation-bar-container ul.nav.side-nav-menu li.active:not(.has-sub-menu)>a .toggle-icon{color:#fff}  .navigation-bar-container ul.nav.side-nav-footer li.active.collapsed>a:after,.navigation-bar-container ul.nav.side-nav-footer li.active:not(.has-sub-menu)>a:after,.navigation-bar-container ul.nav.side-nav-menu li.active.collapsed>a:after,.navigation-bar-container ul.nav.side-nav-menu li.active:not(.has-sub-menu)>a:after{left:0;top:50%;border:solid transparent;border-width:10px 7px;content:" ";height:0;width:0;position:absolute;pointer-events:none;border-color:transparent transparent transparent #31823a;margin-top:-12px}  .navigation-bar-container ul.nav.side-nav-footer .more-actions,.navigation-bar-container ul.nav.side-nav-menu .more-actions{display:none;position:absolute;top:14px;right:33px;line-height:25px;width:20px;text-align:center;font-size:14px;cursor:pointer;vertical-align:middle;color:#fff}  .navigation-bar-container.collapsed,.navigation-bar-container.collapsed ul.nav.side-nav-footer,.navigation-bar-container.collapsed ul.nav.side-nav-header,.navigation-bar-container.collapsed ul.nav.side-nav-menu{width:50px}  .navigation-bar-container ul.nav.side-nav-footer .more-actions .dropdown-menu>li>a,.navigation-bar-container ul.nav.side-nav-footer .more-actions .dropdown-menu>li>a i,.navigation-bar-container ul.nav.side-nav-menu .more-actions .dropdown-menu>li>a,.navigation-bar-container ul.nav.side-nav-menu .more-actions .dropdown-menu>li>a i{color:#666}  .navigation-bar-container ul.nav.side-nav-footer .more-actions .dropdown-menu>li>a:hover,.navigation-bar-container ul.nav.side-nav-menu .more-actions .dropdown-menu>li>a:hover{background:#f5f5f5}  .navigation-bar-container ul.nav.side-nav-footer .menu-item-name,.navigation-bar-container ul.nav.side-nav-menu .menu-item-name{display:inline-block;vertical-align:bottom;max-width:100px;overflow:hidden;text-overflow:ellipsis;-o-text-overflow:ellipsis;-ms-text-overflow:ellipsis;white-space:nowrap}  .navigation-bar-container.collapsed ul.nav.side-nav-footer .more-actions,.navigation-bar-container.collapsed ul.nav.side-nav-footer li a .navigation-menu-item,.navigation-bar-container.collapsed ul.nav.side-nav-footer li a .toggle-icon,.navigation-bar-container.collapsed ul.nav.side-nav-header li.navigation-header .dropdown-menu,.navigation-bar-container.collapsed ul.nav.side-nav-header li.navigation-header span.ambari-header,.navigation-bar-container.collapsed ul.nav.side-nav-header li.navigation-header span.toggle-icon,.navigation-bar-container.collapsed ul.nav.side-nav-menu .more-actions,.navigation-bar-container.collapsed ul.nav.side-nav-menu li a .navigation-menu-item,.navigation-bar-container.collapsed ul.nav.side-nav-menu li a .toggle-icon{display:none}  .navigation-bar-container .nav-pills>li.active>a,.navigation-bar-container .nav-pills>li.active>a:focus,.navigation-bar-container .nav-pills>li.active>a:hover,.navigation-bar-container .nav>li>a:focus,.navigation-bar-container .nav>li>a:hover{background-color:inherit}  .navigation-bar-container.collapsed ul.nav.side-nav-header li.navigation-header{padding:15px 0 15px 15px}  .navigation-bar-container.collapsed ul.nav.side-nav-footer li a,.navigation-bar-container.collapsed ul.nav.side-nav-menu li a{padding:15px 0 15px 15px;width:50px}  .navigation-bar-container.collapsed ul.nav.side-nav-footer li.navigation-footer a .navigation-icon,.navigation-bar-container.collapsed ul.nav.side-nav-menu li.navigation-footer a .navigation-icon{padding:0 5px;left:0}  .navigation-bar-container.collapsed ul.nav.side-nav-footer li ul.sub-menu,.navigation-bar-container.collapsed ul.nav.side-nav-menu li ul.sub-menu{display:none;width:230px;position:absolute;z-index:100;top:0;left:50px}  .navigation-bar-container.collapsed ul.nav.side-nav-footer li.submenu-li>a,.navigation-bar-container.collapsed ul.nav.side-nav-menu li.submenu-li>a{padding:10px 5px 10px 25px;width:230px}  .navigation-bar-container.collapsed ul.nav.side-nav-footer li.active,.navigation-bar-container.collapsed ul.nav.side-nav-menu li.active{background:#404351;cursor:pointer}  .navigation-bar-container.collapsed ul.nav.side-nav-footer li.active>a,.navigation-bar-container.collapsed ul.nav.side-nav-footer li.active>a .navigation-icon,.navigation-bar-container.collapsed ul.nav.side-nav-footer li.active>a .navigation-menu-item,.navigation-bar-container.collapsed ul.nav.side-nav-footer li.active>a .submenu-item,.navigation-bar-container.collapsed ul.nav.side-nav-footer li.active>a .toggle-icon,.navigation-bar-container.collapsed ul.nav.side-nav-menu li.active>a,.navigation-bar-container.collapsed ul.nav.side-nav-menu li.active>a .navigation-icon,.navigation-bar-container.collapsed ul.nav.side-nav-menu li.active>a .navigation-menu-item,.navigation-bar-container.collapsed ul.nav.side-nav-menu li.active>a .submenu-item,.navigation-bar-container.collapsed ul.nav.side-nav-menu li.active>a .toggle-icon{color:#fff}  .navigation-bar-container.collapsed ul.nav.side-nav-footer li.active>a:after,.navigation-bar-container.collapsed ul.nav.side-nav-menu li.active>a:after{left:0;top:50%;border:solid transparent;border-width:12px 6px;content:" ";height:0;width:0;position:absolute;pointer-events:none;border-color:transparent transparent transparent #31823a;margin-top:-12px}  .navigation-bar-fit-height{position:fixed;top:0;bottom:0;left:0;z-index:2079}  .navigation-bar-fit-height .side-nav-header{position:absolute;top:0}  .navigation-bar-fit-height .side-nav-menu{position:absolute;top:55px;bottom:50px}  .navigation-bar-fit-height .side-nav-footer{position:absolute;bottom:0}  .navigation-bar-fit-height .more-actions .dropdown-menu{position:fixed;top:auto;left:auto}  .navigation-bar-fit-height .navigation-bar-container{height:100%}  .navigation-bar-fit-height .navigation-bar-container .side-nav-menu{overflow-y:auto}  .notifications-group{position:relative;top:1px}  #notifications-dropdown.dropdown-menu{min-width:300px;max-width:300px;min-height:150px;padding:0;z-index:1000;right:-50px;left:auto;top:260%;border:none;box-shadow:0 2px 10px 2px rgba(0,0,0,.29)}  #notifications-dropdown.dropdown-menu .popup-arrow-up{position:absolute;right:37px;top:-40px;width:40px;height:40px;overflow:hidden}  #notifications-dropdown.dropdown-menu .popup-arrow-up:after{content:"";position:absolute;width:20px;height:20px;background:#fff;-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg);top:30px;left:10px;box-shadow:-1px -1px 10px -2px rgba(0,0,0,.5)}  #notifications-dropdown.dropdown-menu .notifications-header{border-bottom:1px solid #eee;padding:15px 20px}  #notifications-dropdown.dropdown-menu .notifications-header .notifications-title{font-family:Roboto,sans-serif;font-weight:400;font-style:normal;line-height:1;color:#333;font-size:16px}  #notifications-dropdown.dropdown-menu .notifications-body{padding:0 15px;overflow:auto;max-height:500px}  #notifications-dropdown.dropdown-menu .notifications-body .no-alert-text{padding:15px 5px}  #notifications-dropdown.dropdown-menu .notifications-body .table-controls{padding:10px 0;margin:0;border-bottom:1px solid #eee}  #notifications-dropdown.dropdown-menu .notifications-body .table-controls .state-filter{padding:0;font-family:Roboto,sans-serif;font-weight:400;font-style:normal;line-height:1;font-size:12px;color:#666;position:relative}  #notifications-dropdown.dropdown-menu .notifications-body .table-controls .state-filter .form-control.filter-select{font-size:12px;color:#666;height:25px}  #notifications-dropdown.dropdown-menu .notifications-body .table.alerts-table{margin-top:0}  #notifications-dropdown.dropdown-menu .notifications-body .table.alerts-table tbody tr{cursor:pointer}  #notifications-dropdown.dropdown-menu .notifications-body .table.alerts-table tbody tr.no-alert-tr:hover{cursor:default;border-color:transparent transparent #eee}  #notifications-dropdown.dropdown-menu .notifications-body .table.alerts-table tbody tr.no-alert-tr:hover>td{border-color:transparent;background-color:#fff}  #notifications-dropdown.dropdown-menu .notifications-body .table.alerts-table tbody td.status{width:9%;padding:15px 3px}  #notifications-dropdown.dropdown-menu .notifications-body .table.alerts-table tbody td.status .alert-state-CRITICAL{color:#EF6162}  #notifications-dropdown.dropdown-menu .notifications-body .table.alerts-table tbody td.status .alert-state-WARNING{color:#E98A40}  #notifications-dropdown.dropdown-menu .notifications-body .table.alerts-table tbody td.content{width:90%;padding:15px 3px 10px;font-family:Roboto,sans-serif;font-weight:400;font-style:normal;color:#333;line-height:1.3}  #notifications-dropdown.dropdown-menu .notifications-body .table.alerts-table tbody td.content .name{font-weight:700;font-size:14px;color:#333;margin-bottom:5px}  #notifications-dropdown.dropdown-menu .notifications-body .table.alerts-table tbody td.content .description{font-size:12px;color:#666;margin-bottom:4px;display:block;display:-webkit-box;-webkit-line-clamp:3;max-height:47px;-webkit-box-orient:vertical;overflow:hidden;text-overflow:ellipsis;overflow-wrap:break-word;word-wrap:break-word;-ms-word-break:break-all;word-break:break-all;word-break:break-word;-ms-hyphens:auto;-moz-hyphens:auto;-webkit-hyphens:auto;hyphens:auto}  #notifications-dropdown.dropdown-menu .notifications-body .table.alerts-table tbody td.content .timestamp{text-align:right;font-size:11px;color:#999}  #notifications-dropdown.dropdown-menu .notifications-footer{border-top:1px solid #eee;padding:15px}  .modal-backdrop{background-color:grey}  .modal .modal-content{border-radius:2px}  .modal .modal-content .modal-body,.modal .modal-content .modal-footer,.modal .modal-content .modal-header{padding-left:20px;padding-right:20px}  .modal .modal-content .modal-header{border-bottom:none;padding-top:20px;color:#666;font-size:20px}  .modal .modal-content .modal-header h4{margin:0;color:inherit;font-size:inherit}  .modal .modal-content .modal-body{color:#666;font-size:12px}  .modal .modal-content .modal-footer{border-top:none;padding-bottom:20px}  .modal .modal-content .modal-footer .btn~.btn{margin-left:10px}  .accordion .panel-group,.wizard .wizard-body .wizard-content .accordion .panel-group{margin-bottom:0}  .accordion .panel-group .panel,.wizard .wizard-body .wizard-content .accordion .panel-group .panel{border-radius:0;border:none;margin-top:0}  .accordion .panel-group .panel .panel-heading,.wizard .wizard-body .wizard-content .accordion .panel-group .panel .panel-heading{height:50px;padding:15px 10px;border:1px solid;border-color:#ddd transparent;border-top:none;background:#fff}  .accordion .panel-group .panel .panel-heading .panel-title,.wizard .wizard-body .wizard-content .accordion .panel-group .panel .panel-heading .panel-title{font-family:Roboto,sans-serif;font-weight:400;font-style:normal;line-height:1;color:#333}  .accordion .panel-group .panel .panel-heading .panel-title>a,.wizard .wizard-body .wizard-content .accordion .panel-group .panel .panel-heading .panel-title>a{font-size:18px;color:#333}  .accordion .panel-group .panel .panel-heading .panel-title>i,.wizard .wizard-body .wizard-content .accordion .panel-group .panel .panel-heading .panel-title>i{font-size:20px;color:#1491c1}  .accordion .panel-group .panel .panel-heading:hover,.wizard .wizard-body .wizard-content .accordion .panel-group .panel .panel-heading:hover{background:#f3faff;cursor:pointer}  .accordion .panel-group .panel .panel-body,.wizard .wizard-body .wizard-content .accordion .panel-group .panel .panel-body{padding:15px 10px 20px 20px}  .h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:Roboto,sans-serif}  .h1,h1{font-size:24px}  .h2,h2{font-size:18px}  .body,body{font-family:Roboto,sans-serif;font-weight:400;font-style:normal;line-height:1;color:#333;font-size:14px}  .description{font-family:Roboto,sans-serif;font-size:12px;color:#000}  a,a:focus,a:visited{color:#1491C1}  a:focus:hover,a:hover,a:visited:hover{text-decoration:underline}  a.disabled:hover,a:active,a:focus.disabled:hover,a:focus:active,a:focus[disabled]:hover,a:visited.disabled:hover,a:visited:active,a:visited[disabled]:hover,a[disabled]:hover{text-decoration:none}  a.disabled,a:focus.disabled,a:focus[disabled],a:visited.disabled,a:visited[disabled],a[disabled]{cursor:not-allowed;color:#666;text-decoration:none}
\ No newline at end of file
+.btn-group.open .dropdown-menu,.dropdown.open .dropdown-menu,.font-mixin{font-family:Roboto,sans-serif;font-weight:400;font-style:normal}.pagination-block .pagination-block-item a,.pagination-block .pagination-block-item a:focus,.pagination-block .pagination-block-item a:visited,.table.table-hover .action:hover,a,a:focus,a:visited{text-decoration:none}@font-face{font-family:Roboto;font-weight:400;font-style:normal;src:url(fonts/Roboto-Regular-webfont.eot);src:url(fonts/Roboto-Regular-webfont.eot?#iefix) format('embedded-opentype'),url(fonts/Roboto-Regular-webfont.woff) format('woff'),url(fonts/Roboto-Regular-webfont.ttf) format('truetype'),url(fonts/Roboto-Regular-webfont.svg#robotoregular) format('svg')}.font-mixin{line-height:1;color:#333}.btn,.btn:focus{outline:0;font-family:Roboto,sans-serif;text-transform:uppercase;height:34px;font-size:14px;padding:10px 20px;line-height:14px}.btn .glyphicon,.btn:focus .glyphicon{top:-1px;float:left}.box-shadow{box-shadow:0 0 2px 0 #1391c1}.btn-default-disabled,.btn-disabled{box-shadow:none;opacity:.6}.btn-default-disabled{color:#FFF;background-color:#808793;border:none}.btn-default,.btn-default:focus{color:#666;background-color:#FFF;border:1px solid #CFD3D7}.btn-default:focus:hover,.btn-default:hover{color:#FFF;background-color:#808793}.btn-default:active,.btn-default:focus:active{color:#666;background-color:#FFF;border:1px solid #CFD3D7;box-shadow:0 0 2px 0 #1391c1}.btn-default.disabled,.btn-default.disabled.active,.btn-default.disabled:active,.btn-default.disabled:hover,.btn-default:focus.disabled,.btn-default:focus.disabled.active,.btn-default:focus.disabled:active,.btn-default:focus.disabled:hover,.btn-default:focus[disabled],.btn-default:focus[disabled].active,.btn-default:focus[disabled]:active,.btn-default:focus[disabled]:hover,.btn-default[disabled],.btn-default[disabled].active,.btn-default[disabled]:active,.btn-default[disabled]:hover{opacity:.6;box-shadow:none;color:#FFF;background-color:#808793;border:none}.btn-primary-disabled{opacity:.6;box-shadow:none;color:#D1E8D1;background-color:#3FAE2A;border:1px solid #3FAE2A}.btn-primary,.btn-primary:focus{color:#FFF;background-color:#3FAE2A;border:1px solid #3FAE2A}.btn-primary:focus:hover,.btn-primary:hover{color:#FFF;background-color:#429929;border:1px solid #429929}.btn-primary.active,.btn-primary:active,.btn-primary:focus.active,.btn-primary:focus:active{color:#FFF;background-color:#3FAE2A;border:1px solid #3FAE2A;box-shadow:0 0 2px 0 #1391c1}.btn-primary.disabled,.btn-primary.disabled.active,.btn-primary.disabled:active,.btn-primary.disabled:hover,.btn-primary:focus.disabled,.btn-primary:focus.disabled.active,.btn-primary:focus.disabled:active,.btn-primary:focus.disabled:hover,.btn-primary:focus[disabled],.btn-primary:focus[disabled].active,.btn-primary:focus[disabled]:active,.btn-primary:focus[disabled]:hover,.btn-primary[disabled],.btn-primary[disabled].active,.btn-primary[disabled]:active,.btn-primary[disabled]:hover{opacity:.6;box-shadow:none;color:#D1E8D1;background-color:#3FAE2A;border:1px solid #3FAE2A}.btn-secondary-disabled{opacity:.6;box-shadow:none;color:#D1E8D1;background-color:#429929;border:1px solid #3FAE2A}.btn-secondary,.btn-secondary:focus{color:#429929;background-color:#FFF;border:1px solid #3FAE2A}.btn-secondary:focus:hover,.btn-secondary:hover{color:#FFF;background-color:#429929}.btn-secondary:active,.btn-secondary:focus:active{color:#429929;background-color:#FFF;box-shadow:0 0 2px 0 #1391c1}.btn-secondary.disabled,.btn-secondary.disabled.active,.btn-secondary.disabled:active,.btn-secondary.disabled:hover,.btn-secondary:focus.disabled,.btn-secondary:focus.disabled.active,.btn-secondary:focus.disabled:active,.btn-secondary:focus.disabled:hover,.btn-secondary:focus[disabled],.btn-secondary:focus[disabled].active,.btn-secondary:focus[disabled]:active,.btn-secondary:focus[disabled]:hover,.btn-secondary[disabled],.btn-secondary[disabled].active,.btn-secondary[disabled]:active,.btn-secondary[disabled]:hover{opacity:.6;box-shadow:none;color:#D1E8D1;background-color:#429929;border:1px solid #3FAE2A}.btn-success{border:none}.btn-regular-default-state{background-color:#FFF;color:#666;border:1px solid #cfd3d7}.btn-primary-default-state{background-color:#3FAE2A;border:1px solid #3FAE2A;color:#FFF}.btn-group.open .btn.dropdown-toggle,.dropdown.open .btn.dropdown-toggle{box-shadow:inset 0 0 3px 0 #1391c1}.btn-group.open .btn.dropdown-toggle,.btn-group.open .btn.dropdown-toggle.btn-default,.btn-group.open .btn.dropdown-toggle.btn-default:hover,.btn-group.open .btn.dropdown-toggle:hover,.dropdown.open .btn.dropdown-toggle,.dropdown.open .btn.dropdown-toggle.btn-default,.dropdown.open .btn.dropdown-toggle.btn-default:hover,.dropdown.open .btn.dropdown-toggle:hover{background-color:#FFF;color:#666;border:1px solid #cfd3d7}.btn-group.open .btn.dropdown-toggle+.dropdown-menu>li>a:hover,.btn-group.open .btn.dropdown-toggle.btn-default+.dropdown-menu>li>a:hover,.dropdown.open .btn.dropdown-toggle+.dropdown-menu>li>a:hover,.dropdown.open .btn.dropdown-toggle.btn-default+.dropdown-menu>li>a:hover{background-color:#808793;color:#FFF}.btn-group.open .btn.dropdown-toggle.btn-primary,.btn-group.open .btn.dropdown-toggle.btn-primary:hover,.dropdown.open .btn.dropdown-toggle.btn-primary,.dropdown.open .btn.dropdown-toggle.btn-primary:hover{background-color:#3FAE2A;border:1px solid #3FAE2A;color:#FFF}.btn-group.open .btn.dropdown-toggle.btn-primary+.dropdown-menu>li>a:hover,.dropdown.open .btn.dropdown-toggle.btn-primary+.dropdown-menu>li>a:hover{background-color:#429929;color:#FFF}.btn-group.open .dropdown-menu,.dropdown.open .dropdown-menu{line-height:1;border-radius:2px;font-size:14px;min-width:200px;background:#FFF;color:#666;border:1px solid #cfd3d7}.btn-group.open .dropdown-menu>li,.dropdown.open .dropdown-menu>li{margin-bottom:1px}.btn-group.open .dropdown-menu>li>a,.dropdown.open .dropdown-menu>li>a{height:24px}.btn-group .btn.dropdown-toggle:first-child,.dropdown .btn.dropdown-toggle:first-child{min-width:80px}.btn-group .btn.dropdown-toggle.disabled,.btn-group .btn.dropdown-toggle[disabled],.dropdown .btn.dropdown-toggle.disabled,.dropdown .btn.dropdown-toggle[disabled]{opacity:.6}input.form-control{font-size:14px;border-radius:2px;color:#666;border:1px solid #CFD3D7;height:34px;padding:10px}input.form-control:focus{border-color:#1291c1;box-shadow:none}.help-block{color:#999;font-size:14px}.help-block.validation-block{color:#999;margin-top:10px}.help-block.validation-block::before{position:relative;top:2px;margin-right:5px;font-family:'Glyphicons Halflings'}.wizard .wizard-body .wizard-content .step-description,.wizard .wizard-body .wizard-content .step-header,.wizard .wizard-body .wizard-content .step-title,.wizard .wizard-header h3,h2.table-title{font-family:Roboto,sans-serif;font-weight:400;font-style:normal}.has-success input.form-control{color:#666;border:1px solid #1EB475}.has-success input.form-control:focus{border-color:#1EB475;box-shadow:none}.has-success .help-block.validation-block::before{content:'\e084';color:#1EB475}.has-error input.form-control{color:#666;border:1px solid #EF6162}.has-error input.form-control:focus{border-color:#EF6162;box-shadow:none}.has-error .help-block.validation-block::before{content:'\e083';color:#EF6162}.has-warning input.form-control{color:#666;border:1px solid #E98A40}.has-warning input.form-control:focus{border-color:#E98A40;box-shadow:none}.has-warning .help-block.validation-block::before{content:'\e101';color:#E98A40}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{color:#999;border-color:#ccc;background-color:#ddd}h2.table-title{line-height:1;color:#333;margin-top:10px;font-size:20px}.table{color:#666;font-size:13px}.table tfoot,.table thead{color:#999}.table input[type=checkbox]+label{position:relative;line-height:1.3em;font-size:initial;top:4px;margin-bottom:0}.table thead>tr>th{border-bottom-color:#EEE}.table tfoot>tr:first-of-type>td{border-top-width:2px;border-top-color:#EEE}.table>tbody>tr>td{border-top-color:#EEE}.table>tbody>tr.active,.table>tbody>tr.active>td{background-color:#EEE}.table.table-hover .action{visibility:hidden;padding:0;line-height:1}.table.table-hover>tbody>tr{border-width:0 1px 1px;border-style:solid;border-color:#EEE transparent}.table.table-hover>tbody>tr>td{border-width:0}.table.table-hover>tbody>tr:hover{border-color:#A7DFF2;background-color:#E7F6FC}.table.table-hover>tbody>tr:hover>td{border-top:1px solid #A7DFF2;background-color:#E7F6FC}.table.table-hover>tbody>tr:hover>td .action{visibility:visible}.table.table-hover>tbody>tr:first-of-type>td{border-top:1px solid transparent}.table.table-hover>tbody>tr:first-of-type:hover>td{border-color:#A7DFF2}.pagination-block .pagination-block-item{float:left;padding:0 5px}.pagination-block .pagination-block-item select{border:none;background-color:transparent;color:#1491C1}.nav.nav-tabs{border:none;margin-bottom:20px}.nav.nav-tabs li a{border-width:0;border-radius:0;border-bottom:3px solid transparent;color:#6B6C6C;text-transform:uppercase}.nav.nav-tabs li a:active,.nav.nav-tabs li a:focus,.nav.nav-tabs li a:hover{color:#333;border-top-width:0;border-left-width:0;border-right-width:0;background:0 0}.nav.nav-tabs li a .badge.badge-important{display:inline;vertical-align:baseline}.nav.nav-tabs li.active a{color:#333;border-bottom:3px solid #3FAE2A;padding-bottom:2px}.nav-tabs-left li,.nav-tabs-right li{float:none;margin-bottom:3px}.nav-tabs-left li a,.nav-tabs-right li a{margin-right:0}.nav-tabs-left li{margin-right:-1px}.nav-tabs-left li a{border:3px solid transparent!important}.nav-tabs-left li.active a,.nav-tabs-left li.active a:active,.nav-tabs-left li.active a:focus,.nav-tabs-left li.active a:hover{border-right:3px solid #3FAE2A!important}.nav-tabs-right li{margin-left:-1px}.nav-tabs-right li a{border:3px solid transparent!important}.nav-tabs-right li.active a,.nav-tabs-right li.active a:active,.nav-tabs-right li.active a:focus,.nav-tabs-right li.active a:hover{border-left:3px solid #3FAE2A!important}.wizard{border:2px solid #ebecf1}.wizard .wizard-header h3{line-height:1;font-size:20px;color:#333;margin:15px 20px}.wizard .wizard-body{overflow:hidden;margin:0}.wizard .wizard-body .wizard-content{background:#ebecf1;padding-top:15px;float:left;margin-bottom:-99999px;padding-bottom:99999px}.wizard .wizard-body .wizard-content .step-header{color:#666;font-size:14px;line-height:1;margin-bottom:5px}.wizard .wizard-body .wizard-content .step-title{line-height:1;color:#333;font-size:16px}.wizard .wizard-body .wizard-content .step-description{font-size:12px;line-height:1.4;color:#999}.wizard .wizard-body .wizard-content .panel.panel-default{border:none;box-shadow:none;margin-top:20px}.wizard .wizard-body .wizard-content .panel.panel-default .panel-body{padding:10px 20px}.wizard .wizard-body .wizard-nav{min-height:550px;padding-top:25px;background-color:#323544;float:left;margin-bottom:-99999px;padding-bottom:99999px}.wizard .wizard-body .wizard-nav .nav li{padding:0 15px}.wizard .wizard-body .wizard-nav .nav li a{height:48px;padding:0 5px;display:table-cell;vertical-align:middle}.wizard .wizard-body .wizard-nav .nav li .step-marker{position:absolute;top:9px;line-height:16px;text-align:center;width:20px;height:20px;border:2px solid #1EB475;border-radius:50%;font-size:12px;font-style:inherit;color:#1EB475;background-color:#323544}.wizard .wizard-body .wizard-nav .nav li .step-description,.wizard .wizard-body .wizard-nav .nav li .step-name{font-family:Roboto,sans-serif;font-weight:400;color:#999;margin-left:30px;font-style:normal}.wizard .wizard-body .wizard-nav .nav li .step-name{line-height:1;font-size:14px}.wizard .wizard-body .wizard-nav .nav li .step-index{line-height:18px}.wizard .wizard-body .wizard-nav .nav li .step-description{line-height:1;font-size:12px}.wizard .wizard-body .wizard-nav .nav li.completed .step-marker{background-color:#1EB475;color:#fff;font-size:10px;padding-left:2px}.wizard .wizard-body .wizard-nav .nav li.completed .step-marker .step-index{display:none}.wizard .wizard-body .wizard-nav .nav li.completed .step-marker:after{font-family:"Glyphicons Halflings";content:"\e013";position:relative;top:1px;left:-1px}.wizard .wizard-body .wizard-nav .nav li.completed:after{width:2px;height:100%;position:absolute;background-color:#1EB475;content:"";top:25px;left:29px}.wizard .wizard-body .wizard-nav .nav li.completed:last-child:after{content:none}.wizard .wizard-body .wizard-nav .nav li.active .step-name{font-weight:700}.wizard .wizard-body .wizard-nav .nav li.disabled .step-marker{color:#666;border-color:#666}.wizard .wizard-body .wizard-nav .nav li.disabled .step-description,.wizard .wizard-body .wizard-nav .nav li.disabled .step-name{color:#666}.wizard .wizard-body .wizard-nav .nav li.disabled.completed .step-marker{background-color:#1EB475;border:2px solid #1EB475;color:#fff}.wizard .wizard-body .wizard-nav .nav-pills>li.active>a,.wizard .wizard-body .wizard-nav .nav-pills>li.active>a:focus,.wizard .wizard-body .wizard-nav .nav-pills>li.active>a:hover,.wizard .wizard-body .wizard-nav .nav>li>a:focus,.wizard .wizard-body .wizard-nav .nav>li>a:hover{background-color:inherit}.wizard .wizard-body .wizard-footer{background:#fff;padding:15px 20px}.wizard .wizard-body .wizard-footer button{margin:0 10px}.checkbox-disabled-style{background-color:#b2b8c1;border-color:#b2b8c1}input[type=radio]:checked,input[type=radio]:not(:checked),input[type=checkbox]:checked,input[type=checkbox]:not(:checked){display:none}input[type=radio]:checked+label,input[type=radio]:not(:checked)+label,input[type=checkbox]:checked+label,input[type=checkbox]:not(:checked)+label{position:relative;padding-left:20px}input[type=radio]:checked+label:hover:before,input[type=radio]:not(:checked)+label:hover:before,input[type=checkbox]:checked+label:hover:before,input[type=checkbox]:not(:checked)+label:hover:before{border-color:#1491C1;background-color:#1491C1}input[type=radio]:checked+label:before,input[type=checkbox]:checked+label:before{background-color:#1491C1;border-color:#1491C1}input[type=radio].disabled+label:before,input[type=radio].disabled+label:hover:before,input[type=radio][disabled]+label:before,input[type=radio][disabled]+label:hover:before,input[type=checkbox].disabled+label:before,input[type=checkbox].disabled+label:hover:before,input[type=checkbox][disabled]+label:before,input[type=checkbox][disabled]+label:hover:before{background-color:#b2b8c1;border-color:#b2b8c1}input[type=checkbox]+label:before{content:'';position:absolute;left:0;top:4px;width:10px;height:10px;-moz-box-sizing:border-box;box-sizing:border-box;border-radius:2px;border-width:1px;border-style:solid;border-color:#ddd}input[type=checkbox]:checked+label:after{content:'\2714';color:#FFF;position:absolute;top:0;left:2px;font-size:9px}input.radio+label:before,input[type=radio]+label:before{content:'';position:absolute;left:0;top:0;width:12px;height:12px;-moz-box-sizing:border-box;box-sizing:border-box;border-radius:12px;border-width:1px;border-style:solid;border-color:#ddd}input.radio:checked+label:after,input[type=radio]:checked+label:after{content:'';background-color:#FFF;position:absolute;top:3px;left:3px;width:6px;height:6px;border-radius:6px}.navigation-bar-container,.navigation-bar-container ul.nav.side-nav-header{width:230px;transition:width .5s ease-out}.navigation-bar-container{height:auto;background-color:#323544;padding:0;-ms-overflow-style:none;-webkit-font-smoothing:antialiased}.navigation-bar-container ul.nav.side-nav-header li.navigation-header{background:#313d54;padding:15px 5px 15px 25px;height:55px}.navigation-bar-container ul.nav.side-nav-header li.navigation-header>a.ambari-logo{padding:0}.navigation-bar-container ul.nav.side-nav-header li.navigation-header>a.ambari-logo>img{height:25px;float:left;margin-left:-3px}.navigation-bar-container ul.nav.side-nav-header li.navigation-header .btn-group{cursor:pointer;margin-top:3px}.navigation-bar-container ul.nav.side-nav-header li.navigation-header .btn-group:hover span.ambari-header{color:#fff}.navigation-bar-container ul.nav.side-nav-header li.navigation-header .btn-group span.ambari-header{font-family:Roboto,sans-serif;font-weight:400;font-style:normal;line-height:1;font-size:20px;width:55px;display:inline;color:#b8bec4;padding:0 8px 0 10px}.navigation-bar-container ul.nav.side-nav-header li.navigation-header .btn-group span.toggle-icon{margin-bottom:5px;font-size:13px;display:inline-block;vertical-align:middle;color:#43AD49}.navigation-bar-container ul.nav.side-nav-header li.navigation-header .btn-group.open .dropdown-toggle{box-shadow:none}.navigation-bar-container ul.nav.side-nav-header li.navigation-header ul.dropdown-menu{top:30px}.navigation-bar-container ul.nav.side-nav-header li.navigation-header ul.dropdown-menu li>a{font-family:Roboto,sans-serif;font-weight:400;font-style:normal;font-size:14px;color:#666;line-height:1.42;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.navigation-bar-container ul.nav.side-nav-header li.navigation-header ul.dropdown-menu li>a:hover{background:#f5f5f5}.navigation-bar-container ul.nav.side-nav-footer,.navigation-bar-container ul.nav.side-nav-menu{background-color:#323544;width:230px;transition:width .5s ease-out}.navigation-bar-container ul.nav.side-nav-footer li,.navigation-bar-container ul.nav.side-nav-menu li{padding:0;margin:0}.navigation-bar-container ul.nav.side-nav-footer li.mainmenu-li>a,.navigation-bar-container ul.nav.side-nav-footer li.navigation-footer>a,.navigation-bar-container ul.nav.side-nav-footer li.submenu-li>a,.navigation-bar-container ul.nav.side-nav-menu li.mainmenu-li>a,.navigation-bar-container ul.nav.side-nav-menu li.navigation-footer>a,.navigation-bar-container ul.nav.side-nav-menu li.submenu-li>a{display:table-cell;vertical-align:middle;width:230px;border-radius:0;-moz-border-radius:0;-webkit-border-radius:0;white-space:nowrap}.navigation-bar-container ul.nav.side-nav-footer li.mainmenu-li>a .navigation-menu-item,.navigation-bar-container ul.nav.side-nav-footer li.navigation-footer>a .navigation-menu-item,.navigation-bar-container ul.nav.side-nav-footer li.submenu-li>a .navigation-menu-item,.navigation-bar-container ul.nav.side-nav-menu li.mainmenu-li>a .navigation-menu-item,.navigation-bar-container ul.nav.side-nav-menu li.navigation-footer>a .navigation-menu-item,.navigation-bar-container ul.nav.side-nav-menu li.submenu-li>a .navigation-menu-item{font-family:Roboto,sans-serif;font-weight:400;font-style:normal;line-height:1;font-size:14px;color:#b8bec4;padding-left:8px}.navigation-bar-container ul.nav.side-nav-footer li.mainmenu-li>a .navigation-icon,.navigation-bar-container ul.nav.side-nav-footer li.navigation-footer>a .navigation-icon,.navigation-bar-container ul.nav.side-nav-footer li.submenu-li>a .navigation-icon,.navigation-bar-container ul.nav.side-nav-menu li.mainmenu-li>a .navigation-icon,.navigation-bar-container ul.nav.side-nav-menu li.navigation-footer>a .navigation-icon,.navigation-bar-container ul.nav.side-nav-menu li.submenu-li>a .navigation-icon{line-height:18px;font-size:16px;color:#b8bec4}.navigation-bar-container ul.nav.side-nav-footer li.mainmenu-li>a .toggle-icon,.navigation-bar-container ul.nav.side-nav-footer li.navigation-footer>a .toggle-icon,.navigation-bar-container ul.nav.side-nav-footer li.submenu-li>a .toggle-icon,.navigation-bar-container ul.nav.side-nav-menu li.mainmenu-li>a .toggle-icon,.navigation-bar-container ul.nav.side-nav-menu li.navigation-footer>a .toggle-icon,.navigation-bar-container ul.nav.side-nav-menu li.submenu-li>a .toggle-icon{line-height:14px;font-size:14px;color:#b8bec4;padding:3px 5px 3px 10px}.navigation-bar-container ul.nav.side-nav-footer li.mainmenu-li>a,.navigation-bar-container ul.nav.side-nav-menu li.mainmenu-li>a{padding:10px 5px 10px 20px}.navigation-bar-container ul.nav.side-nav-footer li.navigation-footer>a,.navigation-bar-container ul.nav.side-nav-menu li.navigation-footer>a{padding:14px 5px 14px 20px}.navigation-bar-container ul.nav.side-nav-footer li.submenu-li>a,.navigation-bar-container ul.nav.side-nav-menu li.submenu-li>a{padding:10px 5px 10px 25px}.navigation-bar-container ul.nav.side-nav-footer li.navigation-footer,.navigation-bar-container ul.nav.side-nav-menu li.navigation-footer{background:#313d54;height:48px}.navigation-bar-container ul.nav.side-nav-footer li.navigation-footer a .navigation-icon,.navigation-bar-container ul.nav.side-nav-menu li.navigation-footer a .navigation-icon{color:#3fae2a;font-size:19px;position:relative;padding:0 15px;left:calc(30%)}.navigation-bar-container ul.nav.side-nav-footer li.navigation-footer a .navigation-icon:hover,.navigation-bar-container ul.nav.side-nav-menu li.navigation-footer a .navigation-icon:hover{color:#fff}.navigation-bar-container ul.nav.side-nav-footer li>ul>li,.navigation-bar-container ul.nav.side-nav-menu li>ul>li{background-color:#323544}.navigation-bar-container ul.nav.side-nav-footer li>ul>li a,.navigation-bar-container ul.nav.side-nav-menu li>ul>li a{font-family:Roboto,sans-serif;font-weight:400;font-style:normal;line-height:1;font-size:14px;color:#999}.navigation-bar-container ul.nav.side-nav-footer li>ul>li a .submenu-icon,.navigation-bar-container ul.nav.side-nav-menu li>ul>li a .submenu-icon{line-height:14px;font-size:14px}.navigation-bar-container ul.nav.side-nav-footer li>a:hover,.navigation-bar-container ul.nav.side-nav-footer li>ul>li>a:hover,.navigation-bar-container ul.nav.side-nav-menu li>a:hover,.navigation-bar-container ul.nav.side-nav-menu li>ul>li>a:hover{background:#404351;cursor:pointer;color:#fff}.navigation-bar-container ul.nav.side-nav-footer li>a:hover .navigation-icon,.navigation-bar-container ul.nav.side-nav-footer li>a:hover .navigation-menu-item,.navigation-bar-container ul.nav.side-nav-footer li>a:hover .submenu-item,.navigation-bar-container ul.nav.side-nav-footer li>a:hover .toggle-icon,.navigation-bar-container ul.nav.side-nav-footer li>ul>li>a:hover .navigation-icon,.navigation-bar-container ul.nav.side-nav-footer li>ul>li>a:hover .navigation-menu-item,.navigation-bar-container ul.nav.side-nav-footer li>ul>li>a:hover .submenu-item,.navigation-bar-container ul.nav.side-nav-footer li>ul>li>a:hover .toggle-icon,.navigation-bar-container ul.nav.side-nav-menu li>a:hover .navigation-icon,.navigation-bar-container ul.nav.side-nav-menu li>a:hover .navigation-menu-item,.navigation-bar-container ul.nav.side-nav-menu li>a:hover .submenu-item,.navigation-bar-container ul.nav.side-nav-menu li>a:hover .toggle-icon,.navigation-bar-container ul.nav.side-nav-menu li>ul>li>a:hover .navigation-icon,.navigation-bar-container ul.nav.side-nav-menu li>ul>li>a:hover .navigation-menu-item,.navigation-bar-container ul.nav.side-nav-menu li>ul>li>a:hover .submenu-item,.navigation-bar-container ul.nav.side-nav-menu li>ul>li>a:hover .toggle-icon{color:#fff}.navigation-bar-container ul.nav.side-nav-footer li.active.collapsed,.navigation-bar-container ul.nav.side-nav-footer li.active:not(.has-sub-menu),.navigation-bar-container ul.nav.side-nav-menu li.active.collapsed,.navigation-bar-container ul.nav.side-nav-menu li.active:not(.has-sub-menu){background:#404351;cursor:pointer}.navigation-bar-container ul.nav.side-nav-footer .more-actions .dropdown-menu>li>a.disabled:hover,.navigation-bar-container ul.nav.side-nav-footer .more-actions .dropdown-menu>li>a:hover,.navigation-bar-container ul.nav.side-nav-menu .more-actions .dropdown-menu>li>a.disabled:hover,.navigation-bar-container ul.nav.side-nav-menu .more-actions .dropdown-menu>li>a:hover{background:#f5f5f5}.navigation-bar-container ul.nav.side-nav-footer li.active.collapsed>a,.navigation-bar-container ul.nav.side-nav-footer li.active:not(.has-sub-menu)>a,.navigation-bar-container ul.nav.side-nav-menu li.active.collapsed>a,.navigation-bar-container ul.nav.side-nav-menu li.active:not(.has-sub-menu)>a{color:#fff}.navigation-bar-container ul.nav.side-nav-footer li.active.collapsed>a .navigation-icon,.navigation-bar-container ul.nav.side-nav-footer li.active.collapsed>a .navigation-menu-item,.navigation-bar-container ul.nav.side-nav-footer li.active.collapsed>a .submenu-item,.navigation-bar-container ul.nav.side-nav-footer li.active.collapsed>a .toggle-icon,.navigation-bar-container ul.nav.side-nav-footer li.active:not(.has-sub-menu)>a .navigation-icon,.navigation-bar-container ul.nav.side-nav-footer li.active:not(.has-sub-menu)>a .navigation-menu-item,.navigation-bar-container ul.nav.side-nav-footer li.active:not(.has-sub-menu)>a .submenu-item,.navigation-bar-container ul.nav.side-nav-footer li.active:not(.has-sub-menu)>a .toggle-icon,.navigation-bar-container ul.nav.side-nav-menu li.active.collapsed>a .navigation-icon,.navigation-bar-container ul.nav.side-nav-menu li.active.collapsed>a .navigation-menu-item,.navigation-bar-container ul.nav.side-nav-menu li.active.collapsed>a .submenu-item,.navigation-bar-container ul.nav.side-nav-menu li.active.collapsed>a .toggle-icon,.navigation-bar-container ul.nav.side-nav-menu li.active:not(.has-sub-menu)>a .navigation-icon,.navigation-bar-container ul.nav.side-nav-menu li.active:not(.has-sub-menu)>a .navigation-menu-item,.navigation-bar-container ul.nav.side-nav-menu li.active:not(.has-sub-menu)>a .submenu-item,.navigation-bar-container ul.nav.side-nav-menu li.active:not(.has-sub-menu)>a .toggle-icon{color:#fff}.navigation-bar-container ul.nav.side-nav-footer li.active.collapsed>a:after,.navigation-bar-container ul.nav.side-nav-footer li.active:not(.has-sub-menu)>a:after,.navigation-bar-container ul.nav.side-nav-menu li.active.collapsed>a:after,.navigation-bar-container ul.nav.side-nav-menu li.active:not(.has-sub-menu)>a:after{left:0;top:50%;border:solid transparent;border-width:10px 7px;content:" ";height:0;width:0;position:absolute;pointer-events:none;border-color:transparent transparent transparent #3fae2a;margin-top:-12px}.navigation-bar-container ul.nav.side-nav-footer .more-actions,.navigation-bar-container ul.nav.side-nav-menu .more-actions{display:block;position:absolute;top:14px;right:33px;line-height:25px;width:20px;text-align:center;font-size:14px;cursor:pointer;vertical-align:middle;color:#fff}.navigation-bar-container.collapsed,.navigation-bar-container.collapsed ul.nav.side-nav-footer,.navigation-bar-container.collapsed ul.nav.side-nav-header,.navigation-bar-container.collapsed ul.nav.side-nav-menu{width:50px}.navigation-bar-container ul.nav.side-nav-footer .more-actions .dropdown-menu>li>a,.navigation-bar-container ul.nav.side-nav-footer .more-actions .dropdown-menu>li>a i,.navigation-bar-container ul.nav.side-nav-menu .more-actions .dropdown-menu>li>a,.navigation-bar-container ul.nav.side-nav-menu .more-actions .dropdown-menu>li>a i{color:#333}.navigation-bar-container ul.nav.side-nav-footer .more-actions .dropdown-menu>li>a.disabled,.navigation-bar-container ul.nav.side-nav-footer .more-actions .dropdown-menu>li>a.disabled i,.navigation-bar-container ul.nav.side-nav-menu .more-actions .dropdown-menu>li>a.disabled,.navigation-bar-container ul.nav.side-nav-menu .more-actions .dropdown-menu>li>a.disabled i{color:#666}.navigation-bar-container ul.nav.side-nav-footer .menu-item-name,.navigation-bar-container ul.nav.side-nav-menu .menu-item-name{display:inline-block;vertical-align:bottom;max-width:100px;overflow:hidden;text-overflow:ellipsis;-o-text-overflow:ellipsis;-ms-text-overflow:ellipsis;white-space:nowrap}.navigation-bar-container.collapsed ul.nav.side-nav-footer .more-actions,.navigation-bar-container.collapsed ul.nav.side-nav-footer li a .navigation-menu-item,.navigation-bar-container.collapsed ul.nav.side-nav-footer li a .toggle-icon,.navigation-bar-container.collapsed ul.nav.side-nav-header li.navigation-header .dropdown-menu,.navigation-bar-container.collapsed ul.nav.side-nav-header li.navigation-header span.ambari-header,.navigation-bar-container.collapsed ul.nav.side-nav-header li.navigation-header span.toggle-icon,.navigation-bar-container.collapsed ul.nav.side-nav-menu .more-actions,.navigation-bar-container.collapsed ul.nav.side-nav-menu li a .navigation-menu-item,.navigation-bar-container.collapsed ul.nav.side-nav-menu li a .toggle-icon{display:none}.navigation-bar-container .nav-pills>li.active>a,.navigation-bar-container .nav-pills>li.active>a:focus,.navigation-bar-container .nav-pills>li.active>a:hover,.navigation-bar-container .nav>li>a:focus,.navigation-bar-container .nav>li>a:hover{background-color:inherit}.navigation-bar-container.collapsed ul.nav.side-nav-header li.navigation-header{padding:15px 0 15px 15px}.navigation-bar-container.collapsed ul.nav.side-nav-footer li a,.navigation-bar-container.collapsed ul.nav.side-nav-menu li a{padding:15px 0 15px 15px;width:50px}.navigation-bar-container.collapsed ul.nav.side-nav-footer li a .navigation-icon,.navigation-bar-container.collapsed ul.nav.side-nav-menu li a .navigation-icon{font-size:19px}.navigation-bar-container.collapsed ul.nav.side-nav-footer li.navigation-footer a .navigation-icon,.navigation-bar-container.collapsed ul.nav.side-nav-menu li.navigation-footer a .navigation-icon{padding:0 5px;left:0}.navigation-bar-container.collapsed ul.nav.side-nav-footer li ul.sub-menu,.navigation-bar-container.collapsed ul.nav.side-nav-menu li ul.sub-menu{display:none;width:230px;position:absolute;z-index:100;top:0;left:50px}.navigation-bar-container.collapsed ul.nav.side-nav-footer li.submenu-li>a,.navigation-bar-container.collapsed ul.nav.side-nav-menu li.submenu-li>a{padding:10px 5px 10px 25px;width:230px}.navigation-bar-container.collapsed ul.nav.side-nav-footer li.active,.navigation-bar-container.collapsed ul.nav.side-nav-menu li.active{background:#404351;cursor:pointer}.navigation-bar-container.collapsed ul.nav.side-nav-footer li.active>a,.navigation-bar-container.collapsed ul.nav.side-nav-footer li.active>a .navigation-icon,.navigation-bar-container.collapsed ul.nav.side-nav-footer li.active>a .navigation-menu-item,.navigation-bar-container.collapsed ul.nav.side-nav-footer li.active>a .submenu-item,.navigation-bar-container.collapsed ul.nav.side-nav-footer li.active>a .toggle-icon,.navigation-bar-container.collapsed ul.nav.side-nav-menu li.active>a,.navigation-bar-container.collapsed ul.nav.side-nav-menu li.active>a .navigation-icon,.navigation-bar-container.collapsed ul.nav.side-nav-menu li.active>a .navigation-menu-item,.navigation-bar-container.collapsed ul.nav.side-nav-menu li.active>a .submenu-item,.navigation-bar-container.collapsed ul.nav.side-nav-menu li.active>a .toggle-icon{color:#fff}.navigation-bar-container.collapsed ul.nav.side-nav-footer li.active>a:after,.navigation-bar-container.collapsed ul.nav.side-nav-menu li.active>a:after{left:0;top:50%;border:solid transparent;border-width:12px 6px;content:" ";height:0;width:0;position:absolute;pointer-events:none;border-color:transparent transparent transparent #3fae2a;margin-top:-12px}.navigation-bar-fit-height{position:fixed;top:0;bottom:0;left:0;z-index:2079}.navigation-bar-fit-height .side-nav-header{position:absolute;top:0}.navigation-bar-fit-height .side-nav-menu{position:absolute;top:55px;bottom:50px}.navigation-bar-fit-height .side-nav-footer{position:absolute;bottom:0}.navigation-bar-fit-height .more-actions .dropdown-menu{position:fixed;top:auto;left:auto}.navigation-bar-fit-height .navigation-bar-container{height:100%}.navigation-bar-fit-height .navigation-bar-container .side-nav-menu{overflow-y:auto}.notifications-group{position:relative;top:1px}#notifications-dropdown.dropdown-menu,.notifications-dropdown{min-width:400px;max-width:400px;min-height:150px;padding:0;z-index:1000;right:-50px;left:auto;top:260%;border:none;box-shadow:0 2px 10px 2px rgba(0,0,0,.29)}#notifications-dropdown.dropdown-menu .popup-arrow-up,.notifications-dropdown .popup-arrow-up{position:absolute;right:37px;top:-40px;width:40px;height:40px;overflow:hidden}#notifications-dropdown.dropdown-menu .popup-arrow-up:after,.notifications-dropdown .popup-arrow-up:after{content:"";position:absolute;width:20px;height:20px;background:#fff;-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg);top:30px;left:10px;box-shadow:-1px -1px 10px -2px rgba(0,0,0,.5)}#notifications-dropdown.dropdown-menu .notifications-header,.notifications-dropdown .notifications-header{border-bottom:1px solid #eee;padding:15px 20px}#notifications-dropdown.dropdown-menu .notifications-header .notifications-title,.notifications-dropdown .notifications-header .notifications-title{font-family:Roboto,sans-serif;font-weight:400;font-style:normal;line-height:1;color:#333;font-size:16px}#notifications-dropdown.dropdown-menu .notifications-body,.notifications-dropdown .notifications-body{padding:0 15px;overflow:auto;max-height:500px}#notifications-dropdown.dropdown-menu .notifications-body .no-alert-text,.notifications-dropdown .notifications-body .no-alert-text{padding:15px 5px}#notifications-dropdown.dropdown-menu .notifications-body .table-controls,.notifications-dropdown .notifications-body .table-controls{padding:10px 0;margin:0;border-bottom:1px solid #eee}#notifications-dropdown.dropdown-menu .notifications-body .table-controls .state-filter,.notifications-dropdown .notifications-body .table-controls .state-filter{padding:0;font-family:Roboto,sans-serif;font-weight:400;font-style:normal;line-height:1;font-size:12px;color:#666;position:relative}#notifications-dropdown.dropdown-menu .notifications-body .table-controls .state-filter .form-control.filter-select,.notifications-dropdown .notifications-body .table-controls .state-filter .form-control.filter-select{font-size:12px;color:#666;height:25px}#notifications-dropdown.dropdown-menu .notifications-body .table.alerts-table,.notifications-dropdown .notifications-body .table.alerts-table{margin-top:0}#notifications-dropdown.dropdown-menu .notifications-body .table.alerts-table tbody tr,.notifications-dropdown .notifications-body .table.alerts-table tbody tr{cursor:pointer}#notifications-dropdown.dropdown-menu .notifications-body .table.alerts-table tbody tr.no-alert-tr:hover,.notifications-dropdown .notifications-body .table.alerts-table tbody tr.no-alert-tr:hover{cursor:default;border-color:transparent transparent #eee}#notifications-dropdown.dropdown-menu .notifications-body .table.alerts-table tbody tr.no-alert-tr:hover>td,.notifications-dropdown .notifications-body .table.alerts-table tbody tr.no-alert-tr:hover>td{border-color:transparent;background-color:#fff}#notifications-dropdown.dropdown-menu .notifications-body .table.alerts-table tbody td.status,.notifications-dropdown .notifications-body .table.alerts-table tbody td.status{width:9%;padding:15px 3px}#notifications-dropdown.dropdown-menu .notifications-body .table.alerts-table tbody td.status .alert-state-CRITICAL,.notifications-dropdown .notifications-body .table.alerts-table tbody td.status .alert-state-CRITICAL{color:#EF6162}#notifications-dropdown.dropdown-menu .notifications-body .table.alerts-table tbody td.status .alert-state-WARNING,.notifications-dropdown .notifications-body .table.alerts-table tbody td.status .alert-state-WARNING{color:#E98A40}#notifications-dropdown.dropdown-menu .notifications-body .table.alerts-table tbody td.content,.notifications-dropdown .notifications-body .table.alerts-table tbody td.content{width:90%;padding:15px 3px 10px;font-family:Roboto,sans-serif;font-weight:400;font-style:normal;color:#333;line-height:1.3}#notifications-dropdown.dropdown-menu .notifications-body .table.alerts-table tbody td.content .name,.notifications-dropdown .notifications-body .table.alerts-table tbody td.content .name{font-weight:700;font-size:14px;color:#333;margin-bottom:5px}#notifications-dropdown.dropdown-menu .notifications-body .table.alerts-table tbody td.content .description,.notifications-dropdown .notifications-body .table.alerts-table tbody td.content .description{font-size:12px;color:#666;margin-bottom:4px;display:block;display:-webkit-box;-webkit-line-clamp:3;max-height:47px;-webkit-box-orient:vertical;overflow:hidden;text-overflow:ellipsis;overflow-wrap:break-word;word-wrap:break-word;-ms-word-break:break-all;word-break:break-all;word-break:break-word;-ms-hyphens:auto;-moz-hyphens:auto;-webkit-hyphens:auto;hyphens:auto}#notifications-dropdown.dropdown-menu .notifications-body .table.alerts-table tbody td.content .timestamp,.notifications-dropdown .notifications-body .table.alerts-table tbody td.content .timestamp{text-align:right;font-size:11px;color:#999}#notifications-dropdown.dropdown-menu .notifications-footer,.notifications-dropdown .notifications-footer{border-top:1px solid #eee;padding:15px}.modal-backdrop{background-color:grey}.modal .modal-content{border-radius:2px}.modal .modal-content .modal-body,.modal .modal-content .modal-footer,.modal .modal-content .modal-header{padding-left:20px;padding-right:20px}.modal .modal-content .modal-header{border-bottom:none;padding-top:20px;color:#666;font-size:20px}.modal .modal-content .modal-header h4{margin:0;color:inherit;font-size:inherit}.modal .modal-content .modal-body{color:#666;font-size:12px}.modal .modal-content .modal-footer{border-top:none;padding-bottom:20px}.modal .modal-content .modal-footer .btn~.btn{margin-left:10px}.accordion .panel-group,.wizard .wizard-body .wizard-content .accordion .panel-group{margin-bottom:0}.accordion .panel-group .panel,.wizard .wizard-body .wizard-content .accordion .panel-group .panel{border-radius:0;border:none;margin-top:0;padding:0 10px}.accordion .panel-group .panel .panel-heading,.wizard .wizard-body .wizard-content .accordion .panel-group .panel .panel-heading{height:50px;padding:15px 10px;border:1px solid;border-color:#ddd transparent;border-top:none;background:#fff}.accordion .panel-group .panel .panel-heading .panel-title,.wizard .wizard-body .wizard-content .accordion .panel-group .panel .panel-heading .panel-title{font-family:Roboto,sans-serif;font-weight:400;font-style:normal;line-height:1;color:#333}.accordion .panel-group .panel .panel-heading .panel-title>a,.wizard .wizard-body .wizard-content .accordion .panel-group .panel .panel-heading .panel-title>a{font-size:18px;color:#333}.accordion .panel-group .panel .panel-heading .panel-title>i,.wizard .wizard-body .wizard-content .accordion .panel-group .panel .panel-heading .panel-title>i{font-size:20px;color:#1491c1}.accordion .panel-group .panel .panel-heading:hover,.wizard .wizard-body .wizard-content .accordion .panel-group .panel .panel-heading:hover{background:#f3faff;cursor:pointer}.accordion .panel-group .panel .panel-body,.wizard .wizard-body .wizard-content .accordion .panel-group .panel .panel-body{padding:15px 10px 20px 20px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:Roboto,sans-serif}.h1,h1{font-size:24px}.h2,h2{font-size:18px}.body,body{font-family:Roboto,sans-serif;font-weight:400;font-style:normal;line-height:1;color:#333;font-size:14px}.description{font-family:Roboto,sans-serif;font-size:12px;color:#000}a,a:focus,a:visited{color:#1491C1}a:focus:hover,a:hover,a:visited:hover{text-decoration:underline}a.disabled:hover,a:active,a:focus.disabled:hover,a:focus:active,a:focus[disabled]:hover,a:visited.disabled:hover,a:visited:active,a:visited[disabled]:hover,a[disabled]:hover{text-decoration:none}a.disabled,a:focus.disabled,a:focus[disabled],a:visited.disabled,a:visited[disabled],a[disabled]{cursor:not-allowed;color:#666;text-decoration:none}
\ No newline at end of file
index a1b68d2..333e65d 100644 (file)
@@ -16,4 +16,4 @@
  * limitations under the License.
  */
 "use strict";$(document).ready(function(){var n=$(this).find('[data-toggle="collapseAccordion"]');n.off("click").on("click",function(n){var l=$(this);return l.siblings(".panel-body").slideToggle(500),l.children().children(".panel-toggle").toggleClass("fa-angle-down fa-angle-up"),n.stopPropagation(),!1})});
-"use strict";!function(e){e.fn.navigationBar=function(n){var t=e.extend({},e.fn.navigationBar.defaults,n);return this.each(function(){function n(){var n=window.location.pathname+window.location.hash;o.find("li a").each(function(t,a){var i=e(a),s=i.attr("data-href")||i.attr("href");n.indexOf(s)!==-1&&["","#"].indexOf(s)===-1?i.parent().addClass("active"):i.parent().removeClass("active")})}function a(n){var a=e(n).parent(),i=t.activeClass,s=f+"."+i,l=c+"."+i;o.find(s).removeClass(i),o.find(l).removeClass(i),a.addClass(i)}var i=this,s=".navigation-bar-container",o=e(this).find(s),l=e(this).find("[data-toggle="+t.navBarToggleDataAttr+"]"),r=e(this).find("[data-toggle="+t.subMenuNavToggleDataAttr+"]"),f=".side-nav-menu>li",c=".side-nav-menu>li>ul>li",d=e(this).find(".more-actions"),u=d.children(".dropdown-menu");r.each(function(n,t){return e(t).parent().addClass("has-sub-menu")}),t.fitHeight&&(e(this).addClass("navigation-bar-fit-height"),e(this).find(".side-nav-menu").on("DOMMouseScroll mousewheel",function(n){var t=e(this),a=this.scrollTop,i=this.scrollHeight,s=t.innerHeight(),o=n.originalEvent.wheelDelta,l=o>0,r=function(){return n.stopPropagation(),n.preventDefault(),n.returnValue=!1,!1};return!l&&-o>i-s-a?(t.scrollTop(i),r()):l&&o>a?(t.scrollTop(0),r()):void 0}));var h=o.width();t.moveLeftContent&&e(t.content).css("margin-left",h),t.moveLeftFooter&&e(t.footer).css("margin-left",h),t.handlePopState&&(n(),e(window).bind("popstate",n)),e(f+">a").on("click",function(){a(this)}),e(c+">a").on("click",function(){a(this),e(this).parent().parent().parent().addClass(t.activeClass)}),r.off("click").on("click",function(n){if(o.hasClass("collapsed"))return!1;var a=e(this);return a.siblings(".sub-menu").slideToggle(600,function(){var e=a.parent(),n=e.find("ul");return n.is(":visible")?e.removeClass("collapsed"):e.addClass("collapsed")}),a.children(".toggle-icon").toggleClass(t.menuLeftClass+" "+t.menuDownClass),n.stopPropagation(),!1}),e(this).find(".mainmenu-li>a").hover(function(){var n=e(this).siblings(".more-actions");n.length&&!o.hasClass("collapsed")&&n.css("display","inline-block")},function(){var n=e(this).siblings(".more-actions");n.length&&!o.hasClass("collapsed")&&n.hide()}),d.hover(function(){e(this).css("display","inline-block")}),t.fitHeight&&d.on("click",function(){var n=e(this),t=e(".side-nav-header");u.css({top:n.offset().top-t.offset().top+20+"px",left:n.offset().left+"px"})}),u.on("click",function(){var n=e(this).parent();setTimeout(function(){n.hide()},1e3)}),o.children(".side-nav-menu").scroll(function(){d.removeClass("open")}),l.click(function(){return o.toggleClass("collapsed").promise().done(function(){var n="ul.sub-menu",a=o.find(n),s=o.find(".side-nav-menu>li");o.hasClass("collapsed")?(a.hide(),d.hide(),s.hover(function(){e(this).find(n).show();var a=e(this),i=e(".side-nav-header");t.fitHeight&&e(this).find(n).css({position:"fixed",top:a.offset().top-i.offset().top+"px",left:"50px"})},function(){e(this).find(n).hide()})):(a.show().each(function(n,t){return e(t).parent().removeClass("collapsed")}),s.unbind("mouseenter mouseleave"),o.find(".toggle-icon").removeClass(t.menuLeftClass).addClass(t.menuDownClass),t.fitHeight&&e(i).find(n).css({position:"relative",top:0,left:0})),o.on("transitionend",function(){var n=o.width();t.moveLeftContent&&e(t.content).css("margin-left",n),t.moveLeftFooter&&e(t.footer).css("margin-left",n)}),l.find("span").toggleClass(t.collapseNavBarClass+" "+t.expandNavBarClass)}),!1})})},e.fn.navigationBar.defaults={handlePopState:!0,fitHeight:!1,content:"#main",footer:"footer",moveLeftContent:!0,moveLeftFooter:!0,menuLeftClass:"glyphicon-menu-right",menuDownClass:"glyphicon-menu-down",collapseNavBarClass:"fa-angle-double-left",expandNavBarClass:"fa-angle-double-right",activeClass:"active",navBarToggleDataAttr:"collapse-side-nav",subMenuNavToggleDataAttr:"collapse-sub-menu"}}(jQuery);
\ No newline at end of file
+"use strict";!function(e){e.fn.navigationBar=function(n){var t=e.extend({},e.fn.navigationBar.defaults,n);return this.each(function(){function n(){var n=window.location.pathname+window.location.hash;o.find("li a").each(function(t,a){var s=e(a),i=s.attr("data-href")||s.attr("href");n.indexOf(i)!==-1&&["","#"].indexOf(i)===-1?s.parent().addClass("active"):s.parent().removeClass("active")})}function a(n){var a=e(n).parent(),s=t.activeClass,i=f+"."+s,l=d+"."+s;o.find(i).removeClass(s),o.find(l).removeClass(s),a.addClass(s)}var s=this,i=".navigation-bar-container",o=e(this).find(i),l=e(this).find("[data-toggle="+t.navBarToggleDataAttr+"]"),r=e(this).find("[data-toggle="+t.subMenuNavToggleDataAttr+"]"),f=".side-nav-menu>li",d=".side-nav-menu>li>ul>li",c=e(this).find(".more-actions"),u=c.children(".dropdown-menu");r.each(function(n,t){return e(t).parent().addClass("has-sub-menu")}),t.fitHeight&&(e(this).addClass("navigation-bar-fit-height"),e(this).find(".side-nav-menu").on("DOMMouseScroll mousewheel",function(n){var t=e(this),a=this.scrollTop,s=this.scrollHeight,i=t.innerHeight(),o=n.originalEvent.wheelDelta,l=o>0,r=function(){return n.stopPropagation(),n.preventDefault(),n.returnValue=!1,!1};return!l&&-o>s-i-a?(t.scrollTop(s),r()):l&&o>a?(t.scrollTop(0),r()):void 0}));var h=o.width();t.moveLeftContent&&e(t.content).css("margin-left",h),t.moveLeftFooter&&e(t.footer).css("margin-left",h),t.handlePopState&&(n(),e(window).bind("popstate",n)),e(f+">a").on("click",function(){a(this)}),e(d+">a").on("click",function(){a(this),e(this).parent().parent().parent().addClass(t.activeClass)}),r.off("click").on("click",function(n){if(o.hasClass("collapsed"))return!1;var a=e(this);return a.siblings(".sub-menu").slideToggle(600,function(){var e=a.parent(),n=e.find("ul");return n.is(":visible")?e.removeClass("collapsed"):e.addClass("collapsed")}),a.children(".toggle-icon").toggleClass(t.menuLeftClass+" "+t.menuDownClass),n.stopPropagation(),!1}),t.fitHeight&&c.on("click",function(){var n=e(this),t=e(".side-nav-header");u.css({top:n.offset().top-t.offset().top+20+"px",left:n.offset().left+"px"})}),o.children(".side-nav-menu").scroll(function(){c.removeClass("open")}),l.click(function(){return o.toggleClass("collapsed").promise().done(function(){var n="ul.sub-menu",a=o.find(n),i=o.find(".side-nav-menu>li");o.hasClass("collapsed")?(a.hide(),c.hide(),i.hover(function(){e(this).find(n).show();var a=e(this),s=e(".side-nav-header");t.fitHeight&&e(this).find(n).css({position:"fixed",top:a.offset().top-s.offset().top+"px",left:"50px"})},function(){e(this).find(n).hide()})):(a.show().each(function(n,t){return e(t).parent().removeClass("collapsed")}),i.unbind("mouseenter mouseleave"),o.find(".toggle-icon").removeClass(t.menuLeftClass).addClass(t.menuDownClass),c.show(),t.fitHeight&&e(s).find(n).css({position:"relative",top:0,left:0})),o.on("transitionend",function(){var n=o.width();t.moveLeftContent&&e(t.content).css("margin-left",n),t.moveLeftFooter&&e(t.footer).css("margin-left",n)}),l.find("span").toggleClass(t.collapseNavBarClass+" "+t.expandNavBarClass)}),!1})})},e.fn.navigationBar.defaults={handlePopState:!0,fitHeight:!1,content:"#main",footer:"footer",moveLeftContent:!0,moveLeftFooter:!0,menuLeftClass:"glyphicon-menu-right",menuDownClass:"glyphicon-menu-down",collapseNavBarClass:"fa-angle-double-left",expandNavBarClass:"fa-angle-double-right",activeClass:"active",navBarToggleDataAttr:"collapse-side-nav",subMenuNavToggleDataAttr:"collapse-sub-menu"}}(jQuery);
\ No newline at end of file
index 8eb2bbd..aed2c50 100644 (file)
   dependencies:
     "@types/d3-dsv" "*"
 
+"@types/d3-scale-chromatic@^1.1.0":
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-1.1.0.tgz#6a5033ed1b89b7bad38f5f085a4f16695f07fdf0"
+
 "@types/d3-scale@*":
   version "1.0.10"
   resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-1.0.10.tgz#8c5c1dca54a159eed042b46719dbb3bdb7e8c842"
@@ -1769,6 +1773,12 @@ d3-request@1.0.6:
     d3-dsv "1"
     xmlhttprequest "1"
 
+d3-scale-chromatic@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-1.1.1.tgz#811406e8e09dab78a49dac4a32047d5d3edd0c44"
+  dependencies:
+    d3-interpolate "1"
+
 d3-scale@1.0.6:
   version "1.0.6"
   resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-1.0.6.tgz#bce19da80d3a0cf422c9543ae3322086220b34ed"