diff --git a/web/pgadmin/browser/static/js/browser.js b/web/pgadmin/browser/static/js/browser.js
index 14640ae8..d9693486 100644
--- a/web/pgadmin/browser/static/js/browser.js
+++ b/web/pgadmin/browser/static/js/browser.js
@@ -1,4 +1,5 @@
 define('pgadmin.browser', [
+  'sources/tree/tree',
   'sources/gettext', 'sources/url_for', 'require', 'jquery', 'underscore', 'underscore.string',
   'bootstrap', 'sources/pgadmin', 'pgadmin.alertifyjs', 'bundled_codemirror',
   'sources/check_node_visibility', 'sources/modify_animation', 'pgadmin.browser.utils', 'wcdocker',
@@ -10,6 +11,7 @@ define('pgadmin.browser', [
   'sources/codemirror/addon/fold/pgadmin-sqlfoldcode',
   'pgadmin.browser.keyboard',
 ], function(
+  tree,
   gettext, url_for, require, $, _, S, Bootstrap, pgAdmin, Alertify,
   codemirror, checkNodeVisibility, modifyAnimation
 ) {
@@ -86,6 +88,7 @@ define('pgadmin.browser', [
       });
 
       b.tree = $('#tree').aciTree('api');
+      b.treeMenu.register($('#tree'));
     };
 
   // Extend the browser class attributes
@@ -100,6 +103,7 @@ define('pgadmin.browser', [
     editor:null,
     // Left hand browser tree
     tree:null,
+    treeMenu: new tree.Tree(),
     // list of script to be loaded, when a certain type of node is loaded
     // It will be used to register extensions, tools, child node scripts,
     // etc.
diff --git a/web/pgadmin/static/js/tree/tree.js b/web/pgadmin/static/js/tree/tree.js
new file mode 100644
index 00000000..974ee8de
--- /dev/null
+++ b/web/pgadmin/static/js/tree/tree.js
@@ -0,0 +1,214 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// 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, domNode, parent) {
+    this.id = id;
+    this.data = data;
+    this.setParent(parent);
+    this.children = [];
+    this.domNode = domNode;
+  }
+
+  hasParent() {
+    return this.parentNode !== null && this.parentNode !== undefined;
+  }
+
+  parent() {
+    return this.parentNode;
+  }
+
+  setParent(parent) {
+    this.parentNode = parent;
+    this.path = this.id;
+    if (parent !== null && parent !== undefined && parent.path !== undefined) {
+      this.path = parent.path + '.' + this.id;
+    }
+  }
+
+  getData() {
+    if (this.data === undefined) {
+      return undefined;
+    } else if (this.data === null) {
+      return null;
+    }
+    return Object.assign({}, this.data);
+  }
+
+  getHtmlIdentifier() {
+    return this.domNode;
+  }
+
+  reload(tree) {
+    this.unload(tree);
+    tree.aciTreeApi.setInode(this.domNode);
+    tree.aciTreeApi.deselect(this.domNode);
+    setTimeout(() => {
+      tree.selectNode(this.domNode);
+    }, 0);
+  }
+
+  unload(tree) {
+    this.children = [];
+    tree.aciTreeApi.unload(this.domNode);
+  }
+
+  anyParent(condition) {
+    let node = this;
+
+    while (node.hasParent()) {
+      node = node.parent();
+      if (condition(node)) {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  /**
+   * Given a condition returns true if the current node
+   * or any of the parent nodes condition result is true
+   */
+  anyFamilyMember(condition) {
+    if(condition(this)) {
+      return true;
+    }
+
+    return this.anyParent(condition);
+  }
+}
+
+export class Tree {
+  constructor() {
+    this.rootNode = new TreeNode(undefined, {});
+    this.aciTreeApi = undefined;
+  }
+
+  addNewNode(id, data, domNode, parentPath) {
+    const parent = this.findNode(parentPath);
+    return this.createOrUpdateNode(id, data, parent, domNode);
+  }
+
+  findNode(path) {
+    if (path === null || path === undefined || 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();
+  }
+
+  selectNode(aciTreeIdentifier) {
+    this.aciTreeApi.select(aciTreeIdentifier);
+  }
+
+  createOrUpdateNode(id, data, parent, domNode) {
+    let oldNodePath = [id];
+    if(parent !== null && parent !== undefined) {
+      oldNodePath = [parent.path, id];
+    }
+    const oldNode = this.findNode(oldNodePath);
+    if (oldNode !== null) {
+      oldNode.data = Object.assign({}, data);
+      return oldNode;
+    }
+
+    const node = new TreeNode(id, data, domNode, parent);
+    if (parent === this.rootNode) {
+      node.parentNode = null;
+    }
+    parent.children.push(node);
+    return node;
+  }
+
+  /**
+   * Given the JQuery object that contains the ACI Tree
+   * this method is responsible for registering this tree class
+   * to listen to all the events that happen in the ACI Tree.
+   *
+   * At this point in time the only event that we care about is
+   * the addition of a new node.
+   * The function will create a new tree node to store the information
+   * that exist in the ACI for it.
+   */
+  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, item, parentId);
+        }
+      }
+    }.bind(this));
+    this.aciTreeApi = $treeJQuery.aciTree('api');
+  }
+
+  /**
+   * As the name hints this functions works as a layer in between ACI and
+   * the adaptor. Given a ACITree JQuery node find the location of it in the
+   * Tree and then returns and array with the path to to the Tree Node in
+   * question
+   *
+   * This is not optimized and will always go through the full tree
+   */
+  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();
+
+/**
+ * Given an initial node and a path, it will navigate through
+ * the new tree to find the node that matches the path
+ */
+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..b285a45f
--- /dev/null
+++ b/web/regression/javascript/tree/tree_fake.js
@@ -0,0 +1,69 @@
+/////////////////////////////////////////////////////////////
+//
+// 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 = {};
+    this.aciTreeApi = jasmine.createSpyObj(
+      ['ACITreeApi'], ['setInode', 'unload', 'deselect', 'select']);
+  }
+
+  addNewNode(id, data, domNode, path) {
+    this.aciTreeToOurTreeTranslator[id] = [id];
+    if (path !== null && path !== undefined) {
+      this.aciTreeToOurTreeTranslator[id] = path.concat(id);
+    }
+    return super.addNewNode(id, data, domNode, path);
+  }
+
+  addChild(parent, child) {
+    child.setParent(parent);
+    this.aciTreeToOurTreeTranslator[child.id] = this.aciTreeToOurTreeTranslator[parent.id].concat(child.id);
+    parent.children.push(child);
+  }
+
+  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) {
+    if(aciTreeNode === undefined || aciTreeNode[0] === undefined) {
+      return null;
+    }
+    return this.aciTreeToOurTreeTranslator[aciTreeNode[0].id];
+  }
+
+  itemData(aciTreeNode) {
+    let node = this.findNodeByDomElement(aciTreeNode);
+    if (node === undefined || node === null) {
+      return undefined;
+    }
+    return node.getData();
+  }
+
+  selected() {
+    return this.selectedNode;
+  }
+
+  selectNode(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..164ceb5b
--- /dev/null
+++ b/web/regression/javascript/tree/tree_spec.js
@@ -0,0 +1,421 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// 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', () => {
+      context('using [] as the parent', () => {
+        beforeEach(() => {
+          tree.addNewNode('some new node', {data: 'interesting'}, undefined, []);
+        });
+
+        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();
+        });
+      });
+
+      context('using null as the parent', () => {
+        beforeEach(() => {
+          tree.addNewNode('some new node', {data: 'interesting'}, undefined, null);
+        });
+
+        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();
+        });
+      });
+
+      context('using undefined as the parent', () => {
+        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'}, undefined, []);
+        tree.addNewNode('some new node', {data: 'interesting'}, undefined, ['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', () => {
+      beforeEach(() => {
+        tree.addNewNode('parent node', {data: 'parent data'}, undefined, []);
+        tree.addNewNode('some new node', {data: 'interesting'}, undefined, ['parent' +
+        ' node']);
+      });
+
+      it('does not add a new child', () => {
+        tree.addNewNode('some new node', {data: 'interesting 1'}, undefined, ['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'}, undefined, ['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', {}, undefined, []);
+
+        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', {}, undefined, []);
+          tree.addNewNode('some id', {}, undefined, ['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', {}, []);
+          let treeNode = new TreeNode('123', {}, [], parentNode);
+          expect(treeNode.hasParent()).toBe(true);
+        });
+      });
+    });
+
+    describe('#reload', () => {
+      let tree;
+      let level2;
+      beforeEach(() => {
+        tree = new TreeFake();
+        tree.addNewNode('level1', {data: 'interesting'}, [{id: 'level1'}], []);
+        level2 = tree.addNewNode('level2', {data: 'data'}, [{id: 'level2'}], ['level1']);
+        tree.addNewNode('level3', {data: 'more data'}, [{id: 'level3'}], ['level1', 'level2']);
+
+        tree.aciTreeApi = jasmine.createSpyObj(
+          'ACITreeApi', ['setInode', 'unload', 'deselect', 'select']);
+      });
+
+      it('reloads the node and its children', () => {
+        level2.reload(tree);
+        expect(tree.findNodeByDomElement([{id: 'level2'}])).toEqual(level2);
+      });
+
+      it('does not reload the children of node', () => {
+        level2.reload(tree);
+        expect(tree.findNodeByDomElement([{id: 'level3'}])).toBeNull();
+      });
+
+      it('select the node', (done) => {
+        level2.reload(tree);
+        setTimeout(() => {
+          expect(tree.selected()).toEqual([{id: 'level2'}]);
+          done();
+        }, 20);
+      });
+
+      describe('ACITree specific', () => {
+        it('sets the current node as a Inode, changing the Icon back to +', () => {
+          level2.reload(tree);
+          expect(tree.aciTreeApi.setInode).toHaveBeenCalledWith([{id: 'level2'}]);
+        });
+
+        it('deselect the node and selects it again to trigger ACI tree' +
+          ' events', (done) => {
+          level2.reload(tree);
+          setTimeout(() => {
+            expect(tree.aciTreeApi.deselect).toHaveBeenCalledWith([{id: 'level2'}]);
+            done();
+          }, 20);
+        });
+      });
+    });
+
+    describe('#unload', () => {
+      let tree;
+      let level2;
+      beforeEach(() => {
+        tree = new TreeFake();
+        tree.addNewNode('level1', {data: 'interesting'}, ['<li>level1</li>'], []);
+        level2 = tree.addNewNode('level2', {data: 'data'}, ['<li>level2</li>'], ['level1']);
+        tree.addNewNode('level3', {data: 'more data'}, ['<li>level3</li>'], ['level1', 'level2']);
+        tree.aciTreeApi = jasmine.createSpyObj('ACITreeApi', ['unload']);
+      });
+
+      it('unloads the children of the current node', () => {
+        level2.unload(tree);
+        expect(tree.findNodeByDomElement([{id: 'level2'}])).toEqual(level2);
+        expect(tree.findNodeByDomElement([{id: 'level3'}])).toBeNull();
+      });
+
+      it('calls unload on the ACI Tree', () => {
+        level2.unload(tree);
+        expect(tree.aciTreeApi.unload).toHaveBeenCalledWith(['<li>level2</li>']);
+      });
+    });
+  });
+
+  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.selectNode(selectedNode);
+    }
+
+    treeTests(TreeFake, fakeTreeSelectNode);
+
+    describe('#hasParent', () => {
+      context('tree contains multiple levels', () => {
+        let tree;
+        beforeEach(() => {
+          tree = new TreeFake();
+          tree.addNewNode('level1', {data: 'interesting'}, undefined, []);
+          tree.addNewNode('level2', {data: 'interesting'}, undefined, ['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'}, undefined, []);
+        tree.addNewNode('level2', {data: 'interesting'}, undefined, ['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'}, undefined, []);
+        tree.addNewNode('level2', {data: 'expected data'}, undefined, ['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();
+        });
+      });
+    });
+
+    describe('#addChild', () => {
+      let root, child;
+      beforeEach(() => {
+        let tree = new TreeFake();
+        root = tree.addNewNode('root', {}, [{id: 'root'}]);
+        child = new TreeNode('node.1', {}, [{id: 'node.1'}]);
+        tree.addChild(root, child);
+      });
+
+      it('adds a new child to a node', () => {
+        expect(root.children).toEqual([child]);
+      });
+
+      it('changes the parent of the child node', () => {
+        expect(root.children[0].parentNode).toEqual(root);
+        expect(child.parentNode).toEqual(root);
+      });
+
+      it('changes the path of the child', () => {
+        expect(child.path).toEqual('root.node.1');
+      });
+    });
+  });
+});
+
-- 
2.16.2

