cloudfoundry/stratos

View on GitHub
src/frontend/packages/core/src/shared/components/list/list.component.types.ts

Summary

Maintainability
A
0 mins
Test Coverage
import { Type } from '@angular/core';
import moment from 'moment';
import { BehaviorSubject, combineLatest, Observable, of as observableOf } from 'rxjs';
import { filter, first, map, startWith, switchMap } from 'rxjs/operators';

import { ListView } from '../../../../../store/src/actions/list.actions';
import { ActionState } from '../../../../../store/src/reducers/api-request-reducer/types';
import {
  defaultClientPaginationPageSize,
} from '../../../../../store/src/reducers/pagination-reducer/pagination-reducer-reset-pagination';
import { ITimeRange } from '../../services/metrics-range-selector.types';
import { ListDataSource } from './data-sources-controllers/list-data-source';
import { IListDataSource } from './data-sources-controllers/list-data-source-types';
import { CardTypes } from './list-cards/card/card.component';
import { ITableColumn, ITableText } from './list-table/table.types';
import { CardCell } from './list.types';

export enum ListViewTypes {
  CARD_ONLY = 'cardOnly',
  TABLE_ONLY = 'tableOnly',
  BOTH = 'both'
}

export interface IListConfig<T> {
  /**
   * List of actions that are presented as individual buttons and applies to general activities surrounding the list (not specific to rows).
   * For example `Add`
   */
  getGlobalActions: () => IGlobalListAction<T>[];
  /**
   * List of actions that are presented as individual buttons when one or more rows are selected. For example `Delete` of selected rows.
   */
  getMultiActions: () => IMultiListAction<T>[];
  /**
   * List of actions that are presented in a mat-menu for an individual entity. For example `unmap` an application route
   */
  getSingleActions: () => IListAction<T>[];
  /**
   * Collection of column definitions to show when the list is in table mode
   */
  getColumns: () => ITableColumn<T>[];
  /**
   * The data source used to provide list entries. This will be custom per data type
   */
  getDataSource: () => IListDataSource<T>;
  /**
   * Collection of configuration objects to support multiple drops downs for filtering local lists. For example the application wall filters
   * by cloud foundry, organization and space. This mechanism supports only the showing and storing of such filters. An additional function
   * to the data sources transformEntities collection should be used to apply these custom settings to the data.
   */
  getMultiFiltersConfigs: () => IListMultiFilterConfig[];
  /**
   * Collection of filter definitions to support filtering across multiple fields in a list.
   * When the filter is selected in a dropdown the filterString filters results using the chosen field.
   * Combined with a transformEntities DataFunction that consumes the filterKey.
   */
  getFilters?: () => IListFilter[];
  /**
   * Fetch an observable that will emit once the underlying config components have been created. For instance if the data source requires
   * something from the store which requires an async call
   */
  getInitialised?: () => Observable<boolean>;
  /**
   * A collection of numbers used to define how many entries per page should be shown. If missing a default will be used per table view type
   */
  pageSizeOptions?: number[];
  /**
   * What different views the user can select (table/cards)
   */
  viewType: ListViewTypes;
  /**
   * What is the initial view that the list will be displayed as (table/cards)
   */
  defaultView?: ListView;
  /**
   * Override the default list text
   */
  text?: ITableText;
  /**
   * Enable a few text filter... other config required
   */
  enableTextFilter?: boolean;
  /**
   * Set a custom value for the minimum height of a table row
   */
  minRowHeight?: string;
  /**
   * Set the align-self of each cell in the row
   */
  tableRowAlignSelf?: string;
  /**
   * The card component used in card view
   */
  cardComponent?: CardTypes<T>;
  /**
   * The component to show when expanding a row
   */
  expandComponent?: ListExpandedComponentType<T>;
  /**
   * Set to true to hide the list refresh button
   */
  hideRefresh?: boolean;
  /**
   * Allow selection regardless of number or visibility of multi actions
   */
  allowSelection?: boolean;
  /**
   * For metrics based data show a metrics range selector
   */
  showCustomTime?: boolean;
  /**
   * Custom time window to show in metrics range selector
   */
  customTimeWindows?: ITimeRange[];
  /**
   * Custom time window validation for metrics range selector
   */
  customTimeValidation?: (start: moment.Moment, end: moment.Moment) => string;
  /**
   * Custom time polling interval. Falsy for disabled.
   */
  customTimePollingInterval?: number;
  /**
   * When enabled set the initial value
   */
  customTimeInitialValue?: string;
}

// Simple list config does not need getDataSource
export type ISimpleListConfig<T> = Omit<IListConfig<T>, 'getDataSource'>;

export interface IListMultiFilterConfig {
  key: string;
  label: string;
  allLabel?: string;
  hideAllOption?: boolean;
  list$: Observable<IListMultiFilterConfigItem[]>;
  loading$: Observable<boolean>;
  select: BehaviorSubject<any>;
  autoSelectFirst?: boolean;
}

export interface IListFilter {
  default?: boolean;
  key: string;
  label: string;
  placeholder: string;
}

export interface IListMultiFilterConfigItem {
  label: string;
  item: any;
  value: string;
}

export const defaultPaginationPageSizeOptionsCards = [defaultClientPaginationPageSize, 30, 80];
export const defaultPaginationPageSizeOptionsTable = [defaultClientPaginationPageSize, 20, 80];

export class ListConfig<T, A = T> implements IListConfig<T> {
  isLocal = false;
  pageSizeOptions = defaultPaginationPageSizeOptionsCards;
  viewType = ListViewTypes.BOTH;
  text: ITableText = null;
  enableTextFilter = false;
  cardComponent = null;
  defaultView = 'table' as ListView;
  allowSelection = false;
  getGlobalActions = (): IGlobalListAction<T>[] => null;
  getMultiActions = (): IMultiListAction<T>[] => null;
  getSingleActions = (): IListAction<T>[] => null;
  getColumns = (): ITableColumn<T>[] => null;
  getDataSource = (): ListDataSource<T, A> => null;
  getMultiFiltersConfigs = (): IListMultiFilterConfig[] => [];
  getFilters = (): IListFilter[] => [];
  getInitialised = () => observableOf(true);
}

export interface IBaseListAction<T> {
  icon?: string;
  label: string;
  description?: string;
}

export interface IListAction<T> extends IBaseListAction<T> {
  action: (item: T) => void;
  createVisible?: (row$: Observable<T>) => Observable<boolean>;
  createEnabled?: (row$: Observable<T>) => Observable<boolean>;
}

export interface IOptionalAction<T> extends IBaseListAction<T> {
  visible$?: Observable<boolean>;
  enabled$?: Observable<boolean>;
}

export interface IMultiListAction<T> extends IOptionalAction<T> {
  /**
   * Return true if the selection should be cleared
   */
  action: (items?: T[]) => boolean | Observable<ActionState>;
}

export interface IGlobalListAction<T> extends IOptionalAction<T> {
  action: () => void;
}

export class MultiFilterManager<T> {
  public filterIsReady$: Observable<boolean>;
  public filterItems$: Observable<IListMultiFilterConfigItem[]>;
  public hasItems$: Observable<boolean>;
  public hasOneOrLessItems$: Observable<boolean>;
  public value: string;

  public filterKey: string;
  public allLabel: string;
  public hideAllOption = false;

  constructor(
    public multiFilterConfig: IListMultiFilterConfig,
    dataSource: IListDataSource<T>,
  ) {
    this.filterKey = this.multiFilterConfig.key;
    this.allLabel = multiFilterConfig.allLabel || 'All';
    this.hideAllOption = multiFilterConfig.hideAllOption || false;
    this.filterItems$ = this.getItemObservable(multiFilterConfig);
    this.hasOneOrLessItems$ = this.filterItems$.pipe(map(items => items.length <= 1));
    this.hasItems$ = this.filterItems$.pipe(map(items => !!items.length));
    this.filterIsReady$ = this.getReadyObservable(multiFilterConfig, dataSource, this.hasItems$);

    // Also select the first option if configured
    if (multiFilterConfig.autoSelectFirst) {
      this.filterItems$.pipe(first()).subscribe(options => {
        if (options && options.length > 0) {
          this.selectItem(options[0].value);
        }
      });
    }
  }

  private getReadyObservable(
    multiFilterConfig: IListMultiFilterConfig,
    dataSource: IListDataSource<T>,
    hasItems$: Observable<boolean>
  ) {
    return combineLatest(
      dataSource.isLoadingPage$,
      multiFilterConfig.loading$,
      hasItems$,
    ).pipe(
      map(([fetchingListPage, fetchingFilter, hasItems]) => (!fetchingListPage && !fetchingFilter) && hasItems),
      startWith(false)
    );
  }

  private getItemObservable(multiFilterConfig: IListMultiFilterConfig) {
    return multiFilterConfig.list$.pipe(
      map(list => list ? list : [])
    );
  }

  public applyValue(multiFilters: {}) {
    this.selectItem(multiFilters[this.multiFilterConfig.key]);

  }

  public hasValue(multiFilters: {}): boolean {
    return !!multiFilters[this.multiFilterConfig.key];
  }

  public selectItem(itemValue: string) {
    this.multiFilterConfig.loading$.pipe(
      filter(ready => !ready),
      switchMap(() => this.filterItems$),
      first(),
    ).subscribe(items => {
      // Ensure we actually have the item. Could be from storage and invalid
      if (itemValue === undefined || items.find(i => i.value === itemValue)) {
        this.value = itemValue;
        this.multiFilterConfig.select.next(itemValue);
      }
    });
  }
}

export type ListExpandedComponentType<T> = Type<CardCell<T>>;