diff --git a/web/pgadmin/static/js/tree/tree.js b/web/pgadmin/static/js/tree/tree.js
new file mode 100644
index 00000000..ed67aa85
--- /dev/null
+++ b/web/pgadmin/static/js/tree/tree.js
@@ -0,0 +1,134 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////////////////
+export class TreeNode {
+  constructor(id, data, parent) {
+    this.id = id;
+    this.data = data;
+    this.parentNode = parent;
+    this.path = this.id;
+    if (parent !== null && parent !== undefined && parent.path !== undefined) {
+      this.path = parent.path + '.' + id;
+    }
+    this.children = [];
+  }
+
+  hasParent() {
+    return this.parentNode !== null && this.parentNode !== undefined;
+  }
+
+  parent() {
+    return this.parentNode;
+  }
+
+  getData() {
+    if (this.data === undefined) {
+      return undefined;
+    } else if (this.data === null) {
+      return null;
+    }
+    return Object.assign({}, this.data);
+  }
+}
+
+export class Tree {
+  constructor() {
+    this.rootNode = new TreeNode(undefined, {});
+    this.aciTreeApi = undefined;
+  }
+
+  addNewNode(id, data, path) {
+    const parent = this.findNode(path);
+    return this.createOrUpdateNode(id, data, parent);
+  }
+
+  findNode(path) {
+    if (path.length === 0) {
+      return this.rootNode;
+    }
+    return findInTree(this.rootNode, path.join('.'));
+  }
+
+  findNodeByDomElement(domElement) {
+    const path = this.translateTreeNodeIdFromACITree(domElement);
+    if(!path || !path[0]) {
+      return undefined;
+    }
+
+    return this.findNode(path);
+  }
+
+  selected() {
+    return this.aciTreeApi.selected();
+  }
+
+  register($treeJQuery) {
+    $treeJQuery.on('acitree', function (event, api, item, eventName) {
+      if (api.isItem(item)) {
+        if (eventName === 'added') {
+          const id = api.getId(item);
+          const data = api.itemData(item);
+          const parentId = this.translateTreeNodeIdFromACITree(api.parent(item));
+          this.addNewNode(id, data, parentId);
+        }
+      }
+    }.bind(this));
+    this.aciTreeApi = $treeJQuery.aciTree('api');
+  }
+
+  createOrUpdateNode(id, data, parent) {
+    const oldNode = this.findNode([parent.path, id]);
+    if (oldNode !== null) {
+      oldNode.data = Object.assign({}, data);
+      return oldNode;
+    }
+
+    const node = new TreeNode(id, data, parent);
+    if (parent === this.rootNode) {
+      node.parentNode = null;
+    }
+    parent.children.push(node);
+    return node;
+  }
+
+  translateTreeNodeIdFromACITree(aciTreeNode) {
+    let currentTreeNode = aciTreeNode;
+    let path = [];
+    while (currentTreeNode !== null && currentTreeNode !== undefined && currentTreeNode.length > 0) {
+      path.unshift(this.aciTreeApi.getId(currentTreeNode));
+      if (this.aciTreeApi.hasParent(currentTreeNode)) {
+        currentTreeNode = this.aciTreeApi.parent(currentTreeNode);
+      } else {
+        break;
+      }
+    }
+    return path;
+  }
+}
+
+export let tree = new Tree();
+
+function findInTree(rootNode, path) {
+  if (path === null) {
+    return rootNode;
+  }
+  return (function findInNode(currentNode) {
+    for (let i = 0, length = currentNode.children.length; i < length; i++) {
+      const calculatedNode = findInNode(currentNode.children[i]);
+      if (calculatedNode !== null) {
+        return calculatedNode;
+      }
+    }
+
+    if (currentNode.path === path) {
+      return currentNode;
+    } else {
+      return null;
+    }
+  })(rootNode);
+}
diff --git a/web/regression/javascript/tree/tree_fake.js b/web/regression/javascript/tree/tree_fake.js
new file mode 100644
index 00000000..a0983c67
--- /dev/null
+++ b/web/regression/javascript/tree/tree_fake.js
@@ -0,0 +1,55 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {Tree} from "../../../pgadmin/static/js/tree/tree";
+
+export class TreeFake extends Tree {
+  constructor() {
+    super();
+    this.aciTreeToOurTreeTranslator = {};
+  }
+
+  addNewNode(id, data, path) {
+    this.aciTreeToOurTreeTranslator[id] = path.concat(id);
+    return super.addNewNode(id, data, path);
+  }
+
+  hasParent(aciTreeNode) {
+    return this.translateTreeNodeIdFromACITree(aciTreeNode).length > 1;
+  }
+
+  parent(aciTreeNode) {
+    if (this.hasParent(aciTreeNode)) {
+      let path = this.translateTreeNodeIdFromACITree(aciTreeNode);
+      return [{id: this.findNode(path).parent().id}];
+    }
+
+    return null;
+  }
+
+  translateTreeNodeIdFromACITree(aciTreeNode) {
+    return this.aciTreeToOurTreeTranslator[aciTreeNode[0].id];
+  }
+
+  itemData(aciTreeNode) {
+    let path = this.translateTreeNodeIdFromACITree(aciTreeNode);
+    if(path === undefined || path === null) {
+      return undefined;
+    }
+    return this.findNode(path).getData();
+  }
+
+  selected() {
+    return this.selectedNode;
+  }
+
+  setSelectedNode(selectedNode) {
+    this.selectedNode = selectedNode;
+  }
+}
diff --git a/web/regression/javascript/tree/tree_spec.js b/web/regression/javascript/tree/tree_spec.js
new file mode 100644
index 00000000..e8930d84
--- /dev/null
+++ b/web/regression/javascript/tree/tree_spec.js
@@ -0,0 +1,284 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////////////////
+
+import {Tree, TreeNode} from "../../../pgadmin/static/js/tree/tree";
+import {TreeFake} from "./tree_fake";
+
+const context = describe;
+
+const treeTests = (treeClass, setDefaultCallBack) => {
+  let tree;
+  beforeEach(() => {
+    tree = new treeClass();
+  });
+
+  describe('#addNewNode', () => {
+    describe('when add a new root element', () => {
+      beforeEach(() => {
+        tree.addNewNode('some new node', {data: 'interesting'}, []);
+      });
+
+      it('can be retrieved', () => {
+        const node = tree.findNode(['some new node']);
+        expect(node.data).toEqual({data: 'interesting'});
+      });
+
+      it('return false for #hasParent()', () => {
+        const node = tree.findNode(['some new node']);
+        expect(node.hasParent()).toBe(false);
+      });
+
+      it('return null for #parent()', () => {
+        const node = tree.findNode(['some new node']);
+        expect(node.parent()).toBeNull();
+      });
+    });
+
+    describe('when add a new element as a child', () => {
+      let parentNode;
+      beforeEach(() => {
+        parentNode = tree.addNewNode('parent node', {data: 'parent data'}, []);
+        tree.addNewNode('some new node', {data: 'interesting'}, ['parent' +
+        ' node']);
+      });
+
+      it('can be retrieved', () => {
+        const node = tree.findNode(['parent node', 'some new node']);
+        expect(node.data).toEqual({data: 'interesting'});
+      });
+
+      it('return true for #hasParent()', () => {
+        const node = tree.findNode(['parent node', 'some new node']);
+        expect(node.hasParent()).toBe(true);
+      });
+
+      it('return "parent node" object for #parent()', () => {
+        const node = tree.findNode(['parent node', 'some new node']);
+        expect(node.parent()).toEqual(parentNode);
+      });
+    });
+
+    describe('when add an element that already exists under a parent', () => {
+      let parentNode;
+      beforeEach(() => {
+        parentNode = tree.addNewNode('parent node', {data: 'parent data'}, []);
+        tree.addNewNode('some new node', {data: 'interesting'}, ['parent' +
+        ' node']);
+      });
+
+      it('does not add a new child', () => {
+        tree.addNewNode('some new node', {data: 'interesting 1'}, ['parent' +
+        ' node']);
+        const parentNode = tree.findNode(['parent node']);
+        expect(parentNode.children.length).toBe(1);
+      });
+
+      it('updates the existing node data', () => {
+        tree.addNewNode('some new node', {data: 'interesting 1'}, ['parent' +
+        ' node']);
+        const node = tree.findNode(['parent node', 'some new node']);
+        expect(node.data).toEqual({data: 'interesting 1'});
+      });
+    });
+  });
+
+  describe('#translateTreeNodeIdFromACITree', () => {
+    let aciTreeApi;
+    beforeEach(() => {
+      aciTreeApi = jasmine.createSpyObj('ACITreeApi', [
+        'hasParent',
+        'parent',
+        'getId',
+      ]);
+
+      aciTreeApi.getId.and.callFake((node) => {
+        return node[0].id;
+      });
+      tree.aciTreeApi = aciTreeApi;
+    });
+
+    describe('When tree as a single level', () => {
+      beforeEach(() => {
+        aciTreeApi.hasParent.and.returnValue(false);
+      });
+
+      it('returns an array with the ID of the first level', () => {
+        let node = [{
+          id: 'some id',
+        }];
+        tree.addNewNode('some id', {}, []);
+
+        expect(tree.translateTreeNodeIdFromACITree(node)).toEqual(['some id']);
+      });
+    });
+
+    describe('When tree as a 2 levels', () => {
+      describe('When we try to retrieve the node in the second level', () => {
+        it('returns an array with the ID of the first level and second level', () => {
+          aciTreeApi.hasParent.and.returnValues(true, false);
+          aciTreeApi.parent.and.returnValue([{
+            id: 'parent id'
+          }]);
+          let node = [{
+            id: 'some id',
+          }];
+
+          tree.addNewNode('parent id', {}, []);
+          tree.addNewNode('some id', {}, ['parent id']);
+
+          expect(tree.translateTreeNodeIdFromACITree(node))
+            .toEqual(['parent id', 'some id']);
+        });
+      });
+    });
+  });
+
+  describe('#selected', () => {
+    context('a node in the tree is selected', () => {
+      it('returns that node object', () => {
+        let selectedNode = new TreeNode('bamm', {}, []);
+        setDefaultCallBack(tree, selectedNode)
+        expect(tree.selected()).toEqual(selectedNode);
+      });
+    });
+  });
+
+  describe('#findNodeByTreeElement', () => {
+    context('retrieve data from node not found', () => {
+      it('return undefined', () => {
+        let aciTreeApi = jasmine.createSpyObj('ACITreeApi', [
+          'hasParent',
+          'parent',
+          'getId',
+        ]);
+
+        aciTreeApi.getId.and.callFake((node) => {
+          return node[0].id;
+        });
+        tree.aciTreeApi = aciTreeApi;
+        expect(tree.findNodeByDomElement(['<li>something</li>'])).toBeUndefined();
+      });
+    });
+  });
+};
+
+describe('tree tests', () => {
+  describe('TreeNode', () => {
+    describe('#hasParent', () => {
+      context('parent is null', () => {
+        it('returns false', () => {
+          let treeNode = new TreeNode('123', {}, null);
+          expect(treeNode.hasParent()).toBe(false);
+        });
+      });
+      context('parent is undefined', () => {
+        it('returns false', () => {
+          let treeNode = new TreeNode('123', {}, undefined);
+          expect(treeNode.hasParent()).toBe(false);
+        });
+      });
+      context('parent exists', () => {
+        it('returns true', () => {
+          let parentNode = new TreeNode('456', {}, undefined);
+          let treeNode = new TreeNode('123', {}, parentNode);
+          expect(treeNode.hasParent()).toBe(true);
+        });
+      });
+    });
+  });
+
+  describe('Tree', () => {
+    function realTreeSelectNode(tree, selectedNode) {
+      let aciTreeApi = jasmine.createSpyObj('ACITreeApi', [
+        'selected',
+      ]);
+      tree.aciTreeApi = aciTreeApi;
+      aciTreeApi.selected.and.returnValue(selectedNode);
+    }
+
+    treeTests(Tree, realTreeSelectNode);
+  });
+
+  describe('TreeFake', () => {
+    function fakeTreeSelectNode(tree, selectedNode) {
+      tree.setSelectedNode(selectedNode);
+    }
+
+    treeTests(TreeFake, fakeTreeSelectNode);
+
+    describe('#hasParent', () => {
+      context('tree contains multiple levels', () => {
+        let tree;
+        beforeEach(() => {
+          tree = new TreeFake();
+          tree.addNewNode('level1', {data: 'interesting'}, []);
+          tree.addNewNode('level2', {data: 'interesting'}, ['level1']);
+        });
+
+        context('node is at the first level', () => {
+          it('returns false', () => {
+            expect(tree.hasParent([{id: 'level1'}])).toBe(false);
+          });
+        });
+
+        context('node is at the second level', () => {
+          it('returns true', () => {
+            expect(tree.hasParent([{id: 'level2'}])).toBe(true);
+          });
+        });
+      });
+    });
+
+    describe('#parent', () => {
+      let tree;
+      beforeEach(() => {
+        tree = new TreeFake();
+        tree.addNewNode('level1', {data: 'interesting'}, []);
+        tree.addNewNode('level2', {data: 'interesting'}, ['level1']);
+      });
+
+      context('node is the root', () => {
+        it('returns null', () => {
+          expect(tree.parent([{id: 'level1'}])).toBeNull();
+        });
+      });
+
+      context('node is not root', () => {
+        it('returns root element', () => {
+          expect(tree.parent([{id: 'level2'}])).toEqual([{id: 'level1'}]);
+        });
+      });
+    });
+
+    describe('#itemData', () => {
+      let tree;
+      beforeEach(() => {
+        tree = new TreeFake();
+        tree.addNewNode('level1', {data: 'interesting'}, []);
+        tree.addNewNode('level2', {data: 'expected data'}, ['level1']);
+      });
+
+      context('retrieve data from the node', () => {
+        it('return the node data', () => {
+          expect(tree.itemData([{id: 'level2'}])).toEqual({
+            data: 'expected' +
+            ' data'
+          })
+        });
+      });
+
+      context('retrieve data from node not found', () => {
+        it('return undefined', () => {
+          expect(tree.itemData([{id: 'bamm'}])).toBeUndefined();
+        });
+      });
+    });
+  });
+});
+
--
2.15.1

