From bd60d2149bc9b53c8f0f3e40eb1a4be345eb1518 Mon Sep 17 00:00:00 2001
From: "George Gelashvili, Matt Kleiman and Oliver Switzer"
 <pair+ggelashvili+mkleiman+oswitzer@pivotal.io>
Date: Thu, 20 Apr 2017 17:43:56 -0400
Subject: [PATCH 2/2] Add a tree implemented in React

---
 web/karma.conf.js                                  |  27 ----
 .../browser/templates/browser/js/browser.js        |  44 ++----
 web/pgadmin/static/jsx/components.jsx              |   2 +
 .../jsx/tree_menu/components/server_tree.jsx       |  50 +++++++
 .../static/jsx/tree_menu/components/tree_node.jsx  |  61 ++++++++
 .../jsx/tree_menu/services/server_node_service.js  |  58 ++++++++
 .../tree_menu/components/server_tree_spec.jsx      |  60 ++++++++
 .../tree_menu/components/tree_node_spec.jsx        | 116 ++++++++++++++++
 .../services/server_node_service_spec.jsx          | 154 +++++++++++++++++++++
 9 files changed, 513 insertions(+), 59 deletions(-)
 create mode 100644 web/pgadmin/static/jsx/tree_menu/components/server_tree.jsx
 create mode 100644 web/pgadmin/static/jsx/tree_menu/components/tree_node.jsx
 create mode 100644 web/pgadmin/static/jsx/tree_menu/services/server_node_service.js
 create mode 100644 web/regression/javascript/tree_menu/components/server_tree_spec.jsx
 create mode 100644 web/regression/javascript/tree_menu/components/tree_node_spec.jsx
 create mode 100644 web/regression/javascript/tree_menu/services/server_node_service_spec.jsx

diff --git a/web/karma.conf.js b/web/karma.conf.js
index d3bf3d15..55003ad6 100644
--- a/web/karma.conf.js
+++ b/web/karma.conf.js
@@ -60,10 +60,6 @@ module.exports = function conf(config) {
             }
           },
           {
-            test: /.*jquery\.aci.*/,
-            loader: 'imports-loader?jQuery=jquery'
-          },
-          {
             test: /.*slickgrid\/slick\.(?!core)*/,
             loader: 'imports-loader?' +
             'jquery.ui' +
@@ -101,27 +97,6 @@ module.exports = function conf(config) {
             ',jquery.event.drag' +
             '!exports-loader?' +
             'Slick.Grid'
-          },
-          {
-            test: /.*tree_menu.*/,
-            loader: 'imports-loader?' +
-            'jquery.ui' +
-            ',jquery.event.drag' +
-            ',aciPlugin'
-          },
-          {
-            test: /.*jquery\.aciPlugin.*/,
-            loader: 'imports-loader?' +
-            'jquery.ui' +
-            ',jquery.event.drag' +
-            ',this=>window'
-          },
-          {
-            test: /.*jquery\.aciTree.*/,
-            loader: 'imports-loader?' +
-            'jquery.ui' +
-            ',jquery.event.drag' +
-            ',this=>window'
           }
         ]
       },
@@ -136,8 +111,6 @@ module.exports = function conf(config) {
         ],
         extensions: ['.js', '.jsx'],
         alias: {
-          'aciPlugin': sourcesDir + '/vendor/aciTree/jquery.aciPlugin.min',
-          'aciTree': sourcesDir + '/vendor/aciTree/jquery.aciTree',
           'alertify': sourcesDir + '/vendor/alertifyjs/alertify',
           'jquery': sourcesDir + '/vendor/jquery/jquery-1.11.2',
           'jquery.ui': sourcesDir + '/vendor/jquery-ui/jquery-ui-1.11.3',
diff --git a/web/pgadmin/browser/templates/browser/js/browser.js b/web/pgadmin/browser/templates/browser/js/browser.js
index a663ae86..062b8554 100644
--- a/web/pgadmin/browser/templates/browser/js/browser.js
+++ b/web/pgadmin/browser/templates/browser/js/browser.js
@@ -1,14 +1,16 @@
 define('pgadmin.browser',
         ['require', 'jquery', 'underscore', 'underscore.string', 'bootstrap',
-        'pgadmin', 'alertify', 'codemirror', 'codemirror/mode/sql/sql', 'wcdocker',
-        'jquery.contextmenu', 'jquery.aciplugin', 'jquery.acitree',
+        'pgadmin', 'alertify', 'codemirror',
+        'sources/generated/reactComponents',
+        'codemirror/mode/sql/sql', 'wcdocker',
+        'jquery.contextmenu',
         'pgadmin.alertifyjs', 'pgadmin.browser.messages',
         'pgadmin.browser.menu', 'pgadmin.browser.panel',
         'pgadmin.browser.error', 'pgadmin.browser.frame',
         'pgadmin.browser.node', 'pgadmin.browser.collection'
 
        ],
-function(require, $, _, S, Bootstrap, pgAdmin, Alertify, CodeMirror) {
+function(require, $, _, S, Bootstrap, pgAdmin, Alertify, CodeMirror, reactComponents) {
 
   // Some scripts do export their object in the window only.
   // Generally the one, which do no have AMD support.
@@ -44,40 +46,18 @@ function(require, $, _, S, Bootstrap, pgAdmin, Alertify, CodeMirror) {
     _.each(data, function(d){
       d._label = d.label;
       d.label = _.escape(d.label);
-    })
+    });
     return data;
   };
 
   var initializeBrowserTree = pgAdmin.Browser.initializeBrowserTree =
-    function(b) {
-      $('#tree').aciTree({
-        ajax: {
-          url: '{{ url_for('browser.get_nodes') }}',
-          converters: {
-            'text json': processTreeData,
-          }
-        },
-        ajaxHook: function(item, settings) {
-          if (item != null) {
-            var d = this.itemData(item);
-            n = b.Nodes[d._type];
-            if (n)
-              settings.url = n.generate_url(item, 'children', d, true);
-          }
-        },
-        loaderDelay: 100,
-        show: {
-          duration: 75
-        },
-        hide: {
-          duration: 75
-        },
-        view: {
-          duration: 75
-        }
-      });
+    function(browser) {
+
+      serverTreeElement = reactComponents.React.createElement(
+        reactComponents.ServerTree, {serverGroups: ['Servers']});
+      reactComponents.render(serverTreeElement, $('#tree')[0]);
+
 
-      b.tree = $('#tree').aciTree('api');
     };
 
   // Extend the browser class attributes
diff --git a/web/pgadmin/static/jsx/components.jsx b/web/pgadmin/static/jsx/components.jsx
index 63b72535..edf99e42 100644
--- a/web/pgadmin/static/jsx/components.jsx
+++ b/web/pgadmin/static/jsx/components.jsx
@@ -1,7 +1,9 @@
+import {ServerTree} from "./tree_menu/components/server_tree.jsx";
 import {render} from "react-dom";
 import React from "react";
 
 export {
+  ServerTree,
   render,
   React
 }
\ No newline at end of file
diff --git a/web/pgadmin/static/jsx/tree_menu/components/server_tree.jsx b/web/pgadmin/static/jsx/tree_menu/components/server_tree.jsx
new file mode 100644
index 00000000..7f81cd40
--- /dev/null
+++ b/web/pgadmin/static/jsx/tree_menu/components/server_tree.jsx
@@ -0,0 +1,50 @@
+import React from "react";
+import {ServerNodeService} from "../services/server_node_service";
+import {TreeNode} from "./tree_node";
+
+export class ServerTree extends React.Component {
+
+  constructor(props) {
+    super(props);
+    this.state = {
+      expanded: false,
+      serverGroups: []
+    };
+    self = this;
+
+    ServerNodeService.fetchChildrenFrom({nodeType: "nodes", nodeId: null}).then((nodes) => {
+      self.setState({serverGroups: nodes})
+    });
+  }
+
+  render() {
+    return (
+      <ul>
+        {_.map(this.state.serverGroups, (node, index) =>
+            <li className="closed" key={index}>
+              <TreeNode
+                name={node.name}
+                nodeType={node.nodeType}
+                nodeId={node.nodeId}
+              />
+            </li>)}
+      </ul>);
+  }
+}
+
+ServerTree.propTypes = {
+};
+
+
+  // <ul>
+  //       {function() {
+  //         return _.map(this.state.children, (node, index) => {
+  //           {/*debugger;*/}
+  //           return <li className="closed" key={index}>
+  //             <TreeNode
+  //               name={node.name}/>
+  //           </li>
+  //         })
+  //         }.call(this)}
+  //     </ul>);
+
diff --git a/web/pgadmin/static/jsx/tree_menu/components/tree_node.jsx b/web/pgadmin/static/jsx/tree_menu/components/tree_node.jsx
new file mode 100644
index 00000000..aa104e18
--- /dev/null
+++ b/web/pgadmin/static/jsx/tree_menu/components/tree_node.jsx
@@ -0,0 +1,61 @@
+import React from "react";
+import {ServerNodeService} from "../services/server_node_service"
+
+export class TreeNode extends React.Component {
+
+  constructor(props) {
+    super(props);
+    this.state = {
+      name: props.name,
+      nodeType: props.nodeType,
+      nodeId: props.nodeId,
+      expanded: false,
+      childNodes: []
+    };
+    this.onToggle = this.onToggle.bind(this);
+
+    this.styles = {
+      children: {
+
+        marginLeft: "-20px"
+      }
+    }
+  }
+
+  onToggle() {
+    let callArgs = {nodeType: this.props.nodeType, nodeId: this.props.nodeId};
+    ServerNodeService.fetchChildrenFrom(callArgs).then((newChildren) => {
+      this.setState({childNodes: newChildren, expanded: !this.state.expanded});
+    });
+  }
+
+  displayChildNodes() {
+    if(this.state.expanded) {
+      return (
+        <ul>
+          {_.map(this.state.childNodes, (node, index) => <li key={index}><TreeNode
+            key={index}
+            name={node.name}
+            nodeType={node.nodeType}
+            nodeId={node.nodeId}
+          /></li>
+          )}
+        </ul>
+      );
+    }
+  }
+
+  render() {
+    return (
+      <div className={this.state.expanded ? "opened" : "closed"}>
+        <div className="node-name" onClick={this.onToggle}>{this.state.name}</div>
+        <div style={this.styles.children}>{this.displayChildNodes()}</div>
+      </div>);
+  }
+}
+
+TreeNode.propTypes = {
+  name: React.PropTypes.string.isRequired,
+  nodeType: React.PropTypes.string.isRequired,
+  nodeId: React.PropTypes.number.isRequired
+};
diff --git a/web/pgadmin/static/jsx/tree_menu/services/server_node_service.js b/web/pgadmin/static/jsx/tree_menu/services/server_node_service.js
new file mode 100644
index 00000000..839734b4
--- /dev/null
+++ b/web/pgadmin/static/jsx/tree_menu/services/server_node_service.js
@@ -0,0 +1,58 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2017, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////////////////
+
+import axios from 'axios';
+
+// baseUrl = "/browser";
+
+export class ServerNodeService {
+  static fetchChildrenFrom(parent) {
+    return axios.get(ServerNodeService.buildUrl(parent))
+      .then((response) => {
+        return ServerNodeService.createNodeChildren(response['data'])
+      })
+      .catch((error) => {
+        if (error.response) {
+          ServerNodeService.errorCallback(error.response.errormsg);
+        }
+      });
+  };
+
+  static baseUrl() {
+    return "/browser";
+  };
+
+  static buildUrl(parent) {
+    let url = ServerNodeService.baseUrl() + '/' + parent.nodeType;
+    if (parent.nodeType != 'nodes'){
+      url += '/children/';
+      if (parent.nodeType != 'server-group') {
+        url += '1/'
+      }
+      url += parent.nodeId;
+    }
+    return url;
+  };
+
+  static createNodeChildren(response) {
+    let nodeChildren = _.map(response['data'], function (datum) {
+      let changedNodeId = parseInt(datum['id'].split('/').pop());
+      return {
+        name: datum['label'],
+        nodeType: datum['_type'],
+        nodeId: changedNodeId
+      }
+    });
+    return nodeChildren;
+  };
+
+  static setErrorCallback(callback) {
+    ServerNodeService.errorCallback = callback;
+  };
+}
diff --git a/web/regression/javascript/tree_menu/components/server_tree_spec.jsx b/web/regression/javascript/tree_menu/components/server_tree_spec.jsx
new file mode 100644
index 00000000..79ba94d9
--- /dev/null
+++ b/web/regression/javascript/tree_menu/components/server_tree_spec.jsx
@@ -0,0 +1,60 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2017, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////////////////
+
+import React from "react";
+import {ServerTree} from "../../../../pgadmin/static/jsx/tree_menu/components/server_tree";
+import {TreeNode} from "../../../../pgadmin/static/jsx/tree_menu/components/tree_node";
+import { mount } from 'enzyme';
+import {ServerNodeService} from "../../../../pgadmin/static/jsx/tree_menu/services/server_node_service.js";
+
+
+describe("ServerTree", () => {
+  let topLevelResponse;
+  beforeEach(() => {
+    topLevelResponse = [{
+      name: 'Servers',
+      nodeType: 'server-group',
+      nodeId: 1,
+    }];
+
+    spyOn(ServerNodeService, 'fetchChildrenFrom').and.returnValue(new Promise((resolve) => {
+      resolve(topLevelResponse)
+    }));
+  });
+
+  it("initializes the top TreeNode with [nodes and null nodeId]");
+
+  it("renders the top Tree Node as closed", (done) => {
+
+    const serverTree = mount(
+      <ServerTree/>);
+
+    setImmediate(() => {
+      expect(serverTree.find("TreeNode").length).toBe(1);
+      done();
+    });
+  });
+
+  it("renders the top Tree Node as 'Servers'", (done) => {
+
+    const serverTree = mount(
+      <ServerTree
+      />);
+
+    setImmediate(() => {
+      let treeNode = serverTree.find("TreeNode")//.childAt(0);
+      expect(treeNode.text()).toBe("Servers");
+      done();
+    });
+  });
+
+  describe("when there are multiple Server Groups", () => {
+    it("renders multiple Server Group TreeNodes");
+  });
+});
\ No newline at end of file
diff --git a/web/regression/javascript/tree_menu/components/tree_node_spec.jsx b/web/regression/javascript/tree_menu/components/tree_node_spec.jsx
new file mode 100644
index 00000000..3ee1fab3
--- /dev/null
+++ b/web/regression/javascript/tree_menu/components/tree_node_spec.jsx
@@ -0,0 +1,116 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2017, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////////////////
+
+import React from "react";
+import {TreeNode} from "../../../../pgadmin/static/jsx/tree_menu/components/tree_node";
+import {ServerNodeService} from "../../../../pgadmin/static/jsx/tree_menu/services/server_node_service.js";
+import {mount} from 'enzyme';
+
+describe("TreeNode", () => {
+
+  it("renders itself as closed", () => {
+    const treeNode = mount(<TreeNode
+      name="some server"
+      nodeType="server"
+      nodeId={1}
+    />);
+
+    expect(treeNode.hasClass('closed')).toBe(true);
+    expect(treeNode.find("TreeNode").length).toBe(1);
+  });
+
+  it("renders itself as its constructed name prop", () => {
+    const treeNode = mount(<TreeNode
+      name="some server"
+      nodeType="server"
+      nodeId={1}
+    />);
+
+    expect(treeNode.text()).toBe("some server");
+  });
+
+  describe("when the user opens the tree node", () => {
+    let treeNode;
+
+    beforeEach(function () {
+      treeNode = mount(<TreeNode
+        name="Servers"
+        nodeType="server-group"
+        nodeId={1}
+      />);
+      let response = [
+        {
+          name: "a server",
+          nodeType: "server",
+          nodeId: 1
+        },
+        {
+          name: "another server",
+          nodeType: "server",
+          nodeId: 2
+        }
+      ];
+
+      spyOn(ServerNodeService, 'fetchChildrenFrom').and.returnValue(
+        new Promise((resolve) => {
+          resolve(response)
+        })
+      );
+    });
+
+    it("calls service to fetch children correctly", () => {
+
+      treeNode.find('.node-name').simulate('click');
+
+      let parentArgs = {nodeType: "server-group", nodeId: 1};
+      expect(ServerNodeService.fetchChildrenFrom).toHaveBeenCalledWith(parentArgs);
+    });
+
+    it("renders the node as opened", () => {
+
+      treeNode.find('.node-name').simulate('click');
+
+      setImmediate(() => {
+        expect(treeNode.hasClass('closed')).toBe(false);
+        expect(treeNode.hasClass('opened')).toBe(true);
+      });
+    });
+
+    it("renders tree node children with names", () => {
+      treeNode.find('.node-name').simulate('click');
+
+      setImmediate(() => {
+        expect(treeNode.find('TreeNode').length).toBe(1 + 2);
+        expect(treeNode.text()).toContain("a server");
+        expect(treeNode.text()).toContain("another server");
+      });
+    });
+
+    describe("when the user clicks again on the node", function () {
+      it("should close it", function (done) {
+        treeNode.find('.node-name').simulate('click');
+        treeNode.find('.node-name').simulate('click');
+        setImmediate(() => {
+          expect(treeNode.find("TreeNode").length).toBe(1);
+          expect(treeNode.hasClass('closed')).toBe(true);
+          expect(treeNode.hasClass('opened')).toBe(false);
+          done();
+        });
+      });
+    });
+
+    it("renders the node children as 'loading...'");
+    describe("when the data loading succeeds", () => {
+
+      it("renders tree node children with icons corresponding to their types", () => {
+
+      });
+    });
+  });
+});
\ No newline at end of file
diff --git a/web/regression/javascript/tree_menu/services/server_node_service_spec.jsx b/web/regression/javascript/tree_menu/services/server_node_service_spec.jsx
new file mode 100644
index 00000000..7aac0e0e
--- /dev/null
+++ b/web/regression/javascript/tree_menu/services/server_node_service_spec.jsx
@@ -0,0 +1,154 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2017, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////////////////
+
+import {ServerNodeService} from "../../../../pgadmin/static/jsx/tree_menu/services/server_node_service";
+import axios from 'axios';
+
+describe('ServerNodeService', () => {
+  describe('fetchChildrenFrom', () => {
+    let ajaxResponse, url, parentNode;
+
+    beforeEach(function () {
+      parentNode = {nodeType: "parentType", nodeId: 1};
+      url = '/browser/nodes/';
+
+      ajaxResponse = {
+        data: {
+          info: "",
+          errormsg: "",
+          data: [
+            {
+              _type: "type1",
+              in_recovery: null,
+              server_type: "pg",
+              db: "postgres",
+              module: "module.one",
+              connected: false,
+              user: null,
+              inode: true,
+              id: "longer/path/to/server/1",
+              icon: "icon-server-not-connected",
+              _pid: 1,
+              label: "label one",
+              version: null,
+              _id: "1",
+              wal_pause: null
+            },
+            {
+              _type: "type2",
+              in_recovery: null,
+              server_type: "pg",
+              db: "postgres",
+              module: "module.two",
+              connected: false,
+              user: null,
+              inode: true,
+              id: "longer/path/to/server/2",
+              icon: "icon-server-not-connected",
+              _pid: 1,
+              label: "label two",
+              version: null,
+              _id: "1",
+              wal_pause: null
+            }
+          ],
+          result: null,
+          success: 1
+        }
+      };
+    });
+
+    it("it fetches the browser node");
+
+    describe("calls the right place", () => {
+      beforeEach(function () {
+        spyOn(axios, 'get').and.returnValue(new Promise(function (resolve, reject) {
+        }));
+      });
+
+      it("when called for the top level", () => {
+
+        let parentArgs = {nodeType: "nodes", nodeId: null};
+        ServerNodeService.fetchChildrenFrom(parentArgs);
+
+        let expectedUrl = "/browser/nodes";
+        expect(axios.get).toHaveBeenCalledWith(expectedUrl);
+      });
+
+      it("when called for the second level", () => {
+        let parentArgs = {nodeType: "server-group", nodeId: 3};
+
+        ServerNodeService.fetchChildrenFrom(parentArgs);
+
+        let expectedUrl = "/browser/server-group/children/3";
+        expect(axios.get).toHaveBeenCalledWith(expectedUrl);
+      });
+
+      it("when called for deeper nodes", () => {
+        let parentArgs = {nodeType: "server", nodeId: 3};
+
+        ServerNodeService.fetchChildrenFrom(parentArgs);
+
+        let expectedUrl = "/browser/server/children/1/3";
+        expect(axios.get).toHaveBeenCalledWith(expectedUrl);
+      });
+
+    });
+
+    describe("when backend is not connected to server", () => {
+      it("calls a callback with the error message", (done) => {
+        let ajaxErrorResponse = {
+          response: {
+            info: "",
+            errormsg: "Connection to the server has been lost.",
+            data: null,
+            result: null,
+            success: 0
+          }
+        };
+
+        spyOn(axios, 'get').and.returnValue(new Promise((resolve, reject) => {
+          reject(ajaxErrorResponse)
+        }));
+
+        let errorCallback = jasmine.createSpy('errorCallback');
+        ServerNodeService.setErrorCallback(errorCallback);
+
+        ServerNodeService.fetchChildrenFrom(parentNode).then(() => {
+          expect(errorCallback).toHaveBeenCalledWith("Connection to the server has been lost.");
+          done();
+        });
+      });
+    });
+
+    describe("when the call is successful", () => {
+      it("converts the data into the expected format", (done) => {
+        spyOn(axios, 'get').and.returnValue(new Promise((resolve) => {
+          resolve(ajaxResponse)
+        }));
+
+        let expectedResult = [{
+          name: "label one",
+          nodeType: "type1",
+          nodeId: 1
+        }, {
+          name: "label two",
+          nodeType: "type2",
+          nodeId: 2
+        }];
+
+        ServerNodeService.fetchChildrenFrom(parentNode).then((children) => {
+          expect(children).toEqual(expectedResult);
+          done();
+        });
+      });
+    });
+
+  });
+});
-- 
2.12.0

