Hi,
Please find the attached patch for RM #1253 - Store and reload current
location in treeview
Feature Details:
- The current tree state will be stored in the sqlite database.
- The time interval to store the tree state is configurable via preferences
and the default is 30 secs.
-1 can be used to stop the tree saving functionality,
- On window unload the current tree state will be saved.
- On Reload, while opening the server, the stored path will be populated.
- On closing the node the saved will be updated accordingly.
- Jasmine test cases are included.
Thanks,
Khushboo
diff --git a/web/pgadmin/browser/register_browser_preferences.py b/web/pgadmin/browser/register_browser_preferences.py
index 2eed500..df30346 100644
--- a/web/pgadmin/browser/register_browser_preferences.py
+++ b/web/pgadmin/browser/register_browser_preferences.py
@@ -28,6 +28,16 @@ def register_browser_preferences(self):
True, category_label=gettext('Display')
)
+ self.preference.register(
+ 'display', 'browser_tree_state_save_interval',
+ gettext("Browser tree state saving interval"), 'integer',
+ 30, category_label=gettext('Display'),
+ help_str=gettext(
+ 'Browser tree state saving interval in seconds.'
+ 'Use -1 to disable the tree saving mechanism.'
+ )
+ )
+
self.table_row_count_threshold = self.preference.register(
'properties', 'table_row_count_threshold',
gettext("Count rows if estimated less than"), 'integer', 2000,
diff --git a/web/pgadmin/browser/static/js/browser.js b/web/pgadmin/browser/static/js/browser.js
index 7951f4a..ad6a39e 100644
--- a/web/pgadmin/browser/static/js/browser.js
+++ b/web/pgadmin/browser/static/js/browser.js
@@ -9,7 +9,7 @@ define('pgadmin.browser', [
'pgadmin.browser.error', 'pgadmin.browser.frame',
'pgadmin.browser.node', 'pgadmin.browser.collection',
'sources/codemirror/addon/fold/pgadmin-sqlfoldcode',
- 'pgadmin.browser.keyboard',
+ 'pgadmin.browser.keyboard', 'sources/tree/pgadmin_tree_save_state',
], function(
tree,
gettext, url_for, require, $, _, S, Bootstrap, pgAdmin, Alertify,
@@ -507,9 +507,11 @@ define('pgadmin.browser', [
.done(function() {})
.fail(function() {});
}, 300000);
+
obj.Events.on('pgadmin:browser:tree:add', obj.onAddTreeNode, obj);
obj.Events.on('pgadmin:browser:tree:update', obj.onUpdateTreeNode, obj);
obj.Events.on('pgadmin:browser:tree:refresh', obj.onRefreshTreeNode, obj);
+
},
add_menu_category: function(
@@ -1936,19 +1938,10 @@ define('pgadmin.browser', [
pgAdmin.Browser.editor_shortcut_keys.Tab = 'insertSoftTab';
}
- window.onbeforeunload = function() {
- var e = window.event,
- msg = S(gettext('Are you sure you wish to close the %s browser?')).sprintf(pgBrowser.utils.app_name).value();
-
- // For IE and Firefox prior to version 4
- if (e) {
- e.returnValue = msg;
- }
-
- // For Safari
- return msg;
- };
-
+ $(window).on('beforeunload', function() {
+ if (pgBrowser.get_preference('browser', 'browser_tree_state_save_interval').value !== -1)
+ pgAdmin.Browser.browserTreeState.save_state();
+ });
return pgAdmin.Browser;
});
diff --git a/web/pgadmin/browser/static/js/node.js b/web/pgadmin/browser/static/js/node.js
index 24bf306..8c9e430 100644
--- a/web/pgadmin/browser/static/js/node.js
+++ b/web/pgadmin/browser/static/js/node.js
@@ -1,11 +1,11 @@
define('pgadmin.browser.node', [
- 'sources/tree/pgadmin_tree_node',
+ 'sources/tree/pgadmin_tree_node', 'sources/url_for',
'sources/gettext', 'jquery', 'underscore', 'underscore.string', 'sources/pgadmin',
'pgadmin.browser.menu', 'backbone', 'pgadmin.alertifyjs', 'pgadmin.browser.datamodel',
'backform', 'sources/browser/generate_url', 'sources/utils', 'pgadmin.browser.utils',
'pgadmin.backform',
], function(
- pgadminTreeNode,
+ pgadminTreeNode, url_for,
gettext, $, _, S, pgAdmin, Menu, Backbone, Alertify, pgBrowser, Backform, generateUrl, commonUtils
) {
@@ -849,7 +849,6 @@ define('pgadmin.browser.node', [
}
}
},
-
added: function(item, data, browser) {
var b = browser || pgBrowser,
t = b.tree,
@@ -876,6 +875,8 @@ define('pgadmin.browser.node', [
);
}
+ pgBrowser.Events.trigger('pgadmin:browser:tree:expand-from-previous-tree-state',
+ item);
pgBrowser.Node.callbacks.change_server_background(item, data);
},
// Callback called - when a node is selected in browser tree.
@@ -958,6 +959,14 @@ define('pgadmin.browser.node', [
},
});
},
+ opened: function(item) {
+ pgBrowser.Events.trigger('pgadmin:browser:tree:update-tree-state',
+ item);
+ },
+ closed: function(item) {
+ pgBrowser.Events.trigger('pgadmin:browser:tree:remove-from-tree-state',
+ item);
+ },
},
/**********************************************************************
* A hook (not a callback) to show object properties in given HTML
diff --git a/web/pgadmin/browser/static/js/preferences.js b/web/pgadmin/browser/static/js/preferences.js
index 5ca9e3c..1767d72 100644
--- a/web/pgadmin/browser/static/js/preferences.js
+++ b/web/pgadmin/browser/static/js/preferences.js
@@ -94,6 +94,9 @@ _.extend(pgBrowser, {
modifyAnimation.modifyAlertifyAnimation(self);
}
+ // Initialize Tree saving/reloading
+ pgBrowser.browserTreeState.init();
+
/* Once the cache is loaded after changing the preferences,
* notify the modules of the change
*/
diff --git a/web/pgadmin/preferences/static/js/preferences.js b/web/pgadmin/preferences/static/js/preferences.js
index eeaf4bb..840fc0a 100644
--- a/web/pgadmin/preferences/static/js/preferences.js
+++ b/web/pgadmin/preferences/static/js/preferences.js
@@ -2,6 +2,7 @@ define('pgadmin.preferences', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
'pgadmin.alertifyjs', 'sources/pgadmin', 'pgadmin.backform',
'pgadmin.browser', 'sources/modify_animation',
+ 'sources/tree/pgadmin_tree_save_state',
], function(
gettext, url_for, $, _, Backbone, Alertify, pgAdmin, Backform, pgBrowser,
modifyAnimation
diff --git a/web/pgadmin/settings/__init__.py b/web/pgadmin/settings/__init__.py
index 269bfdf..d0b4fad 100644
--- a/web/pgadmin/settings/__init__.py
+++ b/web/pgadmin/settings/__init__.py
@@ -10,13 +10,15 @@
"""Utility functions for storing and retrieving user configuration settings."""
import traceback
+import json
-from flask import Response, request, render_template, url_for
+from flask import Response, request, render_template, url_for, current_app
from flask_babelex import gettext
from flask_login import current_user
from flask_security import login_required
from pgadmin.utils import PgAdminModule
-from pgadmin.utils.ajax import make_json_response, bad_request
+from pgadmin.utils.ajax import make_json_response, bad_request,\
+ success_return, internal_server_error
from pgadmin.utils.menu import MenuItem
from pgadmin.model import db, Setting
@@ -52,7 +54,8 @@ class SettingsModule(PgAdminModule):
list: a list of url endpoints exposed to the client.
"""
return [
- 'settings.store', 'settings.store_bulk', 'settings.reset_layout'
+ 'settings.store', 'settings.store_bulk', 'settings.reset_layout',
+ 'settings.save_tree_state', 'settings.get_tree_state'
]
@@ -145,3 +148,36 @@ def reset_layout():
)
return make_json_response(result=request.form)
+
+
[email protected]("/save_tree_state/", endpoint="save_tree_state",
+ methods=['POST'])
+@login_required
+def save_browser_tree_state():
+ """Save the browser tree state."""
+ data = request.form if request.form else request.data.decode('utf-8')
+
+ try:
+ store_setting('browser_tree_state', data)
+ except Exception as e:
+ current_app.logger.exception(e)
+ return internal_server_error(errormsg=str(e))
+
+ return success_return()
+
+
[email protected]("/get_tree_state/", endpoint="get_tree_state",
+ methods=['GET'])
+@login_required
+def get_browser_tree_state():
+ """Get the browser tree state."""
+
+ try:
+ data = get_setting('browser_tree_state')
+ except Exception as e:
+ current_app.logger.exception(e)
+ return internal_server_error(errormsg=str(e))
+
+ return Response(response=data,
+ status=200,
+ mimetype="application/json")
diff --git a/web/pgadmin/static/js/tree/pgadmin_tree_save_state.js b/web/pgadmin/static/js/tree/pgadmin_tree_save_state.js
new file mode 100644
index 0000000..d9958fd
--- /dev/null
+++ b/web/pgadmin/static/js/tree/pgadmin_tree_save_state.js
@@ -0,0 +1,235 @@
+import _ from 'underscore';
+import $ from 'jquery';
+import url_for from 'sources/url_for';
+import gettext from 'sources/gettext';
+import pgAdmin from '../../../static/js/pgadmin';
+
+const pgBrowser = pgAdmin.Browser = pgAdmin.Browser || {};
+
+
+const browserTreeState = pgBrowser.browserTreeState = pgBrowser.browserTreeState || {};
+
+_.extend(pgBrowser.browserTreeState, {
+
+ // Parent node to start saving / reloading the tree state
+ parent: 'server',
+
+ // The original parent of the browser tree
+ orig_parent: 'server_group',
+
+ // Last load tree state
+
+ stored_state: {},
+
+ // Previous tree state
+ last_state: {},
+
+ // Current tree state
+ current_state: {},
+
+ init: function() {
+
+ const save_tree_state_period = pgBrowser.get_preference('browser', 'browser_tree_state_save_interval');
+
+ if (!_.isUndefined(save_tree_state_period) && save_tree_state_period.value !== -1) {
+ // Save the tree state every 30 seconds
+ setInterval(this.save_state, (save_tree_state_period.value) * 1000);
+
+ // Fetch the tree last state while loading the browser tree
+ this.fetch_state.apply(this);
+
+ pgBrowser.Events.on('pgadmin:browser:tree:expand-from-previous-tree-state',
+ this.expand_from_previous_state, this);
+ pgBrowser.Events.on('pgadmin:browser:tree:remove-from-tree-state',
+ this.remove_from_cache, this);
+ pgBrowser.Events.on('pgadmin:browser:tree:update-tree-state',
+ this.update_cache, this);
+ }
+ },
+
+ save_state: function() {
+
+ var self = pgBrowser.browserTreeState;
+ if(self.last_state == JSON.stringify(self.current_state))
+ return;
+
+ $.ajax({
+ url: url_for('settings.save_tree_state'),
+ type: 'POST',
+ contentType: 'application/json',
+ data: JSON.stringify(self.current_state),
+ })
+ .done(function() {
+ self.last_state = JSON.stringify(self.current_state);
+ })
+ .fail(function(jqx) {
+ var msg = jqx.responseText;
+ /* Error from the server */
+ if (jqx.status == 417 || jqx.status == 410 || jqx.status == 500) {
+ try {
+ var data = JSON.parse(jqx.responseText);
+ msg = data.errormsg;
+ } catch (e) {
+ console.warn(e.stack || e);
+ }
+ }
+ console.warn(
+ gettext('Error saving the tree state."'), msg);
+ });
+
+ },
+ fetch_state: function() {
+
+ var self = this;
+ $.ajax({
+ url: url_for('settings.get_tree_state'),
+ type: 'GET',
+ dataType: 'json',
+ contentType: 'application/json',
+ })
+ .done(function(res) {
+ self.stored_state = res;
+ })
+ .fail(function(jqx) {
+ var msg = jqx.responseText;
+ /* Error from the server */
+ if (jqx.status == 417 || jqx.status == 410 || jqx.status == 500) {
+ try {
+ var data = JSON.parse(jqx.responseText);
+ msg = data.errormsg;
+ } catch (e) {
+ console.warn(e.stack || e);
+ }
+ }
+ console.warn(
+ gettext('Error fetching the tree state."'), msg);
+ });
+ },
+ update_cache: function(item) {
+ let data = item && pgBrowser.tree.itemData(item),
+ node = data && pgBrowser.Nodes[data._type],
+ treeHierarchy = node.getTreeNodeHierarchy(item),
+ topParent = undefined,
+ pathIDs = pgBrowser.tree.pathId(item),
+ oldPath = pathIDs.join(),
+ path = [],
+ tmpIndex = -1;
+
+ // If no parent so or the server not in tree hierarchy then return
+ if (!pgBrowser.tree.hasParent(item) || !(this.parent in treeHierarchy))
+ return;
+
+ topParent = treeHierarchy[this.parent]['_id'];
+
+ if (pgBrowser.tree.isOpen(item)) {
+ pathIDs.push(data.id);
+ path = pathIDs.join();
+
+ // IF the current path is already saved then return
+ let index = _.find(this.current_state[topParent], function(tData) {
+ return (tData.search(path) !== -1);
+ });
+ if(!_.isUndefined(index))
+ return;
+
+ // Add / Update the current item into the tree path
+ if (!_.isUndefined(this.current_state[topParent])) {
+ tmpIndex = this.current_state[topParent].indexOf(oldPath);
+ } else {
+ this.current_state[topParent] = [];
+ }
+ if (tmpIndex !== -1) {
+ this.current_state[topParent][tmpIndex] = path;
+ }
+ else {
+ this.current_state[topParent].push(path);
+ }
+ }
+ },
+ remove_from_cache: function(item) {
+ let self= this,
+ data = item && pgBrowser.tree.itemData(item),
+ node = data && pgBrowser.Nodes[data._type],
+ treeHierarchy = node && node.getTreeNodeHierarchy(item);
+
+ if (!pgBrowser.tree.hasParent(item) || !(self.parent in treeHierarchy))
+ return;
+
+ let topParent = treeHierarchy && treeHierarchy[self.parent]['_id'],
+ origParent = treeHierarchy && treeHierarchy[self.orig_parent]['id'];
+
+ if (pgBrowser.tree.isClosed(item)) {
+ let tmpTreeData = self.current_state[topParent];
+ if (!_.isUndefined(tmpTreeData) && !_.isUndefined(tmpTreeData.length)) {
+ let tcnt = 0,
+ tmpItemDataStr = undefined;
+ _.each(tmpTreeData, function(tData) {
+ if (_.isUndefined(tData))
+ return;
+
+ let tmpItemData = tData.split(',');
+
+ if (tmpItemData.indexOf(data.id) !== -1 ) {
+ let index = tmpItemData.indexOf(data.id);
+ tmpItemData.splice(index);
+ tmpItemDataStr = tmpItemData.join();
+
+ if (tmpItemDataStr == origParent)
+ self.current_state[topParent].splice(tData, 1);
+ else
+ self.current_state[topParent][tcnt] = tmpItemDataStr;
+ }
+ tcnt ++;
+ });
+
+ if (tmpItemDataStr !== undefined) {
+ let tmpIndex = _.find(tmpTreeData, function(tData) {
+ return (tData.search(tmpItemDataStr) !== -1);
+ });
+
+ if(tmpIndex && tmpIndex !== tmpTreeData.indexOf(tmpItemDataStr))
+ this.current_state[topParent].splice(tmpIndex, 1);
+ }
+ }
+ }
+ },
+ expand_from_previous_state: function(item) {
+ let self = this,
+ treeData = self.stored_state || {},
+ data = item && pgBrowser.tree.itemData(item),
+ node = data && pgBrowser.Nodes[data._type],
+ treeHierarchy = node && node.getTreeNodeHierarchy(item);
+
+ if (!pgBrowser.tree.hasParent(item) || !(self.parent in treeHierarchy))
+ return;
+
+ // If the server node is open then only we should populate the tree
+ if (data['_type'] == self.parent && (pgBrowser.tree.isOpen(item) === false))
+ return;
+
+ let tmpTreeData = treeData[treeHierarchy[self.parent]['_id']];
+
+ if (!_.isUndefined(tmpTreeData) && !_.isUndefined(tmpTreeData.length)) {
+ _.each(tmpTreeData, function(tData) {
+ if (_.isUndefined(tData))
+ return;
+
+ let tmpItemData = tData.split(',');
+
+ // If the item is in the lastTreeState then open it
+ if (tmpItemData.indexOf(data.id) !== -1 ) {
+ let index = tmpItemData.indexOf(data.id);
+ pgBrowser.tree.toggle(item);
+
+ if (index == (tmpItemData.length - 1 )) {
+ let tIndex = treeData[treeHierarchy[self.parent]['_id']].indexOf(tData);
+ treeData[treeHierarchy[self.parent]['_id']].splice(tIndex, 1);
+ }
+ }
+ });
+ }
+ },
+
+});
+
+export {pgBrowser, browserTreeState};
diff --git a/web/regression/javascript/tree/pgadmin_tree_state_save_spec.js b/web/regression/javascript/tree/pgadmin_tree_state_save_spec.js
new file mode 100644
index 0000000..769b663
--- /dev/null
+++ b/web/regression/javascript/tree/pgadmin_tree_state_save_spec.js
@@ -0,0 +1,240 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {pgBrowser, browserTreeState} from '../../../pgadmin/static/js/tree/pgadmin_tree_save_state';
+
+
+describe('#browserTreeState', () => {
+ beforeEach(() => {
+ pgBrowser.Nodes = {
+ server_group: {
+ _type: 'server_group',
+ hasId: true,
+ id: 'server_group/1',
+ _id: 1,
+ getTreeNodeHierarchy: function() { return {
+ server_group: {
+ _type: 'server_group',
+ hasId: true,
+ id: 'server_group/1',
+ _id: 1,
+ },
+ };},
+ },
+ server: {
+ _type: 'server',
+ hasId: true,
+ id: 'server/1',
+ _id: 1,
+ getTreeNodeHierarchy: function() { return {
+ server_group: {
+ _type: 'server_group',
+ hasId: true,
+ id: 'server_group/1',
+ _id: 1,
+ },
+ server: {
+ _type: 'server',
+ hasId: true,
+ id: 'server/1',
+ _id: 1,
+ },
+ };},
+ },
+ coll_database: {
+ _type: 'coll_database',
+ hasId: false,
+ id: 'coll_database/1',
+ _id: 1,
+ getTreeNodeHierarchy: function() { return {
+ server_group: {
+ _type: 'server_group',
+ hasId: true,
+ id: 'server_group/1',
+ _id: 1,
+ },
+ server: {
+ _type: 'server',
+ hasId: true,
+ id: 'server/1',
+ _id: 1,
+ },
+ coll_database: {
+ _type: 'coll_database',
+ hasId: true,
+ id: 'coll_database/1',
+ _id: 1,
+ },
+ };},
+ },
+ database: {
+ _type: 'database',
+ hasId: true,
+ id: 'database/10',
+ _id: 10,
+ getTreeNodeHierarchy: function() { return {
+ server_group: {
+ _type: 'server_group',
+ hasId: true,
+ id: 'server_group/1',
+ _id: 1,
+ },
+ server: {
+ _type: 'server',
+ hasId: true,
+ id: 'server/1',
+ _id: 1,
+ },
+ coll_database: {
+ _type: 'coll_database',
+ hasId: true,
+ id: 'coll_database/1',
+ _id: 1,
+ },
+ database: {
+ _type: 'database',
+ hasId: true,
+ id: 'database/10',
+ _id: 10,
+ },
+ };},
+ },
+ };
+ pgBrowser.tree = jasmine.createSpyObj('tree', ['itemData', 'pathId', 'hasParent', 'isOpen', 'isClosed']);
+ });
+
+ describe('When node is opened tree state is getting updated', () => {
+
+ describe('When tree node server group is opened', () => {
+ let item = {
+ _type: 'server_group',
+ hasId: true,
+ id: 'server_group/1',
+ _id: 1,
+ };
+ beforeEach(() => {
+ pgBrowser.tree.itemData.and.returnValue(item);
+ pgBrowser.tree.pathId.and.returnValue([]);
+ pgBrowser.tree.hasParent.and.returnValue(false);
+ pgBrowser.tree.isOpen.and.returnValue(true);
+ });
+
+ it('The tree current state will be empty', () => {
+ browserTreeState.update_cache(item);
+ expect(browserTreeState.current_state, {});
+ });
+ });
+
+ describe('When server node is opened', () => {
+ let item = {
+ _type: 'server',
+ hasId: true,
+ id: 'server/1',
+ _id: 1,
+ };
+ beforeEach(() => {
+ pgBrowser.tree.itemData.and.returnValue(item);
+ pgBrowser.tree.pathId.and.returnValue(['server_group/1']);
+ pgBrowser.tree.hasParent.and.returnValue(true);
+ pgBrowser.tree.isOpen.and.returnValue(true);
+ });
+
+ it('The tree current state will have server', () => {
+ browserTreeState.update_cache(item);
+ expect(browserTreeState.current_state, {1: ['server_group/1,server/1']});
+ });
+ });
+
+ describe('When coll_database node is opened', () => {
+ let item = {
+ _type: 'coll_database',
+ hasId: true,
+ id: 'coll_database/1',
+ _id: 1,
+ };
+ beforeEach(() => {
+ pgBrowser.tree.itemData.and.returnValue(item);
+ pgBrowser.tree.pathId.and.returnValue(['server_group/1', 'server/1']);
+ pgBrowser.tree.hasParent.and.returnValue(true);
+ pgBrowser.tree.isOpen.and.returnValue(true);
+ });
+
+ it('The tree current state will have coll_database', () => {
+ browserTreeState.update_cache(item);
+ expect(browserTreeState.current_state, {1: ['server_group/1,server/1,coll_database/1']});
+ });
+ });
+
+ describe('When database node is opened', () => {
+ let item = {
+ _type: 'database',
+ hasId: true,
+ id: 'database/10',
+ _id: 10,
+ };
+ beforeEach(() => {
+ pgBrowser.tree.itemData.and.returnValue(item);
+ pgBrowser.tree.pathId.and.returnValue(['server_group/1', 'server/1', 'coll_database/1']);
+ pgBrowser.tree.hasParent.and.returnValue(true);
+ pgBrowser.tree.isOpen.and.returnValue(true);
+ });
+
+ it('The tree current state will have coll_database', () => {
+ browserTreeState.update_cache(item);
+ expect(browserTreeState.current_state, {1: ['server_group/1,server/1,coll_database/1','database/10']});
+ });
+ });
+ });
+
+
+ describe('When node is closed, node should get removed from the tree state cache', () => {
+ describe('When coll_database node is closed, both database and coll_database should be removed', () => {
+ let item = {
+ _type: 'coll_database',
+ hasId: true,
+ id: 'coll_database/1',
+ _id: 1,
+ };
+ beforeEach(() => {
+ pgBrowser.tree.itemData.and.returnValue(item);
+ pgBrowser.tree.pathId.and.returnValue(['server_group/1', 'server/1']);
+ pgBrowser.tree.hasParent.and.returnValue(true);
+ pgBrowser.tree.isOpen.and.returnValue(true);
+ pgBrowser.tree.isClosed.and.returnValue(true);
+ });
+
+ it('The tree current state will remove coll_database and database', () => {
+ browserTreeState.remove_from_cache(item);
+ expect(browserTreeState.current_state, {1: ['server_group/1,server/1']});
+ });
+ });
+
+ describe('When server node is closed, both server and server_group should be removed', () => {
+ let item = {
+ _type: 'server',
+ hasId: true,
+ id: 'server/1',
+ _id: 1,
+ };
+ beforeEach(() => {
+ pgBrowser.tree.itemData.and.returnValue(item);
+ pgBrowser.tree.pathId.and.returnValue(['server_group/1']);
+ pgBrowser.tree.hasParent.and.returnValue(true);
+ pgBrowser.tree.isOpen.and.returnValue(true);
+ pgBrowser.tree.isClosed.and.returnValue(true);
+ });
+
+ it('The tree current state will remove server_group and server', () => {
+ browserTreeState.remove_from_cache(item);
+ expect(browserTreeState.current_state, {1: []});
+ });
+ });
+ });
+
+});