pgadmin4/web/pgadmin/browser/static/js/node.js

1902 lines
64 KiB
JavaScript
Raw Normal View History

2019-01-02 04:24:12 -06:00
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
2020-01-02 08:43:50 -06:00
// Copyright (C) 2013 - 2020, The pgAdmin Development Team
2019-01-02 04:24:12 -06:00
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
define('pgadmin.browser.node', [
'sources/tree/pgadmin_tree_node', 'sources/url_for',
'sources/gettext', 'jquery', 'underscore', 'sources/pgadmin',
'pgadmin.browser.menu', 'backbone', 'pgadmin.alertifyjs', 'pgadmin.browser.datamodel',
'backform', 'sources/browser/generate_url', 'pgadmin.help', 'sources/utils',
'pgadmin.browser.utils', 'pgadmin.backform',
], function(
pgadminTreeNode, url_for,
gettext, $, _, pgAdmin,
Menu, Backbone, Alertify, pgBrowser,
Backform, generateUrl, help,
commonUtils
) {
var wcDocker = window.wcDocker,
keyCode = {
ENTER: 13,
ESCAPE: 27,
F1: 112,
};
// It has already been defined.
// Avoid running this script again.
if (pgBrowser.Node)
return pgBrowser.Node;
pgBrowser.Nodes = pgBrowser.Nodes || {};
// A helper (base) class for all the nodes, this has basic
// operations/callbacks defined for basic operation.
pgBrowser.Node = function() {};
// Helper function to correctly set up the property chain, for subclasses.
// Uses a hash of class properties to be extended.
//
// It is unlikely - we will instantiate an object for this class.
// (Inspired by Backbone.extend function)
pgBrowser.Node.extend = function(props, initialize) {
var parent = this;
var child;
// The constructor function for the new subclass is defined to simply call
// the parent's constructor.
child = function() {
return parent.apply(this, arguments);
};
// Add static properties to the constructor function, if supplied.
_.extend(child, parent, _.omit(props, 'callbacks'));
// Make sure - a child have all the callbacks of the parent.
child.callbacks = _.extend({}, parent.callbacks, props.callbacks);
// Let's not bind the callbacks, or initialize the child.
if (initialize === false)
return child;
var bindToChild = function(cb) {
if (typeof(child.callbacks[cb]) == 'function') {
child.callbacks[cb] = child.callbacks[cb].bind(child);
}
},
callbacks = _.keys(child.callbacks);
for (var idx = 0; idx < callbacks.length; idx++) bindToChild(callbacks[idx]);
// Registering the node by calling child.Init(...) function
child.Init.apply(child);
// Initialize the parent
this.Init.apply(child);
return child;
};
_.extend(pgAdmin.Browser.Node, Backbone.Events, {
// Node type
type: undefined,
// Label
label: '',
2016-05-16 10:25:51 -05:00
// Help pages
2016-04-12 07:35:47 -05:00
sqlAlterHelp: '',
sqlCreateHelp: '',
2016-05-16 10:25:51 -05:00
dialogHelp: '',
2016-04-12 07:35:47 -05:00
title: function(o, d) {
return o.label + (d ? (' - ' + d.label) : '');
},
hasId: true,
///////
// Initialization function
// Generally - used to register the menus for this type of node.
//
// Also, look at pgAdmin.Browser.add_menus(...) function.
//
// NOTE: Override this for each node for initialization purpose
Init: function() {
var self = this;
if (self.node_initialized)
return;
self.node_initialized = true;
pgAdmin.Browser.add_menus([{
name: 'refresh',
node: self.type,
module: self,
applies: ['object', 'context'],
callback: 'refresh',
priority: 1,
label: gettext('Refresh...'),
icon: 'fa fa-sync-alt',
}]);
2016-08-22 06:30:16 -05:00
if (self.canEdit) {
pgAdmin.Browser.add_menus([{
name: 'show_obj_properties',
node: self.type,
module: self,
applies: ['object', 'context'],
callback: 'show_obj_properties',
priority: 999,
label: gettext('Properties...'),
data: {
'action': 'edit',
},
icon: 'fa fa-edit',
enable: _.isFunction(self.canEdit) ?
function() {
return !!(self.canEdit.apply(self, arguments));
} : (!!self.canEdit),
}]);
2016-08-22 06:30:16 -05:00
}
if (self.canDrop) {
pgAdmin.Browser.add_menus([{
name: 'delete_object',
node: self.type,
module: self,
applies: ['object', 'context'],
callback: 'delete_obj',
priority: self.dropPriority,
label: (self.dropAsRemove) ? gettext('Remove %s', self.label) : gettext('Delete/Drop'),
data: {
'url': 'drop',
},
icon: 'fa fa-trash-alt',
enable: _.isFunction(self.canDrop) ?
function() {
return !!(self.canDrop.apply(self, arguments));
} : (!!self.canDrop),
}]);
if (self.canDropCascade) {
pgAdmin.Browser.add_menus([{
name: 'delete_object_cascade',
node: self.type,
module: self,
applies: ['object', 'context'],
callback: 'delete_obj',
priority: 3,
label: gettext('Drop Cascade'),
data: {
'url': 'delete',
},
icon: 'fa fa-trash-alt',
enable: _.isFunction(self.canDropCascade) ?
function() {
return self.canDropCascade.apply(self, arguments);
} : (!!self.canDropCascade),
}]);
}
}
// Show query tool only in context menu of supported nodes.
if (_.indexOf(pgAdmin.unsupported_nodes, self.type) == -1) {
let enable = function(itemData) {
if (itemData._type == 'database' && itemData.allowConn)
return true;
else if (itemData._type != 'database')
return true;
else
return false;
};
pgAdmin.Browser.add_menus([{
name: 'show_query_tool',
node: self.type,
module: self,
applies: ['context'],
callback: 'show_query_tool',
priority: 998,
label: gettext('Query Tool...'),
icon: 'pg-font-icon icon-query-tool',
enable: enable,
}]);
// show search objects same as query tool
pgAdmin.Browser.add_menus([{
name: 'search_objects', node: self.type, module: pgAdmin.SearchObjects,
applies: ['context'], callback: 'show_search_objects',
priority: 997, label: gettext('Search Objects...'),
icon: 'fa fa-search', enable: enable,
}]);
}
// This will add options of scripts eg:'CREATE Script'
if (self.hasScriptTypes && _.isArray(self.hasScriptTypes) &&
self.hasScriptTypes.length > 0) {
// For each script type create menu
_.each(self.hasScriptTypes, function(stype) {
var type_label = gettext('%s Script',stype.toUpperCase());
stype = stype.toLowerCase();
// Adding menu for each script type
pgAdmin.Browser.add_menus([{
name: 'show_script_' + stype,
node: self.type,
module: self,
applies: ['object', 'context'],
callback: 'show_script',
priority: 4,
label: type_label,
category: gettext('Scripts'),
data: {
'script': stype,
},
icon: 'fa fa-pencil-alt',
enable: self.check_user_permission,
}]);
});
}
},
///////
// Checks if Script Type is allowed to user
// First check if role node & create role allowed
// Otherwise test rest of database objects
// if no permission matched then do not allow create script
///////
check_user_permission: function(itemData, item, data) {
// Do not display CREATE script on server group and server node
if (itemData._type == 'server_group' || itemData._type == 'server') {
return false;
}
// Do not display the menu if the database connection is not allowed
if (itemData._type == 'database' && !itemData.allowConn)
return false;
var node = pgBrowser.Nodes[itemData._type],
parentData = node.getTreeNodeHierarchy(item);
if (_.indexOf(['create', 'insert', 'update', 'delete'], data.script) != -1) {
if (itemData.type == 'role' &&
parentData.server.user.can_create_role) {
return true;
} else if (
(
parentData.server && (
parentData.server.user.is_superuser ||
parentData.server.user.can_create_db)
) ||
(
parentData.schema && parentData.schema.can_create
)
) {
return true;
} else {
return false;
}
} else {
return true;
}
},
///////
// Generate a Backform view using the node's model type
//
// Used to generate view for the particular node properties, edit,
// creation.
getView: function(item, type, el, node, formType, callback, ctx, cancelFunc) {
var that = this;
if (!this.type || this.type == '')
// We have no information, how to generate view for this type.
return null;
if (this.model) {
// This will be the URL, used for object manipulation.
// i.e. Create, Update in these cases
var urlBase = this.generate_url(item, type, node, false, null, that.url_jump_after_node);
if (!urlBase)
// Ashamed of myself, I don't know how to manipulate this
// node.
return null;
var attrs = {};
// In order to get the object data from the server, we must set
// object-id in the model (except in the create mode).
if (type !== 'create') {
attrs[this.model.idAttribute || this.model.prototype.idAttribute ||
'id'] = node._id;
}
// We know - which data model to be used for this object.
var info = this.getTreeNodeHierarchy.apply(this, [item]),
newModel = new(this.model.extend({
urlRoot: urlBase,
}))(
attrs, {
node_info: info,
}
),
fields = Backform.generateViewSchema(
info, newModel, type, this, node
);
if (type == 'create' || type == 'edit') {
if (callback && ctx) {
callback = callback.bind(ctx);
} else {
callback = function() {
console.warn(
'Broke something!!! Why we don\'t have the callback or the context???'
);
};
}
var onSessionInvalid = function(msg) {
Fixed following: - Base font size changed from 0.815rem to 0.875rem, for navbar from 0.875rem to 0.925rem. - Dialog sizes made consistent throughout the application. Now there are 3 size options for width and height each - sm, md, lg. Combination of any of these to be used hereafter - Alignment fix for controls of Node properties dialogs which includes showing text and label in one line without dialog size change, checkbox alignment, switch control alignment at places and other minor improvements in other dialogs - Error message design change in dialogs validation - SQL Editor data grid editor popup design changes which were missed - Design change for dashboard server activity grid - Login page language dropdown color fix - Properties accordion collapse design fix - Help, Info icon fixed across all dialogs which were not working if clicked exactly on the text - Added missing icon with buttons at few places - Shadow behind the dialogs is increased to make it look clearly separated and depth. - Control Alignment fix in maintenance dialog - Min height of alertify dialogs set for better UX - File dialog design fix when no files found - Grant wizard fixes - Scroll bar visibility on first page, use full space for SQL generated on the last page - Browser toolbar buttons changed to sync with SQL editor toolbar buttons - Rounded corners for docker floating dialog (no properties) - Renaming file in file dialog should show original file name - SQL data grid text edit popup buttons behaviour was swapped. This is fixed. - Import/Export dialog changes as per new design.
2019-01-02 03:35:15 -06:00
var alertMessage = `
<div class="error-in-footer">
<div class="d-flex px-2 py-1">
<div class="pr-2">
<i class="fa fa-exclamation-triangle text-danger" aria-hidden="true" role="img"></i>
Fixed following: - Base font size changed from 0.815rem to 0.875rem, for navbar from 0.875rem to 0.925rem. - Dialog sizes made consistent throughout the application. Now there are 3 size options for width and height each - sm, md, lg. Combination of any of these to be used hereafter - Alignment fix for controls of Node properties dialogs which includes showing text and label in one line without dialog size change, checkbox alignment, switch control alignment at places and other minor improvements in other dialogs - Error message design change in dialogs validation - SQL Editor data grid editor popup design changes which were missed - Design change for dashboard server activity grid - Login page language dropdown color fix - Properties accordion collapse design fix - Help, Info icon fixed across all dialogs which were not working if clicked exactly on the text - Added missing icon with buttons at few places - Shadow behind the dialogs is increased to make it look clearly separated and depth. - Control Alignment fix in maintenance dialog - Min height of alertify dialogs set for better UX - File dialog design fix when no files found - Grant wizard fixes - Scroll bar visibility on first page, use full space for SQL generated on the last page - Browser toolbar buttons changed to sync with SQL editor toolbar buttons - Rounded corners for docker floating dialog (no properties) - Renaming file in file dialog should show original file name - SQL data grid text edit popup buttons behaviour was swapped. This is fixed. - Import/Export dialog changes as per new design.
2019-01-02 03:35:15 -06:00
</div>
<div role="alert" class="alert-text">${msg}</div>
Fixed following: - Base font size changed from 0.815rem to 0.875rem, for navbar from 0.875rem to 0.925rem. - Dialog sizes made consistent throughout the application. Now there are 3 size options for width and height each - sm, md, lg. Combination of any of these to be used hereafter - Alignment fix for controls of Node properties dialogs which includes showing text and label in one line without dialog size change, checkbox alignment, switch control alignment at places and other minor improvements in other dialogs - Error message design change in dialogs validation - SQL Editor data grid editor popup design changes which were missed - Design change for dashboard server activity grid - Login page language dropdown color fix - Properties accordion collapse design fix - Help, Info icon fixed across all dialogs which were not working if clicked exactly on the text - Added missing icon with buttons at few places - Shadow behind the dialogs is increased to make it look clearly separated and depth. - Control Alignment fix in maintenance dialog - Min height of alertify dialogs set for better UX - File dialog design fix when no files found - Grant wizard fixes - Scroll bar visibility on first page, use full space for SQL generated on the last page - Browser toolbar buttons changed to sync with SQL editor toolbar buttons - Rounded corners for docker floating dialog (no properties) - Renaming file in file dialog should show original file name - SQL data grid text edit popup buttons behaviour was swapped. This is fixed. - Import/Export dialog changes as per new design.
2019-01-02 03:35:15 -06:00
<div class="ml-auto close-error-bar">
<a class="close-error fa fa-times text-danger"></a>
</div>
</div>
</div>`;
if (!_.isUndefined(that.statusBar)) {
that.statusBar.html(alertMessage).css('visibility', 'visible');
that.statusBar.find('a.close-error').bind('click', function() {
this.empty().css('visibility', 'hidden');
}.bind(that.statusBar));
}
var sessHasChanged = false;
if(this.sessChanged && this.sessChanged()){
sessHasChanged = true;
}
callback(true, sessHasChanged);
return true;
};
var onSessionValidated = function(sessHasChanged) {
if (!_.isUndefined(that.statusBar)) {
that.statusBar.empty().css('visibility', 'hidden');
}
callback(false, sessHasChanged);
};
callback(false, false);
newModel.on('pgadmin-session:valid', onSessionValidated);
newModel.on('pgadmin-session:invalid', onSessionInvalid);
}
// 'schema' has the information about how to generate the form.
if (_.size(fields)) {
// This will contain the actual view
var view;
if (formType == 'fieldset') {
// It is used to show, edit, create the object in the
// properties tab.
2019-01-03 08:50:24 -06:00
view = new Backform.Accordian({
el: el,
model: newModel,
schema: fields,
});
} else {
// This generates a view to be used by the node dialog
// (for create/edit operation).
view = new Backform.Dialog({
el: el,
model: newModel,
schema: fields,
});
}
var setFocusOnEl = function() {
var container = $(el).find('.tab-content:first > .tab-pane.active:first');
commonUtils.findAndSetFocus(container);
};
if (!newModel.isNew()) {
// This is definetely not in create mode
var msgDiv = '<div role="status" class="pg-panel-message pg-panel-properties-message">' +
gettext('Retrieving data from the server...') + '</div>',
$msgDiv = $(msgDiv);
var timer = setTimeout(function(_ctx) {
// notify user if request is taking longer than 1 second
if (!_.isUndefined(_ctx)) {
$msgDiv.appendTo(_ctx);
}
}, 1000, ctx);
var fetchAjaxHook = function() {
newModel.fetch({
success: function() {
// Clear timeout and remove message
clearTimeout(timer);
$msgDiv.addClass('d-none');
// We got the latest attributes of the object. Render the view
// now.
view.render();
setFocusOnEl();
newModel.startNewSession();
},
error: function(model, xhr, options) {
var _label = that && item ?
that.getTreeNodeHierarchy(
item
)[that.type].label : '';
pgBrowser.Events.trigger(
'pgadmin:node:retrieval:error', 'properties',
xhr, options.textStatus, options.errorThrown, item
);
if (!Alertify.pgHandleItemError(
xhr, options.textStatus, options.errorThrown, {
item: item,
info: info,
}
)) {
Alertify.pgNotifier(
options.textStatus, xhr,
gettext('Error retrieving properties - %s', options.errorThrown || _label),
function(msg) {
if(msg === 'CRYPTKEY_SET') {
fetchAjaxHook();
} else {
console.warn(arguments);
}
}
);
}
// Close the panel (if could not fetch properties)
if (cancelFunc) {
cancelFunc();
}
},
});
};
fetchAjaxHook();
} else {
// Yay - render the view now!
view.render();
setFocusOnEl();
newModel.startNewSession();
}
}
return view;
}
return null;
},
register_node_panel: function() {
var w = pgBrowser.docker,
p = w.findPanels('node_props');
if (p && p.length == 1)
return;
var events = {};
events[wcDocker.EVENT.RESIZE_ENDED] = function() {
var $container = this.$container.find('.obj_properties').first(),
v = $container.data('obj-view');
if (v && v.model && v.model) {
v.model.trigger(
'pg-browser-resized', {
'view': v,
'panel': this,
'container': $container,
});
}
};
p = new pgBrowser.Panel({
name: 'node_props',
showTitle: true,
isCloseable: true,
isPrivate: true,
isLayoutMember: false,
elContainer: true,
content: '<div class="obj_properties container-fluid"><div role="status" class="pg-panel-message">' + gettext('Please wait while we fetch information about the node from the server...') + '</div></div>',
onCreate: function(myPanel, $container) {
$container.addClass('pg-no-overflow');
},
events: events,
});
p.load(pgBrowser.docker);
},
2016-08-22 06:30:16 -05:00
/*
* Default script type menu for node.
*
* Override this, to show more script type menus (e.g hasScriptTypes: ['create', 'select', 'insert', 'update', 'delete'])
*
* Or set it to empty array to disable script type menu on node (e.g hasScriptTypes: [])
*/
hasScriptTypes: ['create'],
/******************************************************************
* This function determines the given item is editable or not.
*
* Override this, when a node is not editable.
*/
canEdit: true,
/******************************************************************
* This function determines the given item is deletable or not.
*
* Override this, when a node is not deletable.
*/
canDrop: false,
/************************************************************************
* This function determines the given item and children are deletable or
* not.
*
* Override this, when a node is not deletable.
*/
canDropCascade: false,
/*********************************************************************************
dropAsRemove should be true in case, Drop object label needs to be replaced by Remove
*/
dropAsRemove: false,
/******************************************************************************
dropPriority is set to 2 by default, override it when change is required
*/
dropPriority: 2,
// List of common callbacks - that can be used for different
// operations!
callbacks: {
/******************************************************************
* This function allows to create/edit/show properties of any
* object depending on the arguments provided.
*
* args must be a object containing:
* action - create/edit/properties
* item - The properties of the item (tree node item)
*
* NOTE:
* if item is not provided, the action will be done on the
* currently selected tree item node.
*
**/
show_obj_properties: function(args, item) {
var t = pgBrowser.tree,
i = (args && args.item) || item || t.selected(),
d = i && i.length == 1 ? t.itemData(i) : undefined,
o = this,
l = o.title.apply(this, [d]);
// Make sure - the properties dialog type registered
pgBrowser.Node.register_node_panel();
// No node selected.
if (!d)
return;
var self = this,
isParent = (_.isArray(this.parent_type) ?
function(_d) {
return (_.indexOf(self.parent_type, _d._type) != -1);
} : function(_d) {
return (self.parent_type == _d._type);
}),
addPanel = function() {
var body = window.document.body,
el = document.createElement('div');
body.insertBefore(el, body.firstChild);
Fixed following: - Base font size changed from 0.815rem to 0.875rem, for navbar from 0.875rem to 0.925rem. - Dialog sizes made consistent throughout the application. Now there are 3 size options for width and height each - sm, md, lg. Combination of any of these to be used hereafter - Alignment fix for controls of Node properties dialogs which includes showing text and label in one line without dialog size change, checkbox alignment, switch control alignment at places and other minor improvements in other dialogs - Error message design change in dialogs validation - SQL Editor data grid editor popup design changes which were missed - Design change for dashboard server activity grid - Login page language dropdown color fix - Properties accordion collapse design fix - Help, Info icon fixed across all dialogs which were not working if clicked exactly on the text - Added missing icon with buttons at few places - Shadow behind the dialogs is increased to make it look clearly separated and depth. - Control Alignment fix in maintenance dialog - Min height of alertify dialogs set for better UX - File dialog design fix when no files found - Grant wizard fixes - Scroll bar visibility on first page, use full space for SQL generated on the last page - Browser toolbar buttons changed to sync with SQL editor toolbar buttons - Rounded corners for docker floating dialog (no properties) - Renaming file in file dialog should show original file name - SQL data grid text edit popup buttons behaviour was swapped. This is fixed. - Import/Export dialog changes as per new design.
2019-01-02 03:35:15 -06:00
let w, h, x, y;
if(screen.width < 800) {
w = pgAdmin.toPx(el, '95%', 'width', true);
Fixed following: - Base font size changed from 0.815rem to 0.875rem, for navbar from 0.875rem to 0.925rem. - Dialog sizes made consistent throughout the application. Now there are 3 size options for width and height each - sm, md, lg. Combination of any of these to be used hereafter - Alignment fix for controls of Node properties dialogs which includes showing text and label in one line without dialog size change, checkbox alignment, switch control alignment at places and other minor improvements in other dialogs - Error message design change in dialogs validation - SQL Editor data grid editor popup design changes which were missed - Design change for dashboard server activity grid - Login page language dropdown color fix - Properties accordion collapse design fix - Help, Info icon fixed across all dialogs which were not working if clicked exactly on the text - Added missing icon with buttons at few places - Shadow behind the dialogs is increased to make it look clearly separated and depth. - Control Alignment fix in maintenance dialog - Min height of alertify dialogs set for better UX - File dialog design fix when no files found - Grant wizard fixes - Scroll bar visibility on first page, use full space for SQL generated on the last page - Browser toolbar buttons changed to sync with SQL editor toolbar buttons - Rounded corners for docker floating dialog (no properties) - Renaming file in file dialog should show original file name - SQL data grid text edit popup buttons behaviour was swapped. This is fixed. - Import/Export dialog changes as per new design.
2019-01-02 03:35:15 -06:00
} else {
w = pgAdmin.toPx(
el, self.width || (pgBrowser.stdW.default + 'px'),
'width', true
);
Fixed following: - Base font size changed from 0.815rem to 0.875rem, for navbar from 0.875rem to 0.925rem. - Dialog sizes made consistent throughout the application. Now there are 3 size options for width and height each - sm, md, lg. Combination of any of these to be used hereafter - Alignment fix for controls of Node properties dialogs which includes showing text and label in one line without dialog size change, checkbox alignment, switch control alignment at places and other minor improvements in other dialogs - Error message design change in dialogs validation - SQL Editor data grid editor popup design changes which were missed - Design change for dashboard server activity grid - Login page language dropdown color fix - Properties accordion collapse design fix - Help, Info icon fixed across all dialogs which were not working if clicked exactly on the text - Added missing icon with buttons at few places - Shadow behind the dialogs is increased to make it look clearly separated and depth. - Control Alignment fix in maintenance dialog - Min height of alertify dialogs set for better UX - File dialog design fix when no files found - Grant wizard fixes - Scroll bar visibility on first page, use full space for SQL generated on the last page - Browser toolbar buttons changed to sync with SQL editor toolbar buttons - Rounded corners for docker floating dialog (no properties) - Renaming file in file dialog should show original file name - SQL data grid text edit popup buttons behaviour was swapped. This is fixed. - Import/Export dialog changes as per new design.
2019-01-02 03:35:15 -06:00
/* Fit to standard sizes */
if(w <= pgBrowser.stdW.sm) {
w = pgBrowser.stdW.sm;
} else {
if(w <= pgBrowser.stdW.md) {
w = pgBrowser.stdW.md;
} else {
w = pgBrowser.stdW.lg;
}
}
}
if(screen.height < 600) {
h = pgAdmin.toPx(el, '95%', 'height', true);
} else {
h = pgAdmin.toPx(
el, self.height || (pgBrowser.stdH.default + 'px'),
'height', true
);
Fixed following: - Base font size changed from 0.815rem to 0.875rem, for navbar from 0.875rem to 0.925rem. - Dialog sizes made consistent throughout the application. Now there are 3 size options for width and height each - sm, md, lg. Combination of any of these to be used hereafter - Alignment fix for controls of Node properties dialogs which includes showing text and label in one line without dialog size change, checkbox alignment, switch control alignment at places and other minor improvements in other dialogs - Error message design change in dialogs validation - SQL Editor data grid editor popup design changes which were missed - Design change for dashboard server activity grid - Login page language dropdown color fix - Properties accordion collapse design fix - Help, Info icon fixed across all dialogs which were not working if clicked exactly on the text - Added missing icon with buttons at few places - Shadow behind the dialogs is increased to make it look clearly separated and depth. - Control Alignment fix in maintenance dialog - Min height of alertify dialogs set for better UX - File dialog design fix when no files found - Grant wizard fixes - Scroll bar visibility on first page, use full space for SQL generated on the last page - Browser toolbar buttons changed to sync with SQL editor toolbar buttons - Rounded corners for docker floating dialog (no properties) - Renaming file in file dialog should show original file name - SQL data grid text edit popup buttons behaviour was swapped. This is fixed. - Import/Export dialog changes as per new design.
2019-01-02 03:35:15 -06:00
/* Fit to standard sizes */
if(h <= pgBrowser.stdH.sm) {
h = pgBrowser.stdH.sm;
} else {
if(h <= pgBrowser.stdH.md) {
h = pgBrowser.stdH.md;
} else {
h = pgBrowser.stdH.lg;
}
}
}
x = (body.offsetWidth - w) / 2;
y = (body.offsetHeight - h) / 4;
// If the screen resolution is higher, but - it is zoomed, dialog
// may be go out of window, and will not be accessible through the
// keyboard.
if (w > window.innerWidth) {
x = 0;
w = window.innerWidth;
}
if (h > window.innerHeight) {
y = 0;
h = window.innerHeight;
}
var new_panel = pgBrowser.docker.addPanel(
'node_props', wcDocker.DOCK.FLOAT, undefined, {
w: w + 'px',
h: h + 'px',
x: x + 'px',
y: y + 'px',
}
);
body.removeChild(el);
return new_panel;
};
if (args.action == 'create') {
// If we've parent, we will get the information of it for
// proper object manipulation.
//
// You know - we're working with RDBMS, relation is everything
// for us.
if (self.parent_type && !isParent(d)) {
// In browser tree, I can be under any node, But - that
// does not mean, it is my parent.
//
// We have some group nodes too.
//
// i.e.
// Tables, Views, etc. nodes under Schema node
//
// And, actual parent of a table is schema, not Tables.
while (i && t.hasParent(i)) {
i = t.parent(i);
var pd = t.itemData(i);
if (isParent(pd)) {
// Assign the data, this is my actual parent.
d = pd;
break;
}
}
}
// Seriously - I really don't have parent data present?
//
// The only node - which I know - who does not have parent
// node, is the Server Group (and, comes directly under root
// node - which has no parent.)
if (!d || (this.parent_type != null && !isParent(d))) {
// It should never come here.
// If it is here, that means - we do have some bug in code.
return;
}
l = gettext('Create - %s', this.label);
p = addPanel();
setTimeout(function() {
o.showProperties(i, d, p, args.action);
}, 10);
} else {
if (pgBrowser.Node.panels && pgBrowser.Node.panels[d.id] &&
pgBrowser.Node.panels[d.id].$container) {
var p = pgBrowser.Node.panels[d.id];
/** TODO ::
* Run in edit mode (if asked) only when it is
* not already been running edit mode
**/
var mode = p.$container.attr('action-mode');
if (mode) {
var msg = gettext('Are you sure want to stop editing the properties of %s "%s"?');
if (args.action == 'edit') {
msg = gettext('Are you sure want to reset the current changes and re-open the panel for %s "%s"?');
}
Alertify.confirm(
gettext('Edit in progress?'),
commonUtils.sprintf(msg, o.label.toLowerCase(), d.label),
function() {
setTimeout(function() {
o.showProperties(i, d, p, args.action);
}, 10);
},
null).show();
} else {
setTimeout(function() {
o.showProperties(i, d, p, args.action);
}, 10);
}
} else {
pgBrowser.Node.panels = pgBrowser.Node.panels || {};
p = pgBrowser.Node.panels[d.id] = addPanel();
setTimeout(function() {
o.showProperties(i, d, p, args.action);
}, 10);
}
}
p.title(l);
p.icon('icon-' + this.type);
// Make sure the properties dialog is visible
p.focus();
},
// Delete the selected object
delete_obj: function(args, item) {
var input = args || {
'url': 'drop',
},
obj = this,
t = pgBrowser.tree,
i = input.item || item || t.selected(),
d = i && i.length == 1 ? t.itemData(i) : undefined;
if (!d)
return;
/*
* Make sure - we're using the correct version of node
*/
obj = pgBrowser.Nodes[d._type];
var objName = d.label;
var msg, title;
if (input.url == 'delete') {
msg = gettext('Are you sure you want to drop %s "%s" and all the objects that depend on it?',
obj.label.toLowerCase(), d.label);
title = gettext('DROP CASCADE %s?', obj.label);
if (!(_.isFunction(obj.canDropCascade) ?
obj.canDropCascade.apply(obj, [d, i]) : obj.canDropCascade)) {
Alertify.error(
gettext('The %s "%s" cannot be dropped.', obj.label, d.label),
10
);
return;
}
} else {
if (obj.dropAsRemove) {
msg = gettext('Are you sure you want to remove %s "%s"?', obj.label.toLowerCase(), d.label);
title = gettext('Remove %s?', obj.label);
} else {
msg = gettext('Are you sure you want to drop %s "%s"?', obj.label.toLowerCase(), d.label);
title = gettext('Drop %s?', obj.label);
}
if (!(_.isFunction(obj.canDrop) ?
obj.canDrop.apply(obj, [d, i]) : obj.canDrop)) {
Alertify.error(
gettext('The %s "%s" cannot be dropped/removed.', obj.label, d.label),
10
);
return;
}
}
Alertify.confirm(title, msg,
function() {
$.ajax({
url: obj.generate_url(i, input.url, d, true),
type: 'DELETE',
})
.done(function(res) {
if (res.success == 0) {
pgBrowser.report_error(res.errormsg, res.info);
} else {
pgBrowser.removeTreeNode(i, true);
}
return true;
})
.fail(function(jqx) {
var errmsg = jqx.responseText;
/* Error from the server */
if (jqx.status == 417 || jqx.status == 410 || jqx.status == 500) {
try {
var data = JSON.parse(jqx.responseText);
errmsg = data.info || data.errormsg;
} catch (e) {
console.warn(e.stack || e);
}
}
pgBrowser.report_error(
gettext('Error dropping/removing %s: "%s"', obj.label, objName), errmsg);
});
},
null
).set('labels', {
ok: gettext('Yes'),
cancel: gettext('No'),
}).show();
},
// Callback for creating script(s) & opening them in Query editor
show_script: function(args, item) {
var scriptType = args.script,
obj,
t = pgBrowser.tree,
i = item || t.selected(),
d = i && i.length == 1 ? t.itemData(i) : undefined;
if (!d)
return;
/*
* Make sure - we're using the correct version of node
*/
obj = pgBrowser.Nodes[d._type];
var sql_url;
// URL for script type
if (scriptType == 'insert') {
sql_url = 'insert_sql';
} else if (scriptType == 'update') {
sql_url = 'update_sql';
} else if (scriptType == 'delete') {
sql_url = 'delete_sql';
} else if (scriptType == 'select') {
sql_url = 'select_sql';
} else if (scriptType == 'exec') {
sql_url = 'exec_sql';
} else {
// By Default get CREATE SQL
sql_url = 'sql';
}
// Open data grid & pass the URL for fetching
pgAdmin.DataGrid.show_query_tool(
obj.generate_url(i, sql_url, d, true),
i, scriptType
);
},
// Callback to render query editor
show_query_tool: function(args, item) {
var t = pgBrowser.tree,
i = item || t.selected(),
d = i && i.length == 1 ? t.itemData(i) : undefined;
if (!d)
return;
// Here call data grid method to render query tool
pgAdmin.DataGrid.show_query_tool('', i);
},
// Logic to change the server background colour
// There is no way of applying CSS to parent element so we have to
// do it via JS code only
change_server_background: function(item, data) {
if (!item || !data)
return;
// Go further only if node type is a Server
if (data._type && data._type == 'server') {
var element = $(item).find('span.aciTreeItem').first() || null,
// First element will be icon and second will be colour code
bgcolor = data.icon.split(' ')[1] || null,
fgcolor = data.icon.split(' ')[2] || '';
if (bgcolor) {
// li tag for the current branch
var first_level_element = (element && element.parents()[3]) || null,
dynamic_class = 'pga_server_' + data._id + '_bgcolor',
style_tag;
// Prepare dynamic style tag
style_tag = '<style id=' + dynamic_class + ' type=\'text/css\'> \n';
style_tag += '.' + dynamic_class + ' .aciTreeItem {';
style_tag += ' border-radius: 3px; margin-bottom: 2px;';
style_tag += ' background: ' + bgcolor + '} \n';
if (fgcolor) {
style_tag += '.' + dynamic_class + ' .aciTreeText {';
style_tag += ' color: ' + fgcolor + ';} \n';
}
style_tag += '</style>';
// Prepare dynamic style tag using template
$('#' + dynamic_class).remove();
$(style_tag).appendTo('head');
if (first_level_element)
$(first_level_element).addClass(dynamic_class);
}
}
},
added: function(item, data, browser) {
var b = browser || pgBrowser,
t = b.tree,
pItem = t.parent(item),
pData = pItem && t.itemData(pItem),
pNode = pData && pgBrowser.Nodes[pData._type];
// Check node is a collection or not.
if (pNode && pNode.is_collection) {
/* If 'collection_count' is not present in data
* it means tree node expanded first time, so we will
* kept collection count and label in data itself.
*/
if (!('collection_count' in pData)) {
pData.collection_count = 0;
}
pData.collection_count++;
t.setLabel(
pItem, {
label: (
_.escape(pData._label) + ' <span>(' + pData.collection_count + ')</span>'
),
}
);
}
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.
selected: function(item, data, browser) {
// Show the information about the selected node in the below panels,
// which are visible at this time:
// + Properties
// + Query (if applicable, otherwise empty)
// + Dependents
// + Dependencies
// + Statistics
var b = browser || pgBrowser,
t = b.tree,
d = data || t.itemData(item);
// Update the menu items
pgAdmin.Browser.enable_disable_menus.apply(b, [item]);
if (d && b) {
if ('properties' in b.panels &&
b.panels['properties'] &&
b.panels['properties'].panel &&
b.panels['properties'].panel.isVisible()) {
// Show object properties (only when the 'properties' tab
// is active).
this.showProperties(item, d, b.panels['properties'].panel);
}
}
Introduced a event manager for the browser, which will generate certain known events, when any activity happens on the browser layout. The following types of events will be triggered through the browser event manager. - pgadmin-browser:frame:* [1] - pgadmin-browser:frame-<name>:* [1] - pgadmin-browser:panel:* [1] - pgadmin-browser:panel-<name>:* [1] - pgadmin-browser:panel - pgadmin-browser:frame - pgadmin-browser:tree - pgadmin-browser:tree:* [2] [1] The '*' denotes some of the events generated by the wcDocker, which can be useful to do some operations. These events are: + wcDocker.EVENT.UPDATED + wcDocker.EVENT.VISIBILITY_CHANGED + wcDocker.EVENT.BEGIN_DOCK + wcDocker.EVENT.END_DOCK + wcDocker.EVENT.GAIN_FOCUS, + wcDocker.EVENT.LOST_FOCUS + wcDocker.EVENT.CLOSED + wcDocker.EVENT.BUTTON + wcDocker.EVENT.ATTACHED + wcDocker.EVENT.DETACHED + wcDocker.EVENT.MOVE_STARTED + wcDocker.EVENT.MOVE_ENDED + wcDocker.EVENT.MOVED + wcDocker.EVENT.RESIZE_STARTED + wcDocker.EVENT.RESIZE_ENDED + wcDocker.EVENT.RESIZED + wcDocker.EVENT.SCROLLED [2] The '*' denotes all the events generated by the Browser Tree (aciTree). The extension modules can utilize these events to do some operations on nodes, and panels. This patch includes showing 'Reversed Engineered Query' for the selected node (if allowed) using the above approch. The ShowNodeSQL module looks for two events. 1. SQL Panel Visibility change. - Based on the visibility of that panel, we start/stop listening the node selection event. 2. Node Selection in Browser tree - Based on the selected node type, it will look for 'hasSQL' parameter of the node, and fetch the Query from the server, and show it in the SQL editor.
2016-01-19 06:31:14 -06:00
pgBrowser.Events.trigger('pgadmin:browser:tree:update-tree-state',
item);
Introduced a event manager for the browser, which will generate certain known events, when any activity happens on the browser layout. The following types of events will be triggered through the browser event manager. - pgadmin-browser:frame:* [1] - pgadmin-browser:frame-<name>:* [1] - pgadmin-browser:panel:* [1] - pgadmin-browser:panel-<name>:* [1] - pgadmin-browser:panel - pgadmin-browser:frame - pgadmin-browser:tree - pgadmin-browser:tree:* [2] [1] The '*' denotes some of the events generated by the wcDocker, which can be useful to do some operations. These events are: + wcDocker.EVENT.UPDATED + wcDocker.EVENT.VISIBILITY_CHANGED + wcDocker.EVENT.BEGIN_DOCK + wcDocker.EVENT.END_DOCK + wcDocker.EVENT.GAIN_FOCUS, + wcDocker.EVENT.LOST_FOCUS + wcDocker.EVENT.CLOSED + wcDocker.EVENT.BUTTON + wcDocker.EVENT.ATTACHED + wcDocker.EVENT.DETACHED + wcDocker.EVENT.MOVE_STARTED + wcDocker.EVENT.MOVE_ENDED + wcDocker.EVENT.MOVED + wcDocker.EVENT.RESIZE_STARTED + wcDocker.EVENT.RESIZE_ENDED + wcDocker.EVENT.RESIZED + wcDocker.EVENT.SCROLLED [2] The '*' denotes all the events generated by the Browser Tree (aciTree). The extension modules can utilize these events to do some operations on nodes, and panels. This patch includes showing 'Reversed Engineered Query' for the selected node (if allowed) using the above approch. The ShowNodeSQL module looks for two events. 1. SQL Panel Visibility change. - Based on the visibility of that panel, we start/stop listening the node selection event. 2. Node Selection in Browser tree - Based on the selected node type, it will look for 'hasSQL' parameter of the node, and fetch the Query from the server, and show it in the SQL editor.
2016-01-19 06:31:14 -06:00
return true;
},
removed: function(item) {
var self = this,
t = pgBrowser.tree,
pItem = t.parent(item),
pData = pItem && t.itemData(pItem),
pNode = pData && pgBrowser.Nodes[pData._type];
// Check node is a collection or not.
if (
pNode && pNode.is_collection && 'collection_count' in pData
) {
pData.collection_count--;
t.setLabel(
pItem, {
label: (
_.escape(pData._label) + ' <span>(' + pData.collection_count + ')</span>'
),
}
);
}
setTimeout(function() {
self.clear_cache.apply(self, item);
}, 0);
},
unloaded: function(item) {
var self = this,
t = pgBrowser.tree,
data = item && t.itemData(item);
// In case of unload remove the collection counter
if (self.is_collection && data === Object(data) &&'collection_count' in data) {
delete data.collection_count;
t.setLabel(item, {
label: _.escape(data._label),
});
}
},
refresh: function(cmd, _item) {
var self = this,
t = pgBrowser.tree,
data = _item && t.itemData(_item);
$(pgBrowser.panels['properties'].panel).removeData('node-prop');
pgBrowser.Events.trigger(
'pgadmin:browser:tree:refresh', _item || pgBrowser.tree.selected(), {
success: function() {
self.callbacks.selected.apply(self, [_item, data, pgBrowser]);
},
});
},
opened: function(item) {
let tree = pgBrowser.tree,
auto_expand = pgBrowser.get_preference('browser', 'auto_expand_sole_children');
pgBrowser.Events.trigger('pgadmin:browser:tree:update-tree-state',
item);
if (auto_expand && auto_expand.value == true && tree.children(item).length == 1) {
// Automatically expand the child node, if a treeview node has only a single child.
tree.open(tree.first(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
* element.
*
* This has been used for the showing, editing properties of the node.
* This has also been used for creating a node.
**/
showProperties: function(item, data, panel, action) {
var that = this,
tree = pgAdmin.Browser.tree,
j = panel.$container.find('.obj_properties').first(),
view = j.data('obj-view'),
content = $('<div></div>')
.addClass('pg-prop-content col-12'),
confirm_close = true;
// Handle key press events for Cancel, save and help button
var handleKeyDown = function(event, context) {
// If called on panel other than node_props, return
if (panel && panel['_type'] !== 'node_props') return;
switch (event.which) {
case keyCode.ESCAPE:
closePanel(true);
break;
case keyCode.ENTER:
// Return if event is fired from child element
if (event.target !== context) return;
if (view && view.model && view.model.sessChanged()) {
var btn = $(event.target).closest('.obj_properties')
.find('.pg-prop-btn-group')
.find('button.btn-primary');
onSave.call(this, view, btn);
}
break;
case keyCode.F1:
onDialogHelp();
break;
default:
break;
}
}.bind(panel);
setTimeout(function() {
// Register key press events with panel element
panel.$container.find('.backform-tab').on('keydown', function(event) {
handleKeyDown(event, this);
});
}, 200); // wait for panel tab to render
// Template function to create the status bar
var createStatusBar = function(location) {
var statusBar = $('<div role="status"></div>').addClass(
'pg-prop-status-bar'
).appendTo(j);
statusBar.css('visibility', 'hidden');
if (location == 'header') {
statusBar.appendTo(that.header);
} else {
statusBar.prependTo(that.footer);
}
that.statusBar = statusBar;
return statusBar;
}.bind(panel),
// Template function to create the button-group
createButtons = function(buttons, location, extraClasses) {
// Arguments must be non-zero length array of type
// object, which contains following attributes:
// label, type, extraClasses, register
if (buttons && _.isArray(buttons) && buttons.length > 0) {
// All buttons will be created within a single
// div area.
var btnGroup =
$('<div class="pg-prop-btn-group"></div>'),
// Template used for creating a button
tmpl = _.template([
'<button tabindex="0" type="<%= type %>" ',
'class="btn <%=extraClasses.join(\' \')%>"',
'<% if (disabled) { %> disabled="disabled"<% } %> title="<%-tooltip%>"',
'<% if (label != "") {} else { %> aria-label="<%-tooltip%>"<% } %> >',
'<span class="<%= icon %>"></span><% if (label != "") { %>&nbsp;<%-label%><% } %></button>',
].join(' '));
if (location == 'header') {
btnGroup.appendTo(that.header);
} else {
btnGroup.appendTo(that.footer);
}
if (extraClasses) {
btnGroup.addClass(extraClasses);
}
_.each(buttons, function(btn) {
// Create the actual button, and append to
// the group div
// icon may not present for this button
if (!btn.icon) {
btn.icon = '';
}
var b = $(tmpl(btn));
btnGroup.append(b);
// Register is a callback to set callback
// for certain operation for this button.
btn.register(b);
});
return btnGroup;
}
return null;
}.bind(panel),
// Callback to show object properties
properties = function() {
// Avoid unnecessary reloads
var i = tree.selected(),
d = i && tree.itemData(i),
n = i && d && pgBrowser.Nodes[d._type],
treeHierarchy = n.getTreeNodeHierarchy(i);
if (_.isEqual($(this).data('node-prop'), treeHierarchy)) {
return;
}
// Cache the current IDs for next time
$(this).data('node-prop', treeHierarchy);
if (!content.hasClass('has-pg-prop-btn-group'))
content.addClass('has-pg-prop-btn-group');
// We need to release any existing view, before
// creating new view.
if (view) {
// Release the view
view.remove({
data: true,
internal: true,
silent: true,
});
// Deallocate the view
// delete view;
view = null;
// Reset the data object
j.data('obj-view', null);
}
// Make sure the HTML element is empty.
j.empty();
that.header = $('<div></div>').addClass(
'pg-prop-header'
).appendTo(j);
that.footer = $('<div></div>').addClass(
'pg-prop-footer'
).appendTo(j);
// Create a view to show the properties in fieldsets
view = that.getView(item, 'properties', content, data, 'fieldset', undefined, j);
if (view) {
// Save it for release it later
j.data('obj-view', view);
// Create proper buttons
var buttons = [];
2016-08-22 06:30:16 -05:00
buttons.push({
label: gettext('Edit'),
type: 'edit',
tooltip: gettext('Edit'),
extraClasses: ['btn', 'btn-primary', 'pull-right', 'm-1'],
icon: 'fa fa-sm fa-pencil-alt',
disabled: _.isFunction(that.canEdit) ? !that.canEdit.apply(that, [d, i]) : !that.canEdit,
register: function(btn) {
btn.on('click',() => {
onEdit();
});
},
});
2016-08-22 06:30:16 -05:00
2016-04-12 07:35:47 -05:00
buttons.push({
label: '',
type: 'help',
tooltip: gettext('SQL help for this object type.'),
extraClasses: ['btn-primary-icon', 'btn-primary-icon', 'm-1'],
icon: 'fa fa-info',
2016-04-12 07:35:47 -05:00
disabled: (that.sqlAlterHelp == '' && that.sqlCreateHelp == '') ? true : false,
register: function(btn) {
btn.on('click',() => {
2016-04-12 07:35:47 -05:00
onSqlHelp();
});
},
2016-04-12 07:35:47 -05:00
});
createButtons(buttons, 'header', 'pg-prop-btn-group-above');
}
j.append(content);
}.bind(panel),
2016-04-12 07:35:47 -05:00
onSqlHelp = function() {
// Construct the URL
var server = that.getTreeNodeHierarchy(item).server;
2016-04-12 07:35:47 -05:00
var url = pgBrowser.utils.pg_help_path;
2016-04-12 07:35:47 -05:00
if (server.server_type == 'ppas') {
url = pgBrowser.utils.edbas_help_path;
2016-04-12 07:35:47 -05:00
}
var fullUrl = '';
2016-04-12 07:35:47 -05:00
if (that.sqlCreateHelp == '' && that.sqlAlterHelp != '') {
fullUrl = help.getHelpUrl(url, that.sqlAlterHelp, server.version, server.server_type);
2016-04-12 07:35:47 -05:00
} else if (that.sqlCreateHelp != '' && that.sqlAlterHelp == '') {
fullUrl = help.getHelpUrl(url, that.sqlCreateHelp, server.version, server.server_type);
2016-04-12 07:35:47 -05:00
} else {
if (view.model.isNew()) {
fullUrl = help.getHelpUrl(url, that.sqlCreateHelp, server.version, server.server_type);
2016-04-12 07:35:47 -05:00
} else {
fullUrl = help.getHelpUrl(url, that.sqlAlterHelp, server.version, server.server_type);
2016-04-12 07:35:47 -05:00
}
}
window.open(fullUrl, 'postgres_help');
}.bind(panel),
2016-05-16 10:25:51 -05:00
onDialogHelp = function() {
window.open(that.dialogHelp, 'pgadmin_help');
2016-05-16 10:25:51 -05:00
}.bind(panel),
warnBeforeChangesLost = function(warn_text, yes_callback) {
var $props = this.$container.find('.obj_properties').first(),
objview = $props && $props.data('obj-view'),
self = this;
let confirm_on_properties_close = pgBrowser.get_preferences_for_module('browser').confirm_on_properties_close;
if (confirm_on_properties_close && confirm_close && objview && objview.model) {
if(objview.model.sessChanged()){
Alertify.confirm(
gettext('Warning'),
warn_text,
function() {
setTimeout(function(){
yes_callback();
}.bind(self), 50);
return true;
},
function() {
return true;
}
).set('labels', {
ok: gettext('Yes'),
cancel: gettext('No'),
}).show();
} else {
return true;
}
} else {
yes_callback();
return true;
}
}.bind(panel),
warnBeforeAttributeChange = function(yes_callback) {
var $props = this.$container.find('.obj_properties').first(),
objview = $props && $props.data('obj-view'),
self = this;
if (objview && objview.model && !_.isUndefined(objview.model.warn_text) && !_.isNull(objview.model.warn_text)) {
let warn_text;
warn_text = gettext(objview.model.warn_text);
if(objview.model.sessChanged()){
Alertify.confirm(
gettext('Warning'),
warn_text,
function() {
setTimeout(function(){
yes_callback();
}.bind(self), 50);
return true;
},
function() {
return true;
}
).set('labels', {
ok: gettext('Yes'),
cancel: gettext('No'),
}).show();
} else {
return true;
}
} else {
yes_callback();
return true;
}
}.bind(panel),
informBeforeAttributeChange = function(ok_callback) {
var $props = this.$container.find('.obj_properties').first(),
objview = $props && $props.data('obj-view');
if (objview && objview.model && !_.isUndefined(objview.model.inform_text) && !_.isNull(objview.model.inform_text)) {
Alertify.alert(
gettext('Warning'),
gettext(objview.model.inform_text)
);
}
ok_callback();
return true;
}.bind(panel),
onSave = function(_view, saveBtn) {
var m = _view.model,
d = m.toJSON(true),
// Generate a timer for the request
timer = setTimeout(function() {
$('.obj_properties').addClass('show_progress');
}, 1000);
// Prevent subsequent save operation by disabling Save button
if (saveBtn)
$(saveBtn).prop('disabled', true);
if (d && !_.isEmpty(d)) {
m.save({}, {
attrs: d,
validate: false,
cache: false,
wait: true,
success: function() {
onSaveFunc.call();
// Hide progress cursor
$('.obj_properties').removeClass('show_progress');
clearTimeout(timer);
// Removing the node-prop property of panel
// so that we show updated data on panel
var pnlProperties = pgBrowser.docker.findPanels('properties')[0],
pnlSql = pgBrowser.docker.findPanels('sql')[0],
pnlStats = pgBrowser.docker.findPanels('statistics')[0],
pnlDependencies = pgBrowser.docker.findPanels('dependencies')[0],
pnlDependents = pgBrowser.docker.findPanels('dependents')[0];
if (pnlProperties)
$(pnlProperties).removeData('node-prop');
if (pnlSql)
$(pnlSql).removeData('node-prop');
if (pnlStats)
$(pnlStats).removeData('node-prop');
if (pnlDependencies)
$(pnlDependencies).removeData('node-prop');
if (pnlDependents)
$(pnlDependents).removeData('node-prop');
},
error: function(_m, jqxhr) {
Alertify.pgNotifier(
'error', jqxhr,
gettext('Error saving properties')
);
// Hide progress cursor
$('.obj_properties').removeClass('show_progress');
clearTimeout(timer);
if (saveBtn)
$(saveBtn).prop('disabled', false);
},
});
}
}.bind(panel),
editFunc = function() {
var self = this;
if (action && action == 'properties') {
action = 'edit';
}
self.$container.attr('action-mode', action);
// We need to release any existing view, before
// creating the new view.
if (view) {
// Release the view
view.remove({
data: true,
internal: true,
silent: true,
});
// Deallocate the view
view = null;
// Reset the data object
j.data('obj-view', null);
}
// Make sure the HTML element is empty.
j.empty();
that.header = $('<div></div>').addClass(
'pg-prop-header'
).appendTo(j);
that.footer = $('<div></div>').addClass(
'pg-prop-footer'
).appendTo(j);
var updateButtons = function(hasError, modified) {
var btnGroup = this.find('.pg-prop-btn-group'),
btnSave = btnGroup.find('button.btn-primary'),
Fixed following: - Base font size changed from 0.815rem to 0.875rem, for navbar from 0.875rem to 0.925rem. - Dialog sizes made consistent throughout the application. Now there are 3 size options for width and height each - sm, md, lg. Combination of any of these to be used hereafter - Alignment fix for controls of Node properties dialogs which includes showing text and label in one line without dialog size change, checkbox alignment, switch control alignment at places and other minor improvements in other dialogs - Error message design change in dialogs validation - SQL Editor data grid editor popup design changes which were missed - Design change for dashboard server activity grid - Login page language dropdown color fix - Properties accordion collapse design fix - Help, Info icon fixed across all dialogs which were not working if clicked exactly on the text - Added missing icon with buttons at few places - Shadow behind the dialogs is increased to make it look clearly separated and depth. - Control Alignment fix in maintenance dialog - Min height of alertify dialogs set for better UX - File dialog design fix when no files found - Grant wizard fixes - Scroll bar visibility on first page, use full space for SQL generated on the last page - Browser toolbar buttons changed to sync with SQL editor toolbar buttons - Rounded corners for docker floating dialog (no properties) - Renaming file in file dialog should show original file name - SQL data grid text edit popup buttons behaviour was swapped. This is fixed. - Import/Export dialog changes as per new design.
2019-01-02 03:35:15 -06:00
btnReset = btnGroup.find('button.btn-secondary[type="reset"]');
if (hasError || !modified) {
btnSave.prop('disabled', true);
btnSave.attr('disabled', 'disabled');
} else {
btnSave.prop('disabled', false);
btnSave.removeAttr('disabled');
}
if (!modified) {
btnReset.prop('disabled', true);
btnReset.attr('disabled', 'disabled');
} else {
btnReset.prop('disabled', false);
btnReset.removeAttr('disabled');
}
};
// Create a view to edit/create the properties in fieldsets
view = that.getView(item, action, content, data, 'dialog', updateButtons, j, onCancelFunc);
if (view) {
// Save it to release it later
j.data('obj-view', view);
self.icon(
_.isFunction(that['node_image']) ?
(that['node_image']).apply(that, [data, view.model]) :
(that['node_image'] || ('icon-' + that.type))
);
// Create proper buttons
let btn_grp = createButtons([{
label: '',
type: 'help',
tooltip: gettext('SQL help for this object type.'),
extraClasses: ['btn-primary-icon', 'pull-left', 'mx-1'],
icon: 'fa fa-info',
2016-05-16 10:25:51 -05:00
disabled: (that.sqlAlterHelp == '' && that.sqlCreateHelp == '') ? true : false,
2016-04-12 07:35:47 -05:00
register: function(btn) {
btn.on('click',() => {
2016-04-12 07:35:47 -05:00
onSqlHelp();
});
},
}, {
label: '',
type: 'help',
tooltip: gettext('Help for this dialog.'),
extraClasses: ['btn-primary-icon', 'pull-left', 'mx-1'],
icon: 'fa fa-question',
2016-05-16 10:25:51 -05:00
disabled: (that.dialogHelp == '') ? true : false,
register: function(btn) {
btn.on('click',() => {
2016-05-16 10:25:51 -05:00
onDialogHelp();
});
},
}, {
label: gettext('Cancel'),
type: 'cancel',
tooltip: gettext('Cancel changes to this object.'),
extraClasses: ['btn-secondary', 'mx-1'],
icon: 'fa fa-times pg-alertify-button',
disabled: false,
register: function(btn) {
btn.on('click',() => {
// Removing the action-mode
self.$container.removeAttr('action-mode');
onCancelFunc.call(true);
});
},
}, {
label: gettext('Reset'),
type: 'reset',
tooltip: gettext('Reset the fields on this dialog.'),
extraClasses: ['btn-secondary', 'mx-1'],
icon: 'fa fa-recycle pg-alertify-button',
disabled: true,
register: function(btn) {
btn.on('click',() => {
warnBeforeChangesLost.call(
self,
gettext('Changes will be lost. Are you sure you want to reset?'),
function() {
setTimeout(function() {
editFunc.call();
}, 0);
}
);
});
},
}, {
label: gettext('Save'),
type: 'save',
tooltip: gettext('Save this object.'),
extraClasses: ['btn-primary', 'mx-1'],
icon: 'fa fa-save pg-alertify-button',
disabled: true,
register: function(btn) {
// Save the changes
btn.on('click',() => {
warnBeforeAttributeChange.call(
self,
function() {
informBeforeAttributeChange.call(self, function(){
setTimeout(function() {
onSave.call(this, view, btn);
}, 0);
});
}
);
});
},
}], 'footer', 'pg-prop-btn-group-below');
btn_grp.on('keydown', 'button', function(event) {
if (!event.shiftKey && event.keyCode == 9 && $(this).nextAll('button:not([disabled])').length == 0) {
// set focus back to first focusable element on dialog
view.$el.closest('.wcFloating').find('[tabindex]:not([tabindex="-1"]').first().focus();
return false;
}
let btnGroup = $(self.$container.find('.pg-prop-btn-group'));
let el = $(btnGroup).find('button:first');
if (self.$container.find('.number-cell.editable:last').is(':visible')){
if (event.keyCode === 9 && event.shiftKey) {
if ($(el).is($(event.target))){
$(self.$container.find('td.editable:last').trigger('click'));
event.preventDefault();
event.stopPropagation();
}
}
}
});
setTimeout(function() {
pgBrowser.keyboardNavigation.getDialogTabNavigator(self.pgElContainer);
}, 200);
}
// Create status bar.
createStatusBar('footer');
// Add some space, so that - button group does not override the
// space
content.addClass('pg-prop-has-btn-group-below');
// Show contents before buttons
j.prepend(content);
// add required attributes to select2 input to resolve accessibility issue.
$('.select2-search__field').attr('aria-label', 'select2');
view.$el.closest('.wcFloating').find('.wcFrameButtonBar > .wcFrameButton[style!="display: none;"]').on('keydown', function(e) {
if(e.shiftKey && e.keyCode === 9) {
e.stopPropagation();
setTimeout(() => {
view.$el.closest('.wcFloating').find('[tabindex]:not([tabindex="-1"]):not([disabled])').last().focus();
}, 10);
}
});
}.bind(panel),
closePanel = function(confirm_close_flag) {
if(!_.isUndefined(confirm_close_flag)) {
confirm_close = confirm_close_flag;
}
// Closing this panel
2016-04-15 08:02:21 -05:00
this.close();
}.bind(panel),
updateTreeItem = function(obj) {
var _old = data,
_new = _.clone(view.model.tnode),
info = _.clone(view.model.node_info);
// Clear the cache for this node now.
setTimeout(function() {
obj.clear_cache.apply(obj, item);
}, 0);
pgBrowser.Events.trigger(
'pgadmin:browser:tree:update',
_old, _new, info, {
success: function(_item, _newNodeData, _oldNodeData) {
pgBrowser.Events.trigger(
'pgadmin:browser:node:updated', _item, _newNodeData,
_oldNodeData
);
pgBrowser.Events.trigger(
'pgadmin:browser:node:' + _newNodeData._type + ':updated',
_item, _newNodeData, _oldNodeData
);
},
}
);
closePanel(false);
},
saveNewNode = function(obj) {
var $props = this.$container.find('.obj_properties').first(),
objview = $props.data('obj-view');
// Clear the cache for this node now.
setTimeout(function() {
obj.clear_cache.apply(obj, item);
}, 0);
try {
pgBrowser.Events.trigger(
'pgadmin:browser:tree:add', _.clone(objview.model.tnode),
_.clone(objview.model.node_info)
);
} catch (e) {
console.warn(e.stack || e);
}
closePanel(false);
}.bind(panel, that),
editInNewPanel = function() {
// Open edit in separate panel
setTimeout(function() {
that.callbacks.show_obj_properties.apply(that, [{
'action': 'edit',
'item': item,
}]);
}, 0);
},
2016-04-15 08:02:21 -05:00
onCancelFunc = closePanel,
onSaveFunc = updateTreeItem.bind(panel, that),
onEdit = editFunc.bind(panel);
if (action) {
if (action == 'create') {
2016-04-15 08:02:21 -05:00
onSaveFunc = saveNewNode;
}
if (action != 'properties') {
// We need to keep track edit/create mode for this panel.
editFunc();
} else {
properties();
}
} else {
/* Show properties */
properties();
onEdit = editInNewPanel.bind(panel);
}
if (panel.closeable()) {
panel.on(wcDocker.EVENT.CLOSING, warnBeforeChangesLost.bind(
panel,
gettext('Changes will be lost. Are you sure you want to close the dialog?'),
function() {
panel.off(wcDocker.EVENT.CLOSING);
panel.close();
}
));
var onCloseFunc = function() {
var $props = this.$container.find('.obj_properties').first(),
objview = $props && $props.data('obj-view');
if (objview) {
objview.remove({
data: true,
internal: true,
silent: true,
});
}
}.bind(panel);
panel.on(wcDocker.EVENT.CLOSED, onCloseFunc);
}
},
_find_parent_node: function(t, i, d) {
if (this.parent_type) {
d = d || t.itemData(i);
if (_.isString(this.parent_type)) {
if (this.parent_type == d._type) {
return i;
}
while (t.hasParent(i)) {
i = t.parent(i);
d = t.itemData(i);
if (this.parent_type == d._type)
return i;
}
} else {
if (_.indexOf(this.parent_type, d._type) >= 0) {
return i;
}
while (t.hasParent(i)) {
i = t.parent(i);
d = t.itemData(i);
if (_.indexOf(this.parent_type, d._type) >= 0)
return i;
}
}
}
return null;
},
/**********************************************************************
* Generate the URL for different operations
*
* arguments:
* type: Create/drop/edit/properties/sql/depends/statistics
* d: Provide the ItemData for the current item node
* with_id: Required id information at the end?
* jump_after_node: This will skip all the value between jump_after_node
* to the last node, excluding jump_after_node and the last node. This is particularly
* helpful in partition table where we need to skip parent table OID of a partitioned
* table in URL formation. Partitioned table itself is a "table" and can be multilevel
* Supports url generation for create, drop, edit, properties, sql,
* depends, statistics
*/
generate_url: function(item, type, d, with_id, info, jump_after_node) {
var opURL = {
'create': 'obj',
'drop': 'obj',
'edit': 'obj',
'properties': 'obj',
'statistics': 'stats',
},
self = this,
priority = -Infinity;
var treeInfo = (_.isUndefined(item) || _.isNull(item)) ?
info || {} : this.getTreeNodeHierarchy(item);
var actionType = type in opURL ? opURL[type] : type;
var itemID = with_id && d._type == self.type ? encodeURIComponent(d._id) : '';
if (self.parent_type) {
if (_.isString(self.parent_type)) {
let p = treeInfo[self.parent_type];
if (p) {
priority = p.priority;
}
} else {
_.each(self.parent_type, function(o) {
let p = treeInfo[o];
if (p) {
if (priority < p.priority) {
priority = p.priority;
}
}
});
}
}
let jump_after_priority = priority;
if(jump_after_node && treeInfo[jump_after_node]) {
jump_after_priority = treeInfo[jump_after_node].priority;
}
var nodePickFunction = function(treeInfoValue) {
return (treeInfoValue.priority <= jump_after_priority || treeInfoValue.priority == priority);
};
return generateUrl.generate_url(pgBrowser.URL, treeInfo, actionType, self.type, nodePickFunction, itemID);
},
// Base class for Node Data Collection
Collection: pgBrowser.DataCollection,
// Base class for Node Data Model
Model: pgBrowser.DataModel,
getTreeNodeHierarchy: pgadminTreeNode.getTreeNodeHierarchyFromIdentifier.bind(pgBrowser),
cache: function(url, node_info, level, data) {
var cached = this.cached = this.cached || {},
hash = url,
min_priority = (
node_info && node_info[level] && node_info[level].priority
) || 0;
if (node_info) {
_.each(_.sortBy(_.values(_.pick(
node_info,
function(v) {
return (v.priority <= min_priority);
}
)), function(o) {
return o.priority;
}), function(o) {
hash = commonUtils.sprintf('%s/%s', hash, encodeURI(o._id));
});
}
if (_.isUndefined(data)) {
var res = cached[hash];
if (!_.isUndefined(res) &&
(res.at - Date.now() > 300000)) {
res = undefined;
}
return res;
}
res = cached[hash] = {
data: data,
at: Date.now(),
level: level,
};
return res;
},
clear_cache: function(item) {
/*
* Reset the cache, when new node is created.
*
* FIXME:
* At the moment, we will clear all the cache for this node. But - we
* would like to clear the cache only this nodes parent, so that - it
* fetches the new data.
*/
this.cached = {};
// Trigger Notify event about node's cache
var self = this;
pgBrowser.Events.trigger(
'pgadmin:browser:node:' + self.type + ':cache_cleared',
item, self
);
},
cache_level: function(node_info, with_id) {
if (node_info) {
if (with_id && this.type in node_info) {
return this.type;
}
if (_.isArray(this.parent_type)) {
for (var parent in this.parent_type) {
if (parent in node_info) {
return parent;
}
}
return this.type;
}
return this.parent_type;
}
},
});
return pgAdmin.Browser.Node;
});