repo_description: Angular Debugging and Visualization Tools
repo_topics: angular angular2 augury debugging devtools
code: import { ChangeDetectorRef, Component, NgZone, ErrorHandler, OnDestroy, DoCheck, OnInit } from '@angular/core';
  
  import { UncaughtErrorHandler } from './utils/uncaught-error-handler';
  import { reportUncaughtError } from '../utils/error-handling';
  
  import { Connection, DirectConnection } from './channel';
  
  import {
    ApplicationError,
    ApplicationErrorType,
    Message,
    MessageFactory,
    MessageResponse,
    MessageType,
    Subscription
  } from '../communication';
  
  import {
    ComponentInstanceState,
    ExpandState,
    Options,
    Tab,
    StateTab,
    Theme,
    ComponentViewState,
    AnalyticsConsent
  } from './state';
  
  import { Change, MutableTree, Node, Path, deserializeChangePath, serializePath, InstanceWithMetadata } from '../tree';
  
  import { createTree } from '../tree/mutable-tree-factory';
  import { UserActions } from './actions/user-actions/user-actions';
  import { Route } from '../backend/utils';
  import { select } from '@angular-redux/store';
  import { NgRedux } from '@angular-redux/store';
  import { IAppState } from './store/model';
  import { MainActions } from './actions/main-actions';
  
  require('!style!css!postcss!../styles/app.css');
  
  @Component({
    selector: 'bt-app',
    templateUrl: './app.html',
    styleUrls: ['./app.css']
  })
  export class App implements OnInit, DoCheck, OnDestroy {
    private Theme = Theme;
    private AnalyticsConsent = AnalyticsConsent;
  
    private componentState: ComponentInstanceState;
    private routerTree: Array<Route>;
    private ngModules: { [key: string]: any };
    private ngVersion: string;
    private selectedNode: Node;
    private subscription: Subscription;
    private tree: MutableTree;
    private error: ApplicationError = null;
    private unsubscribeUncaughtErrorListener;
    @select(store => store.main.selectedTab) selectedTab;
    @select(store => store.main.selectedComponentsSubTab) selectedComponentsSubTab;
    @select(store => store.main.DOMSelectionActive) domSelectionActive;
  
    constructor(
      private ngRedux: NgRedux<IAppState>,
      private mainActions: MainActions,
      private changeDetector: ChangeDetectorRef,
      private connection: Connection,
      private directConnection: DirectConnection,
      private options: Options,
      private userActions: UserActions,
      private viewState: ComponentViewState,
      private zone: NgZone,
      private errorHandler: ErrorHandler
    ) {
      // this should be our special ErrorHandler subclass which we can listen to
      if (this.errorHandler instanceof UncaughtErrorHandler) {
        this.unsubscribeUncaughtErrorListener = (<UncaughtErrorHandler>this.errorHandler).addListener((err: Error) => {
          this.error = new ApplicationError(ApplicationErrorType.UncaughtException, {
            name: err.name,
            message: err.message,
            stack: err.stack
          });
        });
      }
  
      this.componentState = new ComponentInstanceState(changeDetector);
  
      this.options.changes.subscribe(() => this.requestTree());
  
      this.options.load().then(() => this.changeDetector.detectChanges());
  
      this.viewState.changes.subscribe(() => this.changeDetector.detectChanges());
  
      this.mainActions.initializeAugury();
    }
  
    hasContent() {
      return this.tree && this.tree.roots && this.tree.roots.length > 0;
    }
  
    ngDoCheck() {
      this.selectedNode = this.viewState.selectedTreeNode(this.tree);
      this.changeDetector.detectChanges();
    }
  
    ngOnInit() {
      this.subscription = this.connection.subscribe(this.onReceiveMessage.bind(this));
  
      this.connection.reconnect().then(() => this.requestTree());
    }
  
    ngOnDestroy() {
      if (this.subscription) {
        this.subscription.unsubscribe();
      }
  
      if (this.unsubscribeUncaughtErrorListener) {
        this.unsubscribeUncaughtErrorListener();
      }
  
      this.connection.close();
    }
  
    onSelectNode(node: Node, beforeLoad?: () => void) {
      this.selectedNode = node;
  
      if (node == null) {
        this.viewState.unselect();
        return;
      }
  
      this.viewState.select(node);
  
      const m = MessageFactory.selectComponent(node, node.isComponent);
      const promise = this.directConnection.handleImmediate(m).then(response => {
        if (!response) {
          return;
        }
  
        if (typeof beforeLoad === 'function') {
          beforeLoad();
        }
  
        const { instance, metadata, providers, componentMetadata } = response;
  
        return <InstanceWithMetadata>{
          instance,
          providers,
          metadata: new Map(metadata),
          componentMetadata: new Map(componentMetadata)
        };
      });
  
      this.componentState.wait(node, promise);
    }
  
    onInspectElement(node: Node) {
      chrome.devtools.inspectedWindow.eval(`inspect(inspectedApplication.nodeFromPath('${node.id}'))`);
    }
  
    onCollapseChildren(node: Node) {
      this.recursiveExpansionState(node, ExpandState.Collapsed);
    }
  
    onExpandChildren(node: Node) {
      this.recursiveExpansionState(node, ExpandState.Expanded);
    }
  
    onReportError() {
      if (this.error && this.error.errorType === ApplicationErrorType.UncaughtException) {
        reportUncaughtError(this.error.error, this.ngVersion);
        this.error = null;
      }
    }
  
    onSelectedTabChange(tab: Tab) {
      this.routerTree = this.routerTree ? [].concat(this.routerTree) : null;
      this.mainActions.selectTab(tab);
    }
  
    onSelectedComponentsSubTabMenuChange(tab: StateTab) {
      this.mainActions.selectComponentsSubTab(tab);
    }
  
    onDOMSelectionActiveChange(state: boolean) {
      this.mainActions.setDOMSelectionActive(state);
    }
  
    onEmitValue(data) {
      this.mainActions.emitValue(data.path, data.data);
    }
  
    onUpdateProperty(data) {
      this.mainActions.updateProperty(data.path, data.data);
    }
  
    private requestTree() {
      const options = this.options.simpleOptions();
  
      this.connection.send(MessageFactory.initialize(options)).catch(error => {
        this.error = new ApplicationError(ApplicationErrorType.UncaughtException, error);
  
        this.changeDetector.detectChanges();
      });
    }
  
    private refresh() {
      this.connection.send(MessageFactory.refresh());
    }
  
    onRefresh() {
      this.error = null;
      this.refresh();
    }
  
    private restoreSelection() {
      this.selectedNode = this.viewState.selectedTreeNode(this.tree);
  
      this.onSelectNode(this.selectedNode, () => this.componentState.reset());
    }
  
    private processMessage(msg: Message<any>, sendResponse: (response: MessageResponse<any>) => void) {
      const respond = () => {
        sendResponse(MessageFactory.response(msg, { processed: true }, false));
      };
  
      switch (msg.messageType) {
        case MessageType.Ping:
          this.restoreSelection();
          break;
        case MessageType.NotNgApp:
          this.error = new ApplicationError(ApplicationErrorType.NotNgApp);
          respond();
          break;
        case MessageType.Push:
          this.directConnection.readQueue((innerMessage, innerRespond) =>
            this.processMessage(innerMessage, innerRespond)
          );
          break;
        case MessageType.CompleteTree:
          this.createTree(msg.content);
          respond();
          break;
        case MessageType.TreeDiff:
          if (this.tree == null) {
            this.connection.send(MessageFactory.initialize(this.options.simpleOptions())); // request tree
          } else {
            this.updateTree(msg.content);
          }
          respond();
          break;
        case MessageType.TreeUnchanged:
          this.restoreSelection();
          break;
        case MessageType.RouterTree: // TODO(cbond): support router tree diff
          this.routerTree = msg.content;
          respond();
          break;
        case MessageType.NgModules:
          this.ngModules = msg.content;
          respond();
          break;
        case MessageType.NgVersion:
          this.ngVersion = msg.content;
          respond();
          break;
        case MessageType.FindElement:
          if (msg.content.node) {
            this.viewState.select(msg.content.node);
            this.viewState.expandState(msg.content.node, ExpandState.Expanded);
            this.mainActions.setDOMSelectionActive(true);
            if (msg.content.stop) {
              this.userActions.cancelFindElement();
              this.mainActions.setDOMSelectionActive(false);
            }
            break;
          }
          break;
  
        case MessageType.ApplicationError:
          this.error = msg.content;
          respond();
          break;
        case MessageType.ErrorCleared:
          if (this.error && msg.content.errorTypes.includes(this.error.errorType)) {
            this.error = null;
          }
          respond();
          break;
      }
    }
  
    private createTree(roots: Array<Node>) {
      this.componentState.reset();
  
      this.tree = createTree(roots);
      this.restoreSelection();
    }
  
    private updateTree(changes) {
      /// Patch the treee
      this.tree.patch(changes);
  
      /// This operation must happen after the tree patch
      const changedIdentifiers = this.extractIdentifiersFromChanges(changes);
  
      /// Highlight the nodes that have changed
      this.viewState.nodesChanged(changedIdentifiers);
  
      this.restoreSelection();
    }
  
    private onReceiveMessage(msg: Message<any>, sendResponse: (response: MessageResponse<any>) => void) {
      this.zone.run(() => this.processMessage(msg, sendResponse));
    }
  
    private extractIdentifiersFromChanges(changes: Array<Change>): string[] {
      const identifiers = new Set<string>();
  
      for (const change of changes) {
        const path = this.nodePathFromChangePath(deserializeChangePath(change.path));
  
        identifiers.add(serializePath(path));
      }
  
      const results = new Array<string>();
  
      identifiers.forEach(id => results.push(id));
  
      return results;
    }
  
    private nodePathFromChangePath(changePath: Path) {
      const result = new Array<string | number>();
  
      for (let index = 0; index < changePath.length; ++index) {
        switch (changePath[index]) {
          case 'roots':
          case 'children':
            result.push(changePath[++index]);
            break;
        }
      }
  
      return result;
    }
  
    private recursiveExpansionState(from: Node, state: ExpandState) {
      const apply = (node: Node) => {
        this.viewState.expandState(node, state);
  
        node.children.forEach(n => apply(n));
      };
  
      apply(from);
    }
  }
  