cloudfoundry/stratos

View on GitHub
src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoint-list.helpers.ts

Summary

Maintainability
C
1 day
Test Coverage
import { ComponentFactoryResolver, ComponentRef, Injectable, ViewContainerRef } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { combineLatest, Observable, of } from 'rxjs';
import { map, pairwise } from 'rxjs/operators';

import { RouterNav } from '../../../../../../../store/src/actions/router.actions';
import { AppState } from '../../../../../../../store/src/app-state';
import { entityCatalog } from '../../../../../../../store/src/entity-catalog/entity-catalog';
import { ActionState } from '../../../../../../../store/src/reducers/api-request-reducer/types';
import { stratosEntityCatalog } from '../../../../../../../store/src/stratos-entity-catalog';
import { EndpointModel } from '../../../../../../../store/src/types/endpoint.types';
import { CurrentUserPermissionsService } from '../../../../../core/permissions/current-user-permissions.service';
import { StratosCurrentUserPermissions } from '../../../../../core/permissions/stratos-user-permissions.checker';
import {
  ConnectEndpointDialogComponent,
} from '../../../../../features/endpoints/connect-endpoint-dialog/connect-endpoint-dialog.component';
import { SessionService } from '../../../../../shared/services/session.service';
import { UserProfileService } from '../../../../../core/user-profile.service';
import { SnackBarService } from '../../../../services/snackbar.service';
import { ConfirmationDialogConfig } from '../../../confirmation-dialog.config';
import { ConfirmationDialogService } from '../../../confirmation-dialog.service';
import { createMetaCardMenuItemSeparator } from '../../list-cards/meta-card/meta-card-base/meta-card.component';
import { IListAction } from '../../list.component.types';
import { TableCellCustom } from '../../list.types';

interface EndpointDetailsContainerRefs {
  componentRef: ComponentRef<EndpointListDetailsComponent>;
  component: EndpointListDetailsComponent;
  endpointDetails: ViewContainerRef;
}

export abstract class EndpointListDetailsComponent extends TableCellCustom<EndpointModel> {
  isEndpointListDetailsComponent = true;
  isTable = true;
}

function isEndpointListDetailsComponent(obj: any): EndpointListDetailsComponent {
  return obj ? obj.isEndpointListDetailsComponent ? obj as EndpointListDetailsComponent : null : null;
}

/**
 * Combine the result of all createVisibles functions for the given actions
 */
function combineCreateVisibles(
  customActions: IListAction<EndpointModel>[]
): (row$: Observable<EndpointModel>) => Observable<boolean> {
  const createVisiblesFns = customActions
    .map(action => action.createVisible)
    .filter(createVisible => !!createVisible);
  if (createVisiblesFns.length === 0) {
    return () => of(false);
  } else {
    return (row$: Observable<EndpointModel>) => {
      const createVisibles = createVisiblesFns.map(createVisible => createVisible(row$));
      return combineLatest(createVisibles).pipe(
        map(allRes => allRes.some(res => res))
      );
    };
  }
}

@Injectable()
export class EndpointListHelper {
  constructor(
    private store: Store<AppState>,
    private dialog: MatDialog,
    private currentUserPermissionsService: CurrentUserPermissionsService,
    private confirmDialog: ConfirmationDialogService,
    private snackBarService: SnackBarService,
    private sessionService: SessionService,
    private userProfileService: UserProfileService
  ) { }

  endpointActions(includeSeparators = false): IListAction<EndpointModel>[] {
    // Add any additional actions that are per endpoint type
    const customActions = entityCatalog.getAllEndpointTypes()
      .map(endpoint => endpoint.definition.endpointListActions)
      .filter(endpointListActions => !!endpointListActions)
      .map(endpointListActions => endpointListActions(this.store))
      .reduce((res, actions) => res.concat(actions), []);

    if (includeSeparators && customActions.length) {
      // Only show the separator if we have custom actions to separate AND at least one is visible
      const createVisibleFn = combineCreateVisibles(customActions);
      customActions.splice(0, 0, {
        ...createMetaCardMenuItemSeparator(),
        createVisible: createVisibleFn
      });
    }

    return [
      {
        action: (item) => {
          const message1 = `Are you sure you want to disconnect endpoint '${item.name}'?`;
          // TODO: This only current applies to CF
          const message2 = item.local ? `This will also update your local configuration.` : '';
          const confirmation = new ConfirmationDialogConfig(
            'Disconnect Endpoint',
            `${message1}${message2 ? `<br><br>${message2}` : ''}`,
            'Disconnect',
            false
          );
          this.confirmDialog.open(confirmation, () => {
            const obs$ = stratosEntityCatalog.endpoint.api.disconnect<ActionState>(item.guid, item.cnsi_type);
            this.handleAction(obs$, () => {
              this.snackBarService.show(`Disconnected endpoint '${item.name}'`);
              stratosEntityCatalog.systemInfo.api.getSystemInfo();
            });
          });
        },
        label: 'Disconnect',
        description: ``, // Description depends on console user permission
        createVisible: (row$: Observable<EndpointModel>) => combineLatest([
          this.currentUserPermissionsService.can(StratosCurrentUserPermissions.EDIT_ADMIN_ENDPOINT),
          row$
        ]).pipe(
          map(([isAdmin, row]) => {
            const isConnected = row.connectionStatus === 'connected';
            return isConnected && (!row.system_shared_token || row.system_shared_token && isAdmin);
          })
        )
      },
      {
        action: (item) => {
          this.dialog.open(ConnectEndpointDialogComponent, {
            data: {
              name: item.name,
              guid: item.guid,
              type: item.cnsi_type,
              subType: item.sub_type,
              ssoAllowed: item.sso_allowed
            },
            disableClose: true
          });
        },
        label: 'Connect',
        description: '',
        createVisible: (row$: Observable<EndpointModel>) => {
          return combineLatest([
            this.sessionService.userEndpointsNotDisabled(),
            this.userProfileService.userProfile$,
            row$
          ]).pipe(
            map(([userEndpointsEnabled, profile, row]) => {
              if (userEndpointsEnabled && !row.creator.system && profile.userName !== row.creator.name) {
                // Disable connect for admins if the endpoint was not created by them. Otherwise this could result in an admin connecting to
                // multiple user endpoints that all have the same url.
                return false;
              } else {
                const endpoint = entityCatalog.getEndpoint(row.cnsi_type, row.sub_type);
                const ep = endpoint ? endpoint.definition : { unConnectable: false };
                return !ep.unConnectable && row.connectionStatus === 'disconnected';
              }
            })
          );
        }
      },
      {
        action: (item) => {
          const confirmation = new ConfirmationDialogConfig(
            'Unregister Endpoint',
            `Are you sure you want to unregister endpoint '${item.name}'?`,
            'Unregister',
            true
          );
          this.confirmDialog.open(confirmation, () => {
            const obs$ = stratosEntityCatalog.endpoint.api.unregister<ActionState>(item.guid, item.cnsi_type);
            this.handleAction(obs$, () => {
              this.snackBarService.show(`Unregistered ${item.name}`);
            });
          });
        },
        label: 'Unregister',
        description: 'Remove the endpoint',
        createVisible: (row$: Observable<EndpointModel>) => {
          // I think if we end up using these often there should be specific create,
          // edit, delete style permissions in the stratos permissions checker
          return combineLatest([
            this.sessionService.userEndpointsNotDisabled(),
            this.currentUserPermissionsService.can(StratosCurrentUserPermissions.EDIT_ADMIN_ENDPOINT),
            this.currentUserPermissionsService.can(StratosCurrentUserPermissions.EDIT_ENDPOINT),
            row$
          ]).pipe(
            map(([userEndpointsEnabled, isAdmin, isEndpointAdmin, row]) => {
              if (!userEndpointsEnabled || row.creator.system) {
                return isAdmin;
              } else {
                return isEndpointAdmin || isAdmin;
              }
            })
          );
        }
      },
      {
        action: (item) => {
          const routerLink = `/endpoints/edit/${item.guid}`;
          this.store.dispatch(new RouterNav({ path: routerLink }));
        },
        label: 'Edit endpoint',
        description: 'Edit the endpoint',
        createVisible: (row$: Observable<EndpointModel>) => {
          return combineLatest([
            this.sessionService.userEndpointsNotDisabled(),
            this.currentUserPermissionsService.can(StratosCurrentUserPermissions.EDIT_ADMIN_ENDPOINT),
            this.currentUserPermissionsService.can(StratosCurrentUserPermissions.EDIT_ENDPOINT),
            row$
          ]).pipe(
            map(([userEndpointsEnabled, isAdmin, isEndpointAdmin, row]) => {
              if (!userEndpointsEnabled || row.creator.system) {
                return isAdmin;
              } else {
                return isEndpointAdmin || isAdmin;
              }
            })
          );
        }
      },
      ...customActions
    ];
  }

  private handleAction(obs$: Observable<ActionState>, handleChange: ([o, n]: [ActionState, ActionState]) => void) {
    const disSub = obs$.pipe(
      pairwise()
    ).subscribe(([oldVal, newVal]) => {
      // https://github.com/SUSE/stratos/issues/29 Generic way to handle errors ('Failed to disconnect X')
      if (!newVal.error && (oldVal.busy && !newVal.busy)) {
        handleChange([oldVal, newVal]);
        disSub.unsubscribe();
      }
    });
  }

  createEndpointDetails(listDetailsComponent: any, container: ViewContainerRef, componentFactoryResolver: ComponentFactoryResolver):
    EndpointDetailsContainerRefs {
    const componentFactory = componentFactoryResolver.resolveComponentFactory<EndpointListDetailsComponent>(listDetailsComponent);
    const componentRef = container.createComponent<EndpointListDetailsComponent>(componentFactory);
    const component = isEndpointListDetailsComponent(componentRef.instance);
    const refs = {
      componentRef,
      component,
      endpointDetails: container
    };
    if (!component) {
      console.warn(`Attempted to create a non-endpoint list details component "${listDetailsComponent}"`);
      this.destroyEndpointDetails(refs);
    }
    return refs;
  }

  destroyEndpointDetails(refs: EndpointDetailsContainerRefs) {
    if (refs.componentRef && refs.componentRef.destroy) {
      refs.componentRef.destroy();
    }
    if (refs.endpointDetails && refs.endpointDetails.clear) {
      refs.endpointDetails.clear();
    }
  }
}