cloudfoundry/stratos

View on GitHub
src/frontend/packages/cloud-foundry/src/features/cf/users/manage-users/manage-users-modify/manage-users-modify.component.ts

Summary

Maintainability
B
5 hrs
Test Coverage
import {
  ChangeDetectorRef,
  Component,
  ComponentFactory,
  ComponentFactoryResolver,
  ComponentRef,
  Input,
  OnDestroy,
  OnInit,
  ViewChild,
  ViewContainerRef,
} from '@angular/core';
import { MatSnackBar, MatSnackBarRef, SimpleSnackBar } from '@angular/material/snack-bar';
import { Store } from '@ngrx/store';
import { getRowMetadata } from '@stratosui/store';
import { BehaviorSubject, combineLatest, Observable, of as observableOf, Subscription } from 'rxjs';
import {
  catchError,
  debounceTime,
  delay,
  distinctUntilChanged,
  filter,
  first,
  map,
  share,
  startWith,
  switchMap,
  tap,
} from 'rxjs/operators';

import {
  ITableListDataSource,
} from '../../../../../../../core/src/shared/components/list/data-sources-controllers/list-data-source-types';
import { ITableColumn } from '../../../../../../../core/src/shared/components/list/list-table/table.types';
import { APIResource } from '../../../../../../../store/src/types/api.types';
import { UsersRolesFlipSetRoles, UsersRolesSetOrg } from '../../../../../actions/users-roles.actions';
import { IOrganization } from '../../../../../cf-api.types';
import { CFAppState } from '../../../../../cf-app-state';
import {
  TableCellRoleOrgSpaceComponent,
} from '../../../../../shared/components/list/list-types/cf-users-org-space-roles/table-cell-org-space-role/table-cell-org-space-role.component';
import {
  TableCellSelectOrgComponent,
} from '../../../../../shared/components/list/list-types/cf-users-org-space-roles/table-cell-select-org/table-cell-select-org.component';
import {
  selectCfUsersIsRemove,
  selectCfUsersIsSetByUsername,
  selectCfUsersRolesOrgGuid,
  selectCfUsersRolesPicked,
  selectCfUsersRolesRoles,
} from '../../../../../store/selectors/cf-users-roles.selector';
import { CfUser, OrgUserRoleNames } from '../../../../../store/types/cf-user.types';
import { ActiveRouteCfOrgSpace } from '../../../cf-page.types';
import { CfRolesService } from '../cf-roles.service';
import { SpaceRolesListWrapperComponent } from './space-roles-list-wrapper/space-roles-list-wrapper.component';

interface Org { metadata: { guid: string, }; }
interface CfUserWithWarning extends CfUser {
  showWarning: boolean;
}

@Component({
  selector: 'app-manage-users-modify',
  templateUrl: './manage-users-modify.component.html',
  styleUrls: ['./manage-users-modify.component.scss'],
  entryComponents: [SpaceRolesListWrapperComponent]
})
export class UsersRolesModifyComponent implements OnInit, OnDestroy {


  @Input() setUsernames = false;
  orgColumns: ITableColumn<Org>[] = [
    {
      columnId: 'org',
      headerCell: () => 'Organization',
      cellComponent: TableCellSelectOrgComponent
    },
    {
      columnId: 'manager',
      headerCell: () => 'Manager',
      cellComponent: TableCellRoleOrgSpaceComponent,
      class: 'app-table__cell--table-column-additional-padding',
      cellConfig: {
        role: OrgUserRoleNames.MANAGER
      }
    },
    {
      columnId: 'auditor',
      headerCell: () => 'Auditor',
      cellComponent: TableCellRoleOrgSpaceComponent,
      class: 'app-table__cell--table-column-additional-padding',
      cellConfig: {
        role: OrgUserRoleNames.AUDITOR
      }
    },
    {
      columnId: 'billingManager',
      headerCell: () => 'Billing Manager',
      cellComponent: TableCellRoleOrgSpaceComponent,
      class: 'app-table__cell--table-column-additional-padding',
      cellConfig: {
        role: OrgUserRoleNames.BILLING_MANAGERS
      }
    },
    {
      columnId: 'user',
      headerCell: () => 'User',
      cellComponent: TableCellRoleOrgSpaceComponent,
      class: 'app-table__cell--table-column-additional-padding',
      cellConfig: {
        role: OrgUserRoleNames.USER
      }
    }
  ];
  orgDataSource: ITableListDataSource<APIResource<IOrganization>>;

  @ViewChild('spaceRolesTable', { read: ViewContainerRef, static: true })
  spaceRolesTable: ViewContainerRef;

  private wrapperFactory: ComponentFactory<SpaceRolesListWrapperComponent>;
  private wrapperRef: ComponentRef<SpaceRolesListWrapperComponent>;
  private snackBarRef: MatSnackBarRef<SimpleSnackBar>;

  usersNames$: Observable<string[]>;
  blocked = new BehaviorSubject<boolean>(true);
  blocked$: Observable<boolean> = this.blocked.asObservable().pipe(delay(0));
  valid$: Observable<boolean>;
  orgRoles = OrgUserRoleNames;
  selectedOrgGuid: string;
  orgGuidChangedSub: Subscription;
  usersWithWarning$: Observable<string[]>;
  isSetByUsername$: Observable<boolean>;
  isRemove$: Observable<boolean>;

  constructor(
    private store: Store<CFAppState>,
    private activeRouteCfOrgSpace: ActiveRouteCfOrgSpace,
    private componentFactoryResolver: ComponentFactoryResolver,
    private cfRolesService: CfRolesService,
    private cd: ChangeDetectorRef,
    private snackBar: MatSnackBar,
  ) {
    this.wrapperFactory = this.componentFactoryResolver.resolveComponentFactory(SpaceRolesListWrapperComponent);
  }

  ngOnInit() {
    if (this.setUsernames) {
      this.blocked.next(false);
    } else {
      this.cfRolesService.loading$.subscribe(loading => this.blocked.next(loading));
    }

    const orgEntity$ = this.store.select(selectCfUsersRolesOrgGuid).pipe(
      startWith(''),
      distinctUntilChanged(),
      filter(orgGuid => !!orgGuid),
      tap(orgGuid => this.updateOrg(orgGuid)),
      switchMap(orgGuid => this.cfRolesService.fetchOrg(this.activeRouteCfOrgSpace.cfGuid, orgGuid)),
      share()
    );

    const orgConnect$ = orgEntity$.pipe(
      filter(entityInfo => !!entityInfo.entity),
      map(entityInfo => [entityInfo.entity]),
      share()
    );

    const isTableLoading$ = orgEntity$.pipe(
      map(orgEntity => orgEntity.entityRequestInfo.fetching),
      startWith(true)
    );
    // Data source that will power the orgs table
    this.orgDataSource = {
      isTableLoading$,
      connect: () => orgConnect$,
      disconnect: () => { },
      trackBy: (index, row) => getRowMetadata(row)
    } as ITableListDataSource<APIResource<IOrganization>>;

    // Set the starting state of the org table
    if (this.activeRouteCfOrgSpace.orgGuid) {
      this.cfRolesService.fetchOrg(this.activeRouteCfOrgSpace.cfGuid, this.activeRouteCfOrgSpace.orgGuid).pipe(
        first()
      ).subscribe(org => {
        this.store.dispatch(new UsersRolesSetOrg(this.activeRouteCfOrgSpace.orgGuid, org.entity.entity.name));
      });
    } else {
      this.orgGuidChangedSub = this.cfRolesService.fetchOrgs(this.activeRouteCfOrgSpace.cfGuid).pipe(
        filter(orgs => orgs && !!orgs.length),
        first()
      ).subscribe(orgs => {
        this.store.dispatch(new UsersRolesSetOrg(orgs[0].metadata.guid, orgs[0].entity.name));
      });
    }

    const users$: Observable<CfUserWithWarning[]> = this.store.select(selectCfUsersRolesPicked).pipe(
      filter(users => !!users),
      distinctUntilChanged(),
      map(users => users.map(this.mapUser.bind(this)))
    );

    this.usersNames$ = users$.pipe(
      map(users => users.map(user => user.showWarning ? '*' + user.username : user.username))
    );

    this.usersWithWarning$ = users$.pipe(
      map(users => users.filter(user => !!user.showWarning).map(user => user.username))
    );

    this.valid$ = this.store.select(selectCfUsersRolesRoles).pipe(
      debounceTime(150),
      switchMap(newRoles => this.cfRolesService.createRolesDiff(newRoles.orgGuid)),
      map(changes => !!changes.length)
    );

    this.isSetByUsername$ = this.store.select(selectCfUsersIsSetByUsername);
    this.isRemove$ = this.store.select(selectCfUsersIsRemove);
  }

  private mapUser(user: CfUser): CfUserWithWarning {
    // If we're at the org level or lower we guarantee org roles. If we're at the space we guarantee space roles.

    const showWarning = !!user.missingRoles &&
      ((user.missingRoles.org.length && !this.activeRouteCfOrgSpace.orgGuid) ||
        (user.missingRoles.space.length && !this.activeRouteCfOrgSpace.spaceGuid));
    // Ensure we're in an object where the username is always populated (in some cases it's missing)
    const newUser = {
      ...user,
      showWarning,
      username: user.username || user.guid
    };
    return newUser;
  }

  private destroySpacesList() {
    if (this.wrapperRef) {
      this.wrapperRef.destroy();
    }
    if (this.spaceRolesTable) {
      this.spaceRolesTable.clear();
    }
  }

  ngOnDestroy() {
    if (this.orgGuidChangedSub) {
      this.orgGuidChangedSub.unsubscribe();
    }
    this.destroySpacesList();
    if (this.snackBarRef) {
      this.snackBarRef.dismiss();
      this.snackBarRef = null;
    }
  }

  updateOrg(orgGuid: string) {
    this.selectedOrgGuid = orgGuid;
    if (!this.selectedOrgGuid) {
      return;
    }

    // When the state is ready (org guid is correct), recreate the space roles table for the selected org
    this.store.select(selectCfUsersRolesRoles).pipe(
      // Wait for the store to have the correct org
      filter(newRoles => newRoles && newRoles.orgGuid === orgGuid),
      first()
    ).subscribe({
      complete: () => {
        // The org has changed, completely recreate the roles table
        this.destroySpacesList();

        this.wrapperRef = this.spaceRolesTable.createComponent(this.wrapperFactory);
        this.cd.detectChanges();
      }
    });
  }

  onEnter = () => {
    if (!this.snackBarRef) {
      this.usersWithWarning$.pipe(first()).subscribe((usersWithWarning => {
        if (usersWithWarning && usersWithWarning.length) {
          this.snackBarRef = this.snackBar.open(`Not all roles are shown for user/s - ${usersWithWarning.join(', ')}. To avoid this please
          navigate to a specific organization or space`, 'Dismiss');
        }
      }));
    }

    // In order to show the removed roles correctly (as ticks) flip them from remove to add
    this.store.select(selectCfUsersIsRemove).pipe(first()).subscribe(isRemove => {
      if (isRemove) {
        this.store.dispatch(new UsersRolesFlipSetRoles());
      }
    });
  };

  onLeave = (isNext: boolean) => {
    if (!isNext && this.snackBarRef) {
      this.snackBarRef.dismiss();
      this.snackBarRef = null;
    }
  };

  onNext = () => {
    return combineLatest([
      this.store.select(selectCfUsersIsRemove).pipe(first()),
      this.cfRolesService.createRolesDiff(this.selectedOrgGuid)
    ]).pipe(
      map(([isRemove]) => {
        if (isRemove) {
          // If we're going to eventually remove the roles flip the add to remove
          this.store.dispatch(new UsersRolesFlipSetRoles());
        }
        return { success: true };
      })
    ).pipe(catchError(err => {
      return observableOf({ success: false });
    }));
  };

}