cloudfoundry/stratos

View on GitHub
src/frontend/packages/core/src/core/extension/extension-service.ts

Summary

Maintainability
A
35 mins
Test Coverage
import { Injectable, ModuleWithProviders, NgModule } from '@angular/core';
import { ActivatedRoute, Route, Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';

import { AppState, GeneralEntityAppState } from '../../../../store/src/app-state';
import { EntityServiceFactory } from '../../../../store/src/entity-service-factory.service';
import { IPageSideNavTab } from '../../features/dashboard/page-side-nav/page-side-nav.component';
import { CurrentUserPermissionsService } from '../permissions/current-user-permissions.service';

export const extensionsActionRouteKey = 'extensionsActionsKey';


// The different types of Tab
export enum StratosTabType {
  Application = 'appTabs',
  CloudFoundry = 'cfTabs',
  CloudFoundryOrg = 'cfOrgTabs',
  CloudFoundrySpace = 'cfSpaceTabs'
}

export interface StratosTabMetadata {
  label: string;
  link: string;
  icon?: string;
  iconFont?: string;
  hidden?: (
    store: Store<AppState>,
    esf: EntityServiceFactory,
    activatedRoute: ActivatedRoute,
    cups: CurrentUserPermissionsService
  ) => Observable<boolean>;
}

export interface StratosTabMetadataConfig extends StratosTabMetadata {
  type: StratosTabType;
}

// The different types of Action
export enum StratosActionType {
  Applications = 'appsActions',
  Application = 'appActions',
  CloudFoundry = 'cfActions',
  CloudFoundryOrg = 'cfOrgActions',
  CloudFoundrySpace = 'cfSpaceActions',
  Endpoints = 'endpointsActions'
}

export interface StratosActionMetadata {
  type: StratosActionType;
  label?: string;
  link: string;
  icon: string;
  iconFont?: string;
  visible?: (store: Store<GeneralEntityAppState>) => Observable<boolean>;
  visible$?: Observable<boolean>;
}

export type StratosRouteType = StratosTabType | StratosActionType;

export interface StratosExtensionRoutes {
  path: string;
  component: any;
}

// Stores the extension metadata as defined by the decorators
const extensionMetadata = {
  loginComponent: null,
  extensionRoutes: {} as { [key: string]: StratosExtensionRoutes[], },
  tabs: {} as { [key: string]: IPageSideNavTab[], },
  actions: {} as { [key: string]: StratosActionMetadata[], },
};

/**
 * Decorator for a Tab extension
 */
export function StratosTab(props: StratosTabMetadataConfig) {
  return target => addExtensionTab(props.type, target, props);
}

/**
 * Decorator for an Action extension
 */
export function StratosAction(props: StratosActionMetadata) {
  return target => addExtensionAction(props.type, target, props);
}

export function StratosLoginComponent() {
  return target => extensionMetadata.loginComponent = target;
}

function addExtensionTab(tab: StratosTabType, target: any, props: StratosTabMetadataConfig) {
  if (!extensionMetadata.tabs[tab]) {
    extensionMetadata.tabs[tab] = [];
  }
  if (!extensionMetadata.extensionRoutes[tab]) {
    extensionMetadata.extensionRoutes[tab] = [];
  }

  extensionMetadata.extensionRoutes[tab].push({
    path: props.link,
    component: target
  });
  extensionMetadata.tabs[tab].push({
    ...props
  });
}

function addExtensionAction(action: StratosActionType, target: any, props: StratosActionMetadata) {
  if (!extensionMetadata.actions[action]) {
    extensionMetadata.actions[action] = [];
    extensionMetadata.extensionRoutes[action] = [];
  }
  extensionMetadata.extensionRoutes[action].push({
    path: props.link,
    component: target
  });
  extensionMetadata.actions[action].push(props);
}

// Empty module used to support the registration of Extension Components
@NgModule()
export class ExtEmptyModule { }

// Injectable Extension Service
@Injectable({
  providedIn: 'root',
})
export class ExtensionService {

  public metadata = extensionMetadata;

  constructor(private router: Router) { }

  // Declare extensions - this is a trick to ensure the Angular Build Optimiser does not
  // optimize out any extension components
  public static declare(components: any[]): ModuleWithProviders<ExtEmptyModule> {
    return {
      ngModule: ExtEmptyModule
    };
  }

  /**
   * Initialize the extensions - to be invoked in the AppModule
   */
  public init() {
    this.applyRoutesFromExtensions(this.router);
  }

  /**
   * Apply route configuration
   */
  private applyRoutesFromExtensions(router: Router) {
    const routeConfig = [...router.config];

    // Find the route that has the 'about' page as a child - this is the dashboard base
    const dashboardRoute = routeConfig.find(r => {
      if (r.path === '' && !!r.component && r.children) {
        return !!r.children.find(c => c.path === 'about');
      } else {
        return false;
      }
    });

    let needsReset = false;
    if (dashboardRoute) {
      // Move any stratos extension routes under the dashboard base route
      while (this.moveExtensionRoute(routeConfig, dashboardRoute)) { }
      needsReset = true;
    }

    if (extensionMetadata.loginComponent) {
      // Override the component used for the login route
      const loginRouteRoot = routeConfig.find(r => r.path === 'login') || { children: [] };
      const loginRoute = loginRouteRoot.children.find(c => c.path === '');
      loginRoute.component = extensionMetadata.loginComponent;
      needsReset = true;
    }

    if (needsReset) {
      router.resetConfig(routeConfig);
    }
  }

  private moveExtensionRoute(routeConfig: Route[], dashboardRoute: Route): boolean {
    const index = routeConfig.findIndex(r => !!r.data && (!!r.data.stratosNavigation || r.data.stratosNavigationPage));
    if (index >= 0) {
      const removed = routeConfig.splice(index, 1);
      dashboardRoute.children = dashboardRoute.children.concat(removed);
    }
    return index >= 0;
  }
}

// Helpers to access Extension metadata (without using the injectable Extension Service)

export function getRoutesFromExtensions(routeType: StratosRouteType): StratosExtensionRoutes[] {
  return extensionMetadata.extensionRoutes[routeType] || [];
}

export function getTabsFromExtensions(tabType: StratosTabType): IPageSideNavTab[] {
  return extensionMetadata.tabs[tabType] || [];
}

export function getActionsFromExtensions(actionType: StratosActionType): StratosActionMetadata[] {
  return extensionMetadata.actions[actionType] || [];
}