cloudfoundry/stratos

View on GitHub
src/frontend/packages/core/src/shared/components/metrics-chart/metrics-chart.component.ts

Summary

Maintainability
A
2 hrs
Test Coverage
import { AfterContentInit, Component, ContentChild, Input, OnDestroy, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { combineLatest, Observable, Subscription, timer } from 'rxjs';
import { debounce, distinctUntilChanged, map, startWith } from 'rxjs/operators';

import { MetricsAction } from '../../../../../store/src/actions/metrics.actions';
import { AppState } from '../../../../../store/src/app-state';
import { EntityMonitor } from '../../../../../store/src/monitors/entity-monitor';
import { EntityMonitorFactory } from '../../../../../store/src/monitors/entity-monitor.factory.service';
import {
  ChartSeries,
  IMetrics,
  MetricResultTypes,
  MetricsFilterSeries,
} from '../../../../../store/src/types/base-metric.types';
import { MetricsRangeSelectorComponent } from '../metrics-range-selector/metrics-range-selector.component';
import { MetricsChartTypes, MetricsLineChartConfig, YAxisTickFormattingFunc } from './metrics-chart.types';
import { MetricsChartManager } from './metrics.component.manager';

const MAX_SERIES_IN_TOOLTIP = 16;

export interface MetricsConfig<T = any> {
  metricsAction: MetricsAction;
  getSeriesName: (T) => string;
  mapSeriesItemName?: (value: any) => string | Date;
  mapSeriesItemValue?: (value: any) => any;
  filterSeries?: MetricsFilterSeries;
  sort?: (a: ChartSeries<T>, b: ChartSeries<T>) => number;
  tooltipValueFormatter?: YAxisTickFormattingFunc;
}

@Component({
  selector: 'app-metrics-chart',
  templateUrl: './metrics-chart.component.html',
  styleUrls: ['./metrics-chart.component.scss']
})
export class MetricsChartComponent implements OnInit, OnDestroy, AfterContentInit {
  @Input()
  public metricsConfig: MetricsConfig;
  @Input()
  public chartConfig: MetricsLineChartConfig;
  @Input()
  public title: string;

  @ContentChild(MetricsRangeSelectorComponent, { static: true })
  public timeRangeSelector: MetricsRangeSelectorComponent;

  @Input()
  set metricsAction(action: MetricsAction) {
    this.commitAction(action);
  }

  public hasMultipleInstances = false;

  public chartTypes = MetricsChartTypes;

  private timeSelectorSub: Subscription;

  public results$: Observable<IMetrics<any> | ChartSeries<any>[]>;

  public metricsMonitor: EntityMonitor<IMetrics>;

  private committedAction: MetricsAction;

  public isRefreshing$: Observable<boolean>;
  public isFetching$: Observable<boolean>;

  constructor(
    private store: Store<AppState>,
    private entityMonitorFactory: EntityMonitorFactory
  ) { }
  private sort(metricsArray: ChartSeries[]) {
    if (this.metricsConfig.sort) {
      const newMetricsArray = [
        ...metricsArray
      ];
      return newMetricsArray.sort(this.metricsConfig.sort);
    }
    return metricsArray;
  }
  private postFetchMiddleware(metricsArray: ChartSeries[], params: [number, number, number]) {
    const [start, end, step] = params;
    const sortedArray = this.sort(metricsArray);
    if (start && end && step) {
      return MetricsChartManager.fillOutTimeOrderedChartSeries(
        sortedArray,
        start,
        end,
        step,
        this.metricsConfig,
      );
    }
    return sortedArray;
  }

  ngOnInit() {
    this.committedAction = this.metricsConfig.metricsAction;
    this.metricsMonitor = this.entityMonitorFactory.create<IMetrics>(
      this.metricsConfig.metricsAction.guid,
      this.committedAction
    );

    const baseResults$ = this.metricsMonitor.entity$.pipe(
      distinctUntilChanged((oldMetrics, newMetrics) => {
        return oldMetrics && oldMetrics.data === newMetrics.data;
      }),

    );

    this.results$ = baseResults$.pipe(
      map(metrics => {
        if (!metrics) {
          return metrics;
        }
        const mapMetricsData = this.mapMetricsToChartData(metrics, this.metricsConfig);
        const metricsArray = this.metricsConfig.filterSeries ? this.metricsConfig.filterSeries(mapMetricsData) : mapMetricsData;
        if (!metricsArray.length) {
          return [];
        }

        const query = metrics.query;
        const { start, end, step } = query.params as { start: number, end: number, step: number };
        this.hasMultipleInstances = metricsArray.length > 1;
        return this.postFetchMiddleware(metricsArray, [start, end, step]);
      }),
      distinctUntilChanged()
    );

    this.isRefreshing$ = combineLatest(
      this.results$,
      this.metricsMonitor.isFetchingEntity$.pipe(startWith(true))
    ).pipe(
      debounce(([results, fetching]) => {
        return !fetching ? timer(800) : timer(0);
      }),
      map(([results, fetching]) => results && fetching),
      distinctUntilChanged()
    );

    this.isFetching$ = combineLatest(
      this.results$.pipe(startWith(null)),
      this.metricsMonitor.isFetchingEntity$.pipe(startWith(true))
    ).pipe(
      map(([results, fetching]) => !results && fetching),
      distinctUntilChanged(),
      startWith(true),
    );
  }

  ngAfterContentInit() {
    if (this.timeRangeSelector) {
      this.timeRangeSelector.baseAction = this.metricsConfig.metricsAction;
      this.timeSelectorSub = this.timeRangeSelector.metricsAction.subscribe((action: MetricsAction) => {
        this.commitAction(action);
      });
    }
  }

  ngOnDestroy() {
    if (this.timeSelectorSub) {
      this.timeSelectorSub.unsubscribe();
    }
  }

  private mapMetricsToChartData(metrics: IMetrics, metricsConfig: MetricsConfig) {
    if (metrics && metrics.data) {
      switch (metrics.data.resultType) {
        case MetricResultTypes.MATRIX:
          return MetricsChartManager.mapMatrix(metrics.data, metricsConfig);
        case MetricResultTypes.VECTOR:
          return MetricsChartManager.mapVector(metrics.data, metricsConfig);
        case MetricResultTypes.SCALAR:
        case MetricResultTypes.STRING:
        default:
          throw new Error(`Could not find chart data mapper for metrics type ${metrics.data.resultType}`);
      }
    } else {
      return [];
    }
  }

  private commitAction(action: MetricsAction) {
    this.committedAction = action;
    this.store.dispatch(action);
  }

  public getTooltipName(model: { name: { toLocaleString: () => any; }; }) {
    return model.name.toLocaleString();
  }

  public getTooltipValue(model: { value: string; }) {
    return this.metricsConfig.tooltipValueFormatter ? this.metricsConfig.tooltipValueFormatter(model.value) : model.value;
  }

  public getSeriesTooltipModel(model) {
    if (model.length <= MAX_SERIES_IN_TOOLTIP) {
      return model;
    }

    const truncated = model.slice(0, MAX_SERIES_IN_TOOLTIP);
    truncated.push({truncated: true});
    return truncated;
  }
}