From 622b67684d7d44c5c78191be4dbfc26554135a12 Mon Sep 17 00:00:00 2001 From: Khushboo Vashi Date: Thu, 14 Feb 2019 11:15:01 +0000 Subject: [PATCH] Optimise display of Dependencies and Dependents, and use on-demand loading of rows in batches of 100. Fixes #3959 --- docs/en_US/release_notes_4_3.rst | 1 + web/pgadmin/browser/static/js/browser.js | 4 +- web/pgadmin/browser/static/js/collection.js | 105 ++++- web/pgadmin/misc/dependencies/__init__.py | 30 ++ .../dependencies/static/js/dependencies.js | 246 ++++++++++ .../misc/{depends => dependents}/__init__.py | 11 +- .../misc/dependents/static/js/dependents.js | 252 ++++++++++ web/pgadmin/misc/depends/static/js/depends.js | 432 ------------------ web/webpack.config.js | 3 +- web/webpack.shim.js | 3 +- 10 files changed, 623 insertions(+), 464 deletions(-) create mode 100644 web/pgadmin/misc/dependencies/__init__.py create mode 100644 web/pgadmin/misc/dependencies/static/js/dependencies.js rename web/pgadmin/misc/{depends => dependents}/__init__.py (64%) create mode 100644 web/pgadmin/misc/dependents/static/js/dependents.js delete mode 100644 web/pgadmin/misc/depends/static/js/depends.js diff --git a/docs/en_US/release_notes_4_3.rst b/docs/en_US/release_notes_4_3.rst index 3553696e7..158407fd7 100644 --- a/docs/en_US/release_notes_4_3.rst +++ b/docs/en_US/release_notes_4_3.rst @@ -21,6 +21,7 @@ Bug fixes | `Bug #3906 `_ - Fix alignment of Close and Maximize button of Grant Wizard. | `Bug #3942 `_ - Close connections gracefully when the user logs out of pgAdmin. | `Bug #3946 `_ - Fix alignment of checkbox to drop multiple schedules of pgAgent job. +| `Bug #3959 `_ - Optimise display of Dependencies and Dependents, and use on-demand loading of rows in batches of 100. | `Bug #3963 `_ - Fix alignment of import/export toggle switch. | `Bug #3970 `_ - Prevent an error when closing the Sort/Filter dialogue with an empty filter string. | `Bug #3974 `_ - Fix alignment of Connection type toggle switch of pgagent. diff --git a/web/pgadmin/browser/static/js/browser.js b/web/pgadmin/browser/static/js/browser.js index 08108e2be..e3d758fb7 100644 --- a/web/pgadmin/browser/static/js/browser.js +++ b/web/pgadmin/browser/static/js/browser.js @@ -189,7 +189,7 @@ define('pgadmin.browser', [ width: 500, isCloseable: false, isPrivate: true, - content: '
' + select_object_msg + '
', + content: '
' + select_object_msg + '
', events: panelEvents, }), // Dependents of the object @@ -200,7 +200,7 @@ define('pgadmin.browser', [ width: 500, isCloseable: false, isPrivate: true, - content: '
' + select_object_msg + '
', + content: '
' + select_object_msg + '
', events: panelEvents, }), }, diff --git a/web/pgadmin/browser/static/js/collection.js b/web/pgadmin/browser/static/js/collection.js index ec6e8a5df..998ceb819 100644 --- a/web/pgadmin/browser/static/js/collection.js +++ b/web/pgadmin/browser/static/js/collection.js @@ -42,6 +42,7 @@ define([ if (this.node_initialized) return; this.node_initialized = true; + pgAdmin.Browser.add_menus([{ name: 'refresh', node: this.type, module: this, applies: ['object', 'context'], callback: 'refresh', @@ -77,12 +78,9 @@ define([ content = $('
') .addClass('pg-prop-content col-12 has-pg-prop-btn-group'), node = pgBrowser.Nodes[that.node], + $msgContainer = '', // This will be the URL, used for object manipulation. urlBase = this.generate_url(item, 'properties', data), - collection = new (node.Collection.extend({ - url: urlBase, - model: node.model, - }))(), info = this.getTreeNodeHierarchy.apply(this, [item]), gridSchema = Backform.generateGridColumnsFromModel( info, node.model, 'properties', that.columns @@ -132,6 +130,10 @@ define([ return null; }.bind(panel); + that.collection = new (node.Collection.extend({ + url: urlBase, + model: node.model, + }))(); // Add the new column for the multi-select menus if((_.isFunction(that.canDrop) ? that.canDrop.apply(that, [data, item]) : that.canDrop) || @@ -180,7 +182,7 @@ define([ that.grid = new Backgrid.Grid({ emptyText: 'No data found', columns: gridSchema.columns, - collection: collection, + collection: that.collection, className: 'backgrid table presentation table-bordered table-noouter-border table-hover', }); @@ -199,6 +201,7 @@ define([ }; if (view) { + // Avoid unnecessary reloads if (_.isEqual($(panel).data('node-prop'), urlBase)) { return; @@ -215,9 +218,14 @@ define([ j.empty(); j.data('obj-view', gridView); + $msgContainer = '
' + + gettext('Retrieving data from the server...') + '
'; + + $msgContainer = $($msgContainer).appendTo(j); + that.header = $('
').addClass( 'pg-prop-header' - ).appendTo(j); + ); // Render the buttons var buttons = []; @@ -255,26 +263,69 @@ define([ // Render subNode grid content.append('
'); content.find('.pg-prop-coll-container').append(that.grid.render().$el); - j.append(content); - // Fetch Data - collection.fetch({ - reset: true, - error: function(model, error, xhr) { - pgBrowser.Events.trigger( - 'pgadmin:collection:retrieval:error', 'properties', xhr, error, - error.message, item, that - ); - if (!Alertify.pgHandleItemError( - xhr, error, error.message, {item: item, info: info} - )) { - Alertify.pgNotifier(error, xhr, S( - gettext('Error retrieving properties - %s.') - ).sprintf(error.message || that.label).value(), function() { + var timer; + + $.ajax({ + url: urlBase, + type: 'GET', + beforeSend: function() { + // Generate a timer for the request + timer = setTimeout(function() { + // notify user if request is taking longer than 1 second + + $msgContainer.text(gettext('Retrieving data from the server...')); + $msgContainer.removeClass('d-none'); + if (self.grid) { + self.grid.remove(); + } + }, 1000); + }, + }) + .done(function(res) { + clearTimeout(timer); + + if (_.isUndefined(that.grid) || _.isNull(that.grid)) return; + + that.data = res; + + if (that.data.length > 0) { + + if (!$msgContainer.hasClass('d-none')) { + $msgContainer.addClass('d-none'); + } + that.header.appendTo(j); + j.append(content); + + // Listen scroll event to load more rows + $('.pg-prop-content').on('scroll', that.__loadMoreRows.bind(that)); + + that.collection.reset(that.data.splice(0, 50)); + } else { + // Do not listen the scroll event + $('.pg-prop-content').off('scroll', that.__loadMoreRows); + + $msgContainer.text(gettext('No properties are available for the selected object.')); + + } + }) + .fail(function(xhr, error) { + pgBrowser.Events.trigger( + 'pgadmin:node:retrieval:error', 'properties', xhr, error.message, item, that + ); + if (!Alertify.pgHandleItemError(xhr, error.message, { + item: item, + info: info, + })) { + Alertify.pgNotifier( + error, xhr, + S(gettext('Error retrieving properties - %s')).sprintf( + error.message || that.label).value(), function() { console.warn(arguments); }); - } - }, + } + // show failed message. + $msgContainer.text(gettext('Failed to retrieve data from the server.')); }); var onDrop = function(type) { @@ -359,6 +410,14 @@ define([ return; }.bind(that); }, + __loadMoreRows: function(e) { + let elem = e.currentTarget; + if ((elem.scrollHeight - 10) < elem.scrollTop + elem.offsetHeight) { + if (this.data.length > 0) { + this.collection.add(this.data.splice(0, 50)); + } + } + }, generate_url: function(item, type) { /* * Using list, and collection functions of a node to get the nodes diff --git a/web/pgadmin/misc/dependencies/__init__.py b/web/pgadmin/misc/dependencies/__init__.py new file mode 100644 index 000000000..06ceebf4a --- /dev/null +++ b/web/pgadmin/misc/dependencies/__init__.py @@ -0,0 +1,30 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2019, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +"""A blueprint module providing utility functions for the application.""" + +from flask import url_for +from pgadmin.utils import PgAdminModule + +MODULE_NAME = 'dependencies' + + +class DependenciesModule(PgAdminModule): + def get_own_javascripts(self): + return [{ + 'name': 'pgadmin.browser.dependencies', + 'path': url_for('dependencies.static', + filename='js/dependencies'), + 'when': None + }] + + +# Initialise the module +blueprint = DependenciesModule(MODULE_NAME, __name__, + url_prefix='/misc/dependencies') diff --git a/web/pgadmin/misc/dependencies/static/js/dependencies.js b/web/pgadmin/misc/dependencies/static/js/dependencies.js new file mode 100644 index 000000000..664a27fee --- /dev/null +++ b/web/pgadmin/misc/dependencies/static/js/dependencies.js @@ -0,0 +1,246 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2019, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +define('misc.dependencies', [ + 'sources/gettext', 'underscore', 'underscore.string', 'jquery', 'backbone', + 'pgadmin.browser', 'pgadmin.alertifyjs', 'pgadmin.backgrid', +], function(gettext, _, S, $, Backbone, pgBrowser, Alertify, Backgrid) { + + if (pgBrowser.NodeDependencies) + return pgBrowser.NodeDependencies; + + var wcDocker = window.wcDocker; + + pgBrowser.NodeDependencies = pgBrowser.NodeDependencies || {}; + + _.extend(pgBrowser.NodeDependencies, { + init: function() { + if (this.initialized) { + return; + } + + this.initialized = true; + this.dependenciesPanel = pgBrowser.docker.findPanels('dependencies')[0]; + /* Parameter is used to set the proper label of the + * backgrid header cell. + */ + _.bindAll(this, 'showDependencies', '__loadMoreRows', '__appendGridToPanel'); + + // Defining Backbone Model for Dependencies. + var Model = Backbone.Model.extend({ + defaults: { + icon: 'icon-unknown', + type: undefined, + name: undefined, + /* field contains 'Database Name' for 'Tablespace and Role node', + * for other node it contains 'Restriction'. + */ + field: undefined, + }, + // This function is used to fetch/set the icon for the type(Function, Role, Database, ....) + parse: function(res) { + var node = pgBrowser.Nodes[res.type]; + res.icon = node ? (_.isFunction(node['node_image']) ? + (node['node_image']).apply(node, [null, null]) : + (node['node_image'] || ('icon-' + res.type))) : + ('icon-' + res.type); + res.type = S.titleize(res.type.replace(/_/g, ' '), true); + return res; + }, + }); + + // Defining Backbone Collection for Dependencies. + this.dependenciesCollection = new(Backbone.Collection.extend({ + model: Model, + }))(null); + + pgBrowser.Events.on('pgadmin-browser:tree:selected', this.showDependencies); + this.__appendGridToPanel(); + }, + + /* Function is used to create and render backgrid with + * empty collection. We just want to add backgrid into the + * panel only once. + */ + __appendGridToPanel: function() { + var $container = this.dependenciesPanel.layout().scene().find('.pg-panel-content'), + $gridContainer = $container.find('.pg-panel-dependencies-container'), + grid = new Backgrid.Grid({ + emptyText: 'No data found', + columns: [{ + name: 'type', + label: gettext('Type'), + // Extend it to render the icon as per the type. + cell: Backgrid.Cell.extend({ + render: function() { + Backgrid.Cell.prototype.render.apply(this, arguments); + this.$el.prepend($('', { + class: 'wcTabIcon ' + this.model.get('icon'), + })); + return this; + }, + }), + editable: false, + }, + { + name: 'name', + label: gettext('Name'), + cell: 'string', + editable: false, + }, + { + name: 'field', + label: '', // label kept blank, it will change dynamically + cell: 'string', + editable: false, + }, + ], + + collection: this.dependenciesCollection, + className: 'backgrid table presentation table-bordered table-noouter-border table-hover', + }); + + // Condition is used to save grid object to change the label of the header. + this.dependenciesGrid = grid; + + $gridContainer.append(grid.render().el); + + return true; + }, + + // Fetch the actual data and update the collection + showDependencies: function(item, data, node) { + let self = this, + msg = gettext('Please select an object in the tree view.'), + panel = this.dependenciesPanel, + $container = panel.layout().scene().find('.pg-panel-content'), + $msgContainer = $container.find('.pg-panel-depends-message'), + $gridContainer = $container.find('.pg-panel-dependencies-container'), + treeHierarchy = node.getTreeNodeHierarchy(item), + n_type = data._type, + url = node.generate_url(item, 'dependency', data, true); + + if (node) { + /* We fetch the Dependencies and Dependencies tab only for + * those node who set the parameter hasDepends to true. + */ + msg = gettext('No dependency information is available for the selected object.'); + if (node.hasDepends) { + /* Updating the label for the 'field' type of the backbone model. + * Label should be "Database" if the node type is tablespace or role + * and dependencies tab is selected. For other nodes and dependencies tab + * it should be 'Restriction'. + */ + + this.dependenciesGrid.columns.models[2].set({ + 'label': gettext('Restriction'), + }); + + // Hide message container and show grid container. + $msgContainer.addClass('d-none'); + $gridContainer.removeClass('d-none'); + + var timer = ''; + $.ajax({ + url: url, + type: 'GET', + beforeSend: function() { + // Generate a timer for the request + timer = setTimeout(function() { + // notify user if request is taking longer than 1 second + + $msgContainer.text(gettext('Fetching dependency information from the server...')); + $msgContainer.removeClass('d-none'); + msg = ''; + + }, 1000); + }, + }) + .done(function(res) { + clearTimeout(timer); + + if (res.length > 0) { + + if (!$msgContainer.hasClass('d-none')) { + $msgContainer.addClass('d-none'); + } + $gridContainer.removeClass('d-none'); + + self.dependenciesData = res; + + // Load only 100 rows + self.dependenciesCollection.reset(self.dependenciesData.splice(0, 100), {parse: true}); + + // Load more rows on scroll down + pgBrowser.Events.on( + 'pgadmin-browser:panel-dependencies:' + + wcDocker.EVENT.SCROLLED, + self.__loadMoreRows + ); + + } else { + // Do not listen the scroll event + pgBrowser.Events.off( + 'pgadmin-browser:panel-dependencies:' + + wcDocker.EVENT.SCROLLED + ); + + self.dependenciesCollection.reset({silent: true}); + $msgContainer.text(msg); + $msgContainer.removeClass('d-none'); + + if (!$gridContainer.hasClass('d-none')) { + $gridContainer.addClass('d-none'); + } + } + + + }) + .fail(function(xhr, error, message) { + var _label = treeHierarchy[n_type].label; + pgBrowser.Events.trigger( + 'pgadmin:node:retrieval:error', 'depends', xhr, error, message + ); + if (!Alertify.pgHandleItemError(xhr, error, message, { + item: item, + info: treeHierarchy, + })) { + Alertify.pgNotifier( + error, xhr, + S(gettext('Error retrieving data from the server: %s')).sprintf( + message || _label).value(), function() { + console.warn(arguments); + }); + } + // show failed message. + $msgContainer.text(gettext('Failed to retrieve data from the server.')); + }); + } + } if (msg != '') { + $msgContainer.text(msg); + $msgContainer.removeClass('d-none'); + if (!$gridContainer.hasClass('d-none')) { + $gridContainer.addClass('d-none'); + } + } + }, + __loadMoreRows: function() { + if (this.dependenciesPanel.length < 1) return ; + + let elem = this.dependenciesPanel.$container.find('.pg-panel-dependencies-container').closest('.wcFrameCenter')[0]; + if ((elem.scrollHeight - 10) < elem.scrollTop + elem.offsetHeight) { + if (this.dependenciesData.length > 0) { + this.dependenciesCollection.add(this.dependenciesData.splice(0, 100), {parse: true}); + } + } + }, + }); + + return pgBrowser.NodeDependencies; +}); diff --git a/web/pgadmin/misc/depends/__init__.py b/web/pgadmin/misc/dependents/__init__.py similarity index 64% rename from web/pgadmin/misc/depends/__init__.py rename to web/pgadmin/misc/dependents/__init__.py index 3ae8abceb..cb6c56629 100644 --- a/web/pgadmin/misc/depends/__init__.py +++ b/web/pgadmin/misc/dependents/__init__.py @@ -12,17 +12,18 @@ from flask import url_for from pgadmin.utils import PgAdminModule -MODULE_NAME = 'depends' +MODULE_NAME = 'dependents' -class DependsModule(PgAdminModule): +class DependentsModule(PgAdminModule): def get_own_javascripts(self): return [{ - 'name': 'pgadmin.browser.object_depends', - 'path': url_for('depends.static', filename='js/depends'), + 'name': 'pgadmin.browser.dependents', + 'path': url_for('dependents.static', filename='js/dependents'), 'when': None }] # Initialise the module -blueprint = DependsModule(MODULE_NAME, __name__, url_prefix='/misc/depends') +blueprint = DependentsModule(MODULE_NAME, __name__, + url_prefix='/misc/dependents') diff --git a/web/pgadmin/misc/dependents/static/js/dependents.js b/web/pgadmin/misc/dependents/static/js/dependents.js new file mode 100644 index 000000000..cf2820dac --- /dev/null +++ b/web/pgadmin/misc/dependents/static/js/dependents.js @@ -0,0 +1,252 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2019, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +define('misc.dependents', [ + 'sources/gettext', 'underscore', 'underscore.string', 'jquery', 'backbone', + 'pgadmin.browser', 'pgadmin.alertifyjs', 'pgadmin.backgrid', +], function(gettext, _, S, $, Backbone, pgBrowser, Alertify, Backgrid) { + + if (pgBrowser.NodeDependents) + return pgBrowser.NodeDependents; + + var wcDocker = window.wcDocker; + + pgBrowser.NodeDependents = pgBrowser.NodeDependents || {}; + + _.extend(pgBrowser.NodeDependents, { + init: function() { + if (this.initialized) { + return; + } + + this.initialized = true; + this.dependentsPanel = pgBrowser.docker.findPanels('dependents')[0]; + /* Parameter is used to set the proper label of the + * backgrid header cell. + */ + _.bindAll(this, 'showDependents', '__loadMoreRows', '__appendGridToPanel'); + + // Defining Backbone Model for Dependents. + var Model = Backbone.Model.extend({ + defaults: { + icon: 'icon-unknown', + type: undefined, + name: undefined, + /* field contains 'Database Name' for 'Tablespace and Role node', + * for other node it contains 'Restriction'. + */ + field: undefined, + }, + // This function is used to fetch/set the icon for the type(Function, Role, Database, ....) + parse: function(res) { + var node = pgBrowser.Nodes[res.type]; + res.icon = node ? (_.isFunction(node['node_image']) ? + (node['node_image']).apply(node, [null, null]) : + (node['node_image'] || ('icon-' + res.type))) : + ('icon-' + res.type); + res.type = S.titleize(res.type.replace(/_/g, ' '), true); + return res; + }, + }); + + // Defining Backbone Collection for Dependents. + this.dependentCollection = new(Backbone.Collection.extend({ + model: Model, + }))(null); + + pgBrowser.Events.on('pgadmin-browser:tree:selected', this.showDependents); + pgBrowser.Events.on('pgadmin-browser:tree:refreshing', this.refreshDependents, this); + this.__appendGridToPanel(); + }, + + /* Function is used to create and render backgrid with + * empty collection. We just want to add backgrid into the + * panel only once. + */ + __appendGridToPanel: function() { + var $container = this.dependentsPanel.layout().scene().find('.pg-panel-content'), + $gridContainer = $container.find('.pg-panel-dependents-container'), + grid = new Backgrid.Grid({ + emptyText: 'No data found', + columns: [{ + name: 'type', + label: gettext('Type'), + // Extend it to render the icon as per the type. + cell: Backgrid.Cell.extend({ + render: function() { + Backgrid.Cell.prototype.render.apply(this, arguments); + this.$el.prepend($('', { + class: 'wcTabIcon ' + this.model.get('icon'), + })); + return this; + }, + }), + editable: false, + }, + { + name: 'name', + label: gettext('Name'), + cell: 'string', + editable: false, + }, + { + name: 'field', + label: '', // label kept blank, it will change dynamically + cell: 'string', + editable: false, + }, + ], + + collection: this.dependentCollection, + className: 'backgrid table presentation table-bordered table-noouter-border table-hover', + }); + + // Condition is used to save grid object to change the label of the header. + this.dependentGrid = grid; + + $gridContainer.append(grid.render().el); + + return true; + }, + + // Fetch the actual data and update the collection + showDependents: function(item, data, node) { + let self = this, + msg = gettext('Please select an object in the tree view.'), + panel = this.dependentsPanel, + $container = panel.layout().scene().find('.pg-panel-content'), + $msgContainer = $container.find('.pg-panel-depends-message'), + $gridContainer = $container.find('.pg-panel-dependents-container'), + treeHierarchy = node.getTreeNodeHierarchy(item), + n_type = data._type, + url = node.generate_url(item, 'dependent', data, true); + + if (node) { + /* We fetch the Dependencies and Dependents tab only for + * those node who set the parameter hasDepends to true. + */ + msg = gettext('No dependent information is available for the selected object.'); + if (node.hasDepends) { + /* Updating the label for the 'field' type of the backbone model. + * Label should be "Database" if the node type is tablespace or role + * and dependent tab is selected. For other nodes and dependencies tab + * it should be 'Restriction'. + */ + if (node.type == 'tablespace' || node.type == 'role') { + this.dependentGrid.columns.models[2].set({ + 'label': gettext('Database'), + }); + } else { + this.dependentGrid.columns.models[2].set({ + 'label': gettext('Restriction'), + }); + } + + // Hide message container and show grid container. + $msgContainer.addClass('d-none'); + $gridContainer.removeClass('d-none'); + + var timer = ''; + $.ajax({ + url: url, + type: 'GET', + beforeSend: function() { + // Generate a timer for the request + timer = setTimeout(function() { + // notify user if request is taking longer than 1 second + + $msgContainer.text(gettext('Fetching dependent information from the server...')); + $msgContainer.removeClass('d-none'); + msg = ''; + + }, 1000); + }, + }) + .done(function(res) { + clearTimeout(timer); + + if (res.length > 0) { + + if (!$msgContainer.hasClass('d-none')) { + $msgContainer.addClass('d-none'); + } + $gridContainer.removeClass('d-none'); + + self.dependentData = res; + + // Load only 100 rows + self.dependentCollection.reset(self.dependentData.splice(0, 100), {parse: true}); + + // Load more rows on scroll down + pgBrowser.Events.on( + 'pgadmin-browser:panel-dependents:' + + wcDocker.EVENT.SCROLLED, + self.__loadMoreRows + ); + + } else { + // Do not listen the scroll event + pgBrowser.Events.off( + 'pgadmin-browser:panel-dependents:' + + wcDocker.EVENT.SCROLLED + ); + + self.dependentCollection.reset({silent: true}); + $msgContainer.text(msg); + $msgContainer.removeClass('d-none'); + + if (!$gridContainer.hasClass('d-none')) { + $gridContainer.addClass('d-none'); + } + } + + + }) + .fail(function(xhr, error, message) { + var _label = treeHierarchy[n_type].label; + pgBrowser.Events.trigger( + 'pgadmin:node:retrieval:error', 'depends', xhr, error, message + ); + if (!Alertify.pgHandleItemError(xhr, error, message, { + item: item, + info: treeHierarchy, + })) { + Alertify.pgNotifier( + error, xhr, + S(gettext('Error retrieving data from the server: %s')).sprintf( + message || _label).value(), function() { + console.warn(arguments); + }); + } + // show failed message. + $msgContainer.text(gettext('Failed to retrieve data from the server.')); + }); + } + } if (msg != '') { + $msgContainer.text(msg); + $msgContainer.removeClass('d-none'); + if (!$gridContainer.hasClass('d-none')) { + $gridContainer.addClass('d-none'); + } + } + }, + __loadMoreRows: function() { + if (this.dependentsPanel.length < 1) return ; + + let elem = this.dependentsPanel.$container.find('.pg-panel-dependents-container').closest('.wcFrameCenter')[0]; + if ((elem.scrollHeight - 10) < elem.scrollTop + elem.offsetHeight) { + if (this.dependentData.length > 0) { + this.dependentCollection.add(this.dependentData.splice(0, 100), {parse: true}); + } + } + }, + }); + + return pgBrowser.NodeDependents; +}); diff --git a/web/pgadmin/misc/depends/static/js/depends.js b/web/pgadmin/misc/depends/static/js/depends.js deleted file mode 100644 index 050b9c56e..000000000 --- a/web/pgadmin/misc/depends/static/js/depends.js +++ /dev/null @@ -1,432 +0,0 @@ -///////////////////////////////////////////////////////////// -// -// pgAdmin 4 - PostgreSQL Tools -// -// Copyright (C) 2013 - 2019, The pgAdmin Development Team -// This software is released under the PostgreSQL Licence -// -////////////////////////////////////////////////////////////// - -define('misc.depends', [ - 'sources/gettext', 'underscore', 'underscore.string', 'jquery', 'backbone', - 'pgadmin.browser', 'pgadmin.alertifyjs', 'pgadmin.backgrid', -], function(gettext, _, S, $, Backbone, pgBrowser, Alertify, Backgrid) { - - if (pgBrowser.ShowNodeDepends) - return pgBrowser.ShowNodeDepends; - - var wcDocker = window.wcDocker; - - pgBrowser.ShowNodeDepends = pgBrowser.ShowNodeDepends || {}; - - _.extend(pgBrowser.ShowNodeDepends, { - init: function() { - if (this.initialized) { - return; - } - - this.initialized = true; - /* Parameter is used to set the proper label of the - * backgrid header cell. - */ - _.bindAll(this, 'showDependents', 'dependentsPanelVisibilityChanged', - 'showDependencies', 'dependenciesPanelVisibilityChanged', '__updateCollection' - ); - - // We will listened to the visibility change of the Dependencies and Dependents panel - pgBrowser.Events.on('pgadmin-browser:panel-dependencies:' + wcDocker.EVENT.VISIBILITY_CHANGED, - this.dependenciesPanelVisibilityChanged); - pgBrowser.Events.on('pgadmin-browser:panel-dependents:' + wcDocker.EVENT.VISIBILITY_CHANGED, - this.dependentsPanelVisibilityChanged); - - // Defining Backbone Model for Dependencies and Dependents. - var Model = Backbone.Model.extend({ - defaults: { - icon: 'icon-unknown', - type: undefined, - name: undefined, - /* field contains 'Database Name' for 'Tablespace and Role node', - * for other node it contains 'Restriction'. - */ - field: undefined, - }, - // This function is used to fetch/set the icon for the type(Function, Role, Database, ....) - parse: function(res) { - var node = pgBrowser.Nodes[res.type]; - res.icon = node ? (_.isFunction(node['node_image']) ? - (node['node_image']).apply(node, [null, null]) : - (node['node_image'] || ('icon-' + res.type))) : - ('icon-' + res.type); - res.type = S.titleize(res.type.replace(/_/g, ' '), true); - return res; - }, - }); - - // Defining Backbone Collection for Dependents. - this.dependentCollection = new(Backbone.Collection.extend({ - model: Model, - }))(null); - - // Defining Backbone Collection for Dependencies. - this.dependenciesCollection = new(Backbone.Collection.extend({ - model: Model, - }))(null); - - var self = this; - - /* Function is used to create and render backgrid with - * empty collection. We just want to add backgrid into the - * panel only once. - */ - var appendGridToPanel = function(collection, panel, is_dependent) { - var $container = panel[0].layout().scene().find('.pg-panel-content'), - $gridContainer = $container.find('.pg-panel-depends-container'), - grid = new Backgrid.Grid({ - emptyText: 'No data found', - columns: [{ - name: 'type', - label: gettext('Type'), - // Extend it to render the icon as per the type. - cell: Backgrid.Cell.extend({ - render: function() { - Backgrid.Cell.prototype.render.apply(this, arguments); - this.$el.prepend($('', { - class: 'wcTabIcon ' + this.model.get('icon'), - })); - return this; - }, - }), - editable: false, - }, - { - name: 'name', - label: gettext('Name'), - cell: 'string', - editable: false, - }, - { - name: 'field', - label: '', // label kept blank, it will change dynamically - cell: 'string', - editable: false, - }, - ], - - collection: collection, - className: 'backgrid table presentation table-bordered table-noouter-border table-hover', - }); - - // Condition is used to save grid object to change the label of the header. - if (is_dependent) - self.dependentGrid = grid; - else - self.dependenciesGrid = grid; - - $gridContainer.append(grid.render().el); - - return true; - }; - - // We will listened to the visibility change of the Dependencies and Dependents panel - pgBrowser.Events.on('pgadmin-browser:panel-dependencies:' + wcDocker.EVENT.VISIBILITY_CHANGED, - this.dependenciesPanelVisibilityChanged); - pgBrowser.Events.on('pgadmin-browser:panel-dependents:' + wcDocker.EVENT.VISIBILITY_CHANGED, - this.dependentsPanelVisibilityChanged); - pgBrowser.Events.on( - 'pgadmin:browser:node:updated', - function() { - if (this.dependenciesPanels && this.dependenciesPanels.length) { - $(this.dependenciesPanels[0]).data('node-prop', ''); - this.dependenciesPanelVisibilityChanged(this.dependenciesPanels[0]); - } - if (this.dependentsPanels && this.dependentsPanels.length) { - $(this.dependentsPanels[0]).data('node-prop', ''); - this.dependentsPanelVisibilityChanged(this.dependentsPanels[0]); - } - }, this - ); - - // We will render the grid objects in the panel after some time, because - - // it is possible, it is not yet available. - // Find the panels to render the grid. - var dependenciesPanels = this.dependenciesPanels = pgBrowser.docker.findPanels('dependencies'); - var dependentsPanels = this.dependentsPanels = pgBrowser.docker.findPanels('dependents'); - - if (dependenciesPanels.length == 0) { - pgBrowser.Events.on( - 'pgadmin-browser:panel-dependencies:' + wcDocker.EVENT.INIT, - function() { - this.dependenciesPanels = pgBrowser.docker.findPanels('dependencies'); - appendGridToPanel(this.dependenciesCollection, this.dependenciesPanels, false); - - // If Dependencies panel exists and is focused then we need to listen the browser tree selection events. - if ((dependenciesPanels[0].isVisible()) || dependenciesPanels.length != 1) { - pgBrowser.Events.on('pgadmin-browser:tree:selected', this.showDependencies); - pgBrowser.Events.on('pgadmin-browser:tree:refreshing', this.refreshDependencies, this); - } - }.bind(this) - ); - } else { - appendGridToPanel(this.dependenciesCollection, this.dependenciesPanels, false); - - // If Dependencies panel exists and is focused then we need to listen the browser tree selection events. - if ((dependenciesPanels[0].isVisible()) || dependenciesPanels.length != 1) { - pgBrowser.Events.on('pgadmin-browser:tree:selected', this.showDependencies); - pgBrowser.Events.on('pgadmin-browser:tree:refreshing', this.refreshDependencies, this); - } - } - - if (dependentsPanels.length == 0) { - pgBrowser.Events.on( - 'pgadmin-browser:panel-dependents:' + wcDocker.EVENT.INIT, - function() { - this.dependentsPanels = pgBrowser.docker.findPanels('dependents'); - appendGridToPanel(this.dependentCollection, this.dependentsPanels, true); - - // If Dependents panel exists and is focused then we need to listen the browser tree selection events. - if ((dependentsPanels[0].isVisible()) || dependentsPanels.length != 1) { - pgBrowser.Events.on('pgadmin-browser:tree:selected', this.showDependents); - pgBrowser.Events.on('pgadmin-browser:tree:refreshing', this.refreshDependents, this); - } - }.bind(this) - ); - } else { - appendGridToPanel(this.dependentCollection, this.dependentsPanels, true); - - // If Dependents panel exists and is focused then we need to listen the browser tree selection events. - if ((dependentsPanels[0].isVisible()) || dependentsPanels.length != 1) { - pgBrowser.Events.on('pgadmin-browser:tree:selected', this.showDependents); - pgBrowser.Events.on('pgadmin-browser:tree:refreshing', this.refreshDependents, this); - } - } - }, - - // Fetch the actual data and update the collection - __updateCollection: function(collection, panel, url, messages, node, item, type) { - var msg = messages[0], - $container = panel[0].layout().scene().find('.pg-panel-content'), - $msgContainer = $container.find('.pg-panel-depends-message'), - $gridContainer = $container.find('.pg-panel-depends-container'), - treeHierarchy = node.getTreeNodeHierarchy(item), - n_type = type, - cache_flag = { - node_type: n_type, - url: url, - }; - - // Avoid unnecessary reloads - if (_.isEqual($(panel[0]).data('node-prop'), cache_flag)) { - return; - } - - // Cache the current IDs for next time - $(panel[0]).data('node-prop', cache_flag); - - // Hide the grid container and show the default message container - if (!$gridContainer.hasClass('d-none')) - $gridContainer.addClass('d-none'); - $msgContainer.removeClass('d-none'); - - if (node) { - msg = messages[1]; - /* We fetch the Dependencies and Dependents tab only for - * those node who set the parameter hasDepends to true. - */ - if (node.hasDepends) { - /* Set the message because ajax request may take time to - * fetch the information from the server. - */ - msg = messages[2]; - $msgContainer.text(msg); - - /* Updating the label for the 'field' type of the backbone model. - * Label should be "Database" if the node type is tablespace or role - * and dependent tab is selected. For other nodes and dependencies tab - * it should be 'Restriction'. - */ - if (this.dependent && (node.type == 'tablespace' || node.type == 'role')) - this.dependentGrid.columns.models[2].set({ - 'label': gettext('Database'), - }); - else { - this.dependenciesGrid.columns.models[2].set({ - 'label': gettext('Restriction'), - }); - this.dependentGrid.columns.models[2].set({ - 'label': gettext('Restriction'), - }); - } - - // Hide message container and show grid container. - $msgContainer.addClass('d-none'); - $gridContainer.removeClass('d-none'); - - var timer = setTimeout(function() { - // notify user if request is taking longer than 1 second - - $msgContainer.text(gettext('Retrieving data from the server...')); - $msgContainer.removeClass('d-none'); - if ($gridContainer) { - $gridContainer.addClass('d-none'); - } - }, 1000); - - // Set the url, fetch the data and update the collection - collection.url = url; - collection.fetch({ - reset: true, - success: function() { - clearTimeout(timer); - $gridContainer.removeClass('d-none'); - if (!$msgContainer.hasClass('d-none')) { - $msgContainer.addClass('d-none'); - } - }, - error: function(coll, xhr, error, message) { - var _label = treeHierarchy[n_type].label; - pgBrowser.Events.trigger( - 'pgadmin:node:retrieval:error', 'depends', xhr, error, message - ); - if (!Alertify.pgHandleItemError(xhr, error, message, { - item: item, - info: treeHierarchy, - })) { - Alertify.pgNotifier( - error, xhr, - S( - gettext('Error retrieving the information - %s') - ).sprintf(message || _label).value(), - function() { - console.warn(arguments); - } - ); - } - // show failed message. - $msgContainer.text(gettext('Failed to retrieve data from the server.')); - }, - }); - } - } - if (msg != '') { - $msgContainer.text(msg); - } - }, - refreshDependents: function(item, data, node) { - var that = this, - cache_flag = { - node_type: data._type, - url: node.generate_url(item, 'dependent', data, true), - }; - - if (_.isEqual($(that.dependentsPanels[0]).data('node-prop'), cache_flag)) { - // Reset the current item selection - $(that.dependentsPanels[0]).data('node-prop', ''); - that.showDependents(item, data, node); - } - }, - showDependents: function(item, data, node) { - /** - * We can't start fetching the Dependents immediately, it is possible the user - * is just using the keyboard to select the node, and just traversing - * through. We will wait for some time before fetching the Dependents - **/ - var self = this; - if (!node) { - return; - } - self.dependent = true; - if (self.timeout) { - clearTimeout(self.timeout); - } - self.timeout = setTimeout( - self.__updateCollection( - self.dependentCollection, self.dependentsPanels, - node.generate_url(item, 'dependent', data, true), [ - gettext('No object selected.'), - gettext('No dependent information is available for the current object.'), - gettext('Fetching dependent information from the server...'), - ], node, item, data._type - ), 400 - ); - }, - dependentsPanelVisibilityChanged: function(panel) { - if (panel.isVisible()) { - var t = pgBrowser.tree, - i = t.selected(), - d = i && t.itemData(i), - n = i && d && pgBrowser.Nodes[d._type]; - - pgBrowser.ShowNodeDepends.showDependents.apply(pgBrowser.ShowNodeDepends, [i, d, n]); - - // We will start listening the tree selection event. - pgBrowser.Events.on('pgadmin-browser:tree:selected', pgBrowser.ShowNodeDepends.showDependents); - pgBrowser.Events.on('pgadmin-browser:tree:refreshing', pgBrowser.ShowNodeDepends.refreshDependents, this); - } else { - - // We don't need to listen the tree item selection event. - pgBrowser.Events.off('pgadmin-browser:tree:selected', pgBrowser.ShowNodeDepends.showDependents); - pgBrowser.Events.off('pgadmin-browser:tree:refreshing', pgBrowser.ShowNodeDepends.refreshDependents, this); - } - }, - refreshDependencies: function(item, data, node) { - var that = this, - cache_flag = { - node_type: data._type, - url: node.generate_url(item, 'dependency', data, true), - }; - - if (_.isEqual($(that.dependenciesPanels[0]).data('node-prop'), cache_flag)) { - // Reset the current item selection - $(that.dependenciesPanels[0]).data('node-prop', ''); - that.showDependencies(item, data, node); - } - }, - showDependencies: function(item, data, node) { - /** - * We can't start fetching the Dependencies immediately, it is possible the user - * is just using the keyboard to select the node, and just traversing - * through. We will wait for some time before fetching the Dependencies - **/ - var self = this; - if (!node) { - return; - } - self.dependent = false; - if (self.timeout) { - clearTimeout(self.timeout); - } - self.timeout = setTimeout( - self.__updateCollection( - self.dependenciesCollection, - self.dependenciesPanels, - node.generate_url(item, 'dependency', data, true), [gettext('Please select an object in the tree view.'), gettext('No dependency information is available for the current object.'), - gettext('Fetching dependency information from the server...'), - ], - node, - item, - data._type - ), 400 - ); - }, - dependenciesPanelVisibilityChanged: function(panel) { - if (panel.isVisible()) { - var t = pgBrowser.tree, - i = t.selected(), - d = i && t.itemData(i), - n = i && d && pgBrowser.Nodes[d._type]; - - pgBrowser.ShowNodeDepends.showDependencies.apply(pgBrowser.ShowNodeDepends, [i, d, n]); - - // We will start listening the tree selection event. - pgBrowser.Events.on('pgadmin-browser:tree:selected', pgBrowser.ShowNodeDepends.showDependencies); - pgBrowser.Events.on('pgadmin-browser:tree:refreshing', pgBrowser.ShowNodeDepends.refreshDependencies, this); - } else { - // We don't need to listen the tree item selection event. - pgBrowser.Events.off('pgadmin-browser:tree:selected', pgBrowser.ShowNodeDepends.showDependencies); - pgBrowser.Events.off('pgadmin-browser:tree:refreshing', pgBrowser.ShowNodeDepends.refreshDependencies, this); - } - }, - }); - - return pgBrowser.ShowNodeDepends; -}); diff --git a/web/webpack.config.js b/web/webpack.config.js index 10ae99693..77bc0fcc3 100644 --- a/web/webpack.config.js +++ b/web/webpack.config.js @@ -235,7 +235,8 @@ module.exports = { 'pgadmin.dashboard' + ',pgadmin.tools.user_management' + ',pgadmin.browser.object_statistics' + - ',pgadmin.browser.object_depends' + + ',pgadmin.browser.dependencies' + + ',pgadmin.browser.dependents' + ',pgadmin.browser.object_sql' + ',pgadmin.browser.bgprocess' + ',pgadmin.node.server_group' + diff --git a/web/webpack.shim.js b/web/webpack.shim.js index be2449494..b9645625a 100644 --- a/web/webpack.shim.js +++ b/web/webpack.shim.js @@ -195,7 +195,8 @@ var webpackShimConfig = { 'pgadmin.browser.messages': '/browser/js/messages', 'pgadmin.browser.node': path.join(__dirname, './pgadmin/browser/static/js/node'), 'pgadmin.browser.node.ui': path.join(__dirname, './pgadmin/browser/static/js/node.ui'), - 'pgadmin.browser.object_depends': path.join(__dirname, './pgadmin/misc/depends/static/js/depends'), + 'pgadmin.browser.dependencies': path.join(__dirname, './pgadmin/misc/dependencies/static/js/dependencies'), + 'pgadmin.browser.dependents': path.join(__dirname, './pgadmin/misc/dependents/static/js/dependents'), 'pgadmin.browser.object_sql': path.join(__dirname, './pgadmin/misc/sql/static/js/sql'), 'pgadmin.browser.object_statistics': path.join(__dirname, './pgadmin/misc/statistics/static/js/statistics'), 'pgadmin.browser.panel': path.join(__dirname, './pgadmin/browser/static/js/panel'),