adorsys/datasafe

View on GitHub
frontend/datasafe-ui/src/app/component/filetree/filetree.component.ts

Summary

Maintainability
C
7 hrs
Test Coverage
import {CollectionViewer, SelectionChange} from '@angular/cdk/collections';
import {FlatTreeControl} from '@angular/cdk/tree';
import {Component, Inject, Injectable} from '@angular/core';
import {BehaviorSubject, merge, Observable} from 'rxjs';
import {map} from 'rxjs/operators';
import {ApiService} from "../../service/api/api.service";
import {CredentialsService} from "../../service/credentials/credentials.service";
import {Router} from "@angular/router";
import {ErrorMessageUtil} from "../../app.component";
import {MAT_DIALOG_DATA, MatDialog, MatDialogRef} from "@angular/material";

class UserFileSystem {

  // We need such thing because s3 does not support folders
  uiCreatedFolders = new Set<string>();
  fs = new Map<string, Set<string>>();

  buildFs(files: string[]) {
    this.fs.clear();

    // maintain consistent order
    files.concat(Array.from(this.uiCreatedFolders).map(it => it + "/"))
        .sort()
        .forEach(it => this.addEntry(it));
  }

  rootLevelNodes() : string[] {
    let res = new Set<string>();

    this.fs.forEach((value, key) => {
      let split = key.split("/", 2);
      res.add(split[0] + (split.length > 1 ? "/" : ""));
    });

    return Array.from(res)
  }

  private addEntry(path: string) {

    var fullPath = "";
    var folder = "";
    path.split("/").forEach(segment => {
      fullPath += segment;
      fullPath += (fullPath === path ? "" : "/");

      let name = (((fullPath === path) && (!path.endsWith("/"))) ? segment : segment + "/");
      this.putToFolder(folder, name);
      folder = fullPath
    })
  }

  private putToFolder(folder: string, name: string) {
    if ("" === name || "/" === name) {
      name = null;
    }

    if (folder === "") {
      folder = name;
      name = null;
    }

    if (!this.fs.has(folder)) {
      this.fs.set(folder, new Set<string>());
    }
    this.fs.get(folder).add(name);
  }
}

export class DynamicFlatNode {

      name: string;
      path: string;
      level: number;
      expandable: boolean;
      isLoading: boolean;

    constructor(path: string) {
      let level = path.split("/").length - 1;
      if (path.endsWith("/")) {
        level = level - 1;
      }

      this.name = path.replace(/\/$/, "").match(/(.+\/)*([^\/]+)$/)[2];
      this.path = path;
      this.level = level;
      this.expandable = path.endsWith("/");
    }
}


export class DynamicDatabase {
  storageTree = new UserFileSystem();

  loadData(api: ApiService, creds: CredentialsService, filetreeComponent: FiletreeComponent, router: Router) {
    api.listDocuments("", creds.getCredentialsForApi())
        .then(res => {
          this.storageTree.buildFs(<Array<string>> res);

          filetreeComponent.dataSource.data = this.storageTree.rootLevelNodes()
              .map(path => new DynamicFlatNode(path));
        })
        .catch(err => {
          if (err.code === 401 || err.code === 403) {
            router.navigate(['']);
            return;
          }

          filetreeComponent.error = 'Listing storage failed: ' + ErrorMessageUtil.extract(err);
        });
  }

  rebuildView(filetreeComponent: FiletreeComponent) {
    let paths = this.memoizedFs();

    this.storageTree.buildFs(Array.from(paths));
    filetreeComponent.dataSource.data = this.storageTree.rootLevelNodes()
        .map(path => new DynamicFlatNode(path));
  }

  private memoizedFs() {
    let paths = new Set<string>();
    this.storageTree.fs.forEach((values, key) => {
      paths.add(key);
      values.forEach(file => {
        if (null != file) {
          paths.add(key + file);
        }
      })
    });
    return paths;
  }

  getChildren(node: string): string[] | undefined {
    return Array.from(this.storageTree.fs.get(node)).filter(res => res !== null).map(it => node + it);
  }
}
/**
 * File database, it can build a tree structured Json object from string.
 * Each node in Json object represents a file or a directory. For a file, it has filename and type.
 * For a directory, it has filename and children (a list of files or directories).
 * The input will be a json object string, and the output is a list of `FileNode` with nested
 * structure.
 */
@Injectable()
export class DynamicDataSource {

  private expandedMemoize = new Set<string>();

  dataChange = new BehaviorSubject<DynamicFlatNode[]>([]);

  get data(): DynamicFlatNode[] { return this.dataChange.value; }
  set data(value: DynamicFlatNode[]) {
    this.treeControl.dataNodes = value;
    this.dataChange.next(value);
    // keep expanded node visible:
    this.keepExpandedNodesState();
  }

  private keepExpandedNodesState() {
    let toExpand = new Set<string>(this.expandedMemoize);
    let expanded = false;
    do {
      expanded = false;
      this.treeControl.dataNodes
          .filter(it => toExpand.has(it.path))
          .forEach(node => {
            this.treeControl.expansionModel.select(node);
            expanded = true;
            toExpand.delete(node.path);
          });
    } while (toExpand.size != 0 && expanded);
  }

  constructor(private treeControl: FlatTreeControl<DynamicFlatNode>,
              private database: DynamicDatabase) {}

  connect(collectionViewer: CollectionViewer): Observable<DynamicFlatNode[]> {
    this.treeControl.expansionModel.changed.subscribe(change => {
      if ((change as SelectionChange<DynamicFlatNode>).added ||
          (change as SelectionChange<DynamicFlatNode>).removed) {
        this.handleTreeControl(change as SelectionChange<DynamicFlatNode>);
      }
    });

    return merge(collectionViewer.viewChange, this.dataChange).pipe(map(() => this.data));
  }

  /** Handle expand/collapse behaviors */
  handleTreeControl(change: SelectionChange<DynamicFlatNode>) {
    if (change.added) {
      change.added.forEach(node => this.toggleNode(node, true));
    }
    if (change.removed) {
      change.removed.slice().reverse().forEach(node => this.toggleNode(node, false));
    }
  }

  /**
   * Toggle the node, remove from display list
   */
  toggleNode(node: DynamicFlatNode, expand: boolean) {
    const children = this.database.getChildren(node.path);
    const index = this.data.indexOf(node);
    if (!children || index < 0) { // If no children, or cannot find the node, no op
      return;
    }

    if (expand) {
      this.expandedMemoize.add(node.path);
      const nodes = children.map(path => new DynamicFlatNode(path));
      this.data.splice(index + 1, 0, ...nodes);
    } else {
      this.expandedMemoize.delete(node.path);
      let count = 0;
      for (let i = index + 1; i < this.data.length && this.data[i].level > node.level; i++, count++) {}
      this.data.splice(index + 1, count);
    }

    this.dataChange.next(this.data);
  }
}

export interface NewFolderData {
  folderPath: string;
}

@Component({
  selector: 'add-folder-dialog',
  templateUrl: 'add.folder.dialog.html',
  styleUrls: ['add.folder.dialog.css'],
})
export class AddFolderDialog {

  constructor(
      public dialogRef: MatDialogRef<AddFolderDialog>,
      @Inject(MAT_DIALOG_DATA) public data: NewFolderData) {}

  onNoClick(): void {
    this.dialogRef.close();
  }
}

/**
 * @title Tree with dynamic data
 */
@Component({
  selector: 'file-tree',
  templateUrl: 'filetree.component.html',
  styleUrls: ['filetree.component.css'],
  providers: [DynamicDatabase]
})
export class FiletreeComponent {

  treeControl: FlatTreeControl<DynamicFlatNode>;
  dataSource: DynamicDataSource;
  getLevel = (node: DynamicFlatNode) => node.level;
  isExpandable = (node: DynamicFlatNode) => node.expandable;
  hasChild = (_: number, _nodeData: DynamicFlatNode) => _nodeData.expandable;
  error: any;

  constructor(private database: DynamicDatabase, private api: ApiService, private creds: CredentialsService,
              private router: Router, public dialog: MatDialog) {
    this.treeControl = new FlatTreeControl<DynamicFlatNode>(this.getLevel, this.isExpandable);
    this.dataSource = new DynamicDataSource(this.treeControl, database);

    database.loadData(api, creds, this, router);
  }

  addUiFolderWithPath(path: string) {
    const dialogRef = this.dialog.open(AddFolderDialog, {
      width: '250px',
      data: {folderPath: ""}
    });

    dialogRef.afterClosed().subscribe(result => {
      if (result !== undefined) {
        this.database.storageTree.uiCreatedFolders.add("" !== path ? path + result : result);
        this.database.rebuildView(this);
      }
    });
  }

  addUiFolder() {
    this.addUiFolderWithPath("");
  }

  addUiFolderWithpathFromName(event) {
    this.addUiFolderWithPath(event.currentTarget.name);
  }

  loadTree() {
    this.error = '';
    this.database.loadData(this.api, this.creds, this, this.router);
  }

  downloadFile(path: string) {
    this.error = '';
    this.api.downloadDocument(path, this.creds.getCredentialsForApi());
  }

  deleteFile(path: string) {
    this.error = '';
    this.removePathFromUiCreatedFolders(path);
    this.api.deleteDocument(path, this.creds.getCredentialsForApi())
        .then(res => this.loadTree())
        .catch(err => this.error = 'Delete failed: ' + ErrorMessageUtil.extract(err));
  }

  private removePathFromUiCreatedFolders(path: string) {
    let pathPrefix = path.replace(/\/$/, "");
    let toRemove = Array.from(this.database.storageTree.uiCreatedFolders)
        .filter(it => it.startsWith(pathPrefix));
    toRemove.forEach(remove => this.database.storageTree.uiCreatedFolders.delete(remove));
  }

  uploadFile(event) {
    this.error = '';
    this.api.uploadDocument(event.target.files[0], event.target.files[0].name, this.creds.getCredentialsForApi())
        .then(res => this.loadTree())
        .catch(err => {
          this.error = 'Upload failed: ' + ErrorMessageUtil.extract(err);
        });
  }

  uploadFileWithPathFromName(event) {
    this.error = '';
    this.api.uploadDocument(
        event.currentTarget.files[0],
        event.currentTarget.name + event.currentTarget.files[0].name,
        this.creds.getCredentialsForApi())
        .then(res => this.loadTree())
        .catch(err => {
          this.error = 'Upload failed: ' + ErrorMessageUtil.extract(err);
        });
  }

  refresh() {
    this.error = '';
    this.database.loadData(this.api, this.creds, this, this.router);
  }
}