Optimise display of Dependencies and Dependents, and use on-demand loading of rows in batches of 100. Fixes #3959

This commit is contained in:
Khushboo Vashi
2019-02-14 11:15:01 +00:00
committed by Dave Page
parent e5e154e268
commit 622b67684d
10 changed files with 623 additions and 464 deletions

View File

@@ -21,6 +21,7 @@ Bug fixes
| `Bug #3906 <https://redmine.postgresql.org/issues/3906>`_ - Fix alignment of Close and Maximize button of Grant Wizard.
| `Bug #3942 <https://redmine.postgresql.org/issues/3942>`_ - Close connections gracefully when the user logs out of pgAdmin.
| `Bug #3946 <https://redmine.postgresql.org/issues/3946>`_ - Fix alignment of checkbox to drop multiple schedules of pgAgent job.
| `Bug #3959 <https://redmine.postgresql.org/issues/3959>`_ - Optimise display of Dependencies and Dependents, and use on-demand loading of rows in batches of 100.
| `Bug #3963 <https://redmine.postgresql.org/issues/3963>`_ - Fix alignment of import/export toggle switch.
| `Bug #3970 <https://redmine.postgresql.org/issues/3970>`_ - Prevent an error when closing the Sort/Filter dialogue with an empty filter string.
| `Bug #3974 <https://redmine.postgresql.org/issues/3974>`_ - Fix alignment of Connection type toggle switch of pgagent.

View File

@@ -189,7 +189,7 @@ define('pgadmin.browser', [
width: 500,
isCloseable: false,
isPrivate: true,
content: '<div class="negative-space p-2"><div class="alert alert-info pg-panel-message pg-panel-depends-message">' + select_object_msg + '</div><div class="pg-panel-depends-container d-none"></div></div>',
content: '<div class="negative-space p-2"><div class="alert alert-info pg-panel-message pg-panel-depends-message">' + select_object_msg + '</div><div class="pg-panel-dependencies-container d-none"></div></div>',
events: panelEvents,
}),
// Dependents of the object
@@ -200,7 +200,7 @@ define('pgadmin.browser', [
width: 500,
isCloseable: false,
isPrivate: true,
content: '<div class="negative-space p-2"><div class="alert alert-info pg-panel-message pg-panel-depends-message">' + select_object_msg + '</div><div class="pg-panel-depends-container d-none"></div></div>',
content: '<div class="negative-space p-2"><div class="alert alert-info pg-panel-message pg-panel-depends-message">' + select_object_msg + '</div><div class="pg-panel-dependents-container d-none"></div></div>',
events: panelEvents,
}),
},

View File

@@ -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 = $('<div></div>')
.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 = '<div class="alert alert-info pg-panel-message pg-panel-properties-message">' +
gettext('Retrieving data from the server...') + '</div>';
$msgContainer = $($msgContainer).appendTo(j);
that.header = $('<div></div>').addClass(
'pg-prop-header'
).appendTo(j);
);
// Render the buttons
var buttons = [];
@@ -255,26 +263,69 @@ define([
// Render subNode grid
content.append('<div class="pg-prop-coll-container"></div>');
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

View File

@@ -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')

View File

@@ -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($('<i>', {
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;
});

View File

@@ -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')

View File

@@ -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($('<i>', {
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;
});

View File

@@ -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($('<i>', {
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;
});

View File

@@ -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' +

View File

@@ -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'),