Compare commits

..
10 Commits
56 changed files with 3834 additions and 2826 deletions

No files matched your search

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 53 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 168 KiB

+3
View File
@@ -25,6 +25,9 @@ Use the *File* menu to access the following options:
+-------------------------+---------------------------------------------------------------------------------------------------------+
| *Reset Layout* | If you have modified the workspace, click to restore the default layout. |
+-------------------------+---------------------------------------------------------------------------------------------------------+
| *Lock Layout* | Click to open a submenu to select the level for locking the UI layout |
| | This can also be changed from browser display :ref:`preferences <preferences>` |
+-------------------------+---------------------------------------------------------------------------------------------------------+
The Object Menu
***************
+17 -4
View File
@@ -38,10 +38,6 @@ Use the fields on the *Display* panel to specify general display preferences:
attempt to catch browser close or refresh events and prompt before allowing
them to continue.
* When the *Show system objects?* switch is set to *True*, the client will
display system objects such as system schemas (for example, *pg_temp*) or
system columns (for example, *xmin* or *ctid*) in the tree control.
* When the *Enable browser tree animation?* switch is set to *True*, the client
will display the animated tree control otherwise it will be unanimated.
@@ -49,6 +45,23 @@ Use the fields on the *Display* panel to specify general display preferences:
the client will display the animated dialogues/notifications otherwise it
will be unanimated.
* Use the *Lock layout* field to lock the UI layout at different levels. This
can also be changed from File menu on the :ref:`menu bar <menu_bar>`
+---------------------+-------------------------------------------------------------------+
| Option | Action |
+=====================+===================================================================+
| *None* | No locking. Every panel is resizable and dockable. |
+---------------------+-------------------------------------------------------------------+
| *Prevent docking* | This will disable the docking/undocking of the panels |
+---------------------+-------------------------------------------------------------------+
| *Full* | This will disable resizing, docking/undocking of the panels |
+---------------------+-------------------------------------------------------------------+
* When the *Show system objects?* switch is set to *True*, the client will
display system objects such as system schemas (for example, *pg_temp*) or
system columns (for example, *xmin* or *ctid*) in the tree control.
Use the fields on the *Keyboard shortcuts* panel to configure shortcuts for the
main window navigation:
+1
View File
@@ -11,6 +11,7 @@ notes for it.
.. toctree::
:maxdepth: 1
release_notes_4_8
release_notes_4_7
release_notes_4_6
release_notes_4_5
+19
View File
@@ -0,0 +1,19 @@
***********
Version 4.8
***********
Release date: 2019-06-03
This release contains a number of bug fixes and new features since the release of pgAdmin4 4.7.
New features
************
| `Feature #2653 <https://redmine.postgresql.org/issues/2653>`_ - Allow the UI layout to be fully locked or to prevent docking changes.
Bug fixes
*********
| `Bug #4169 <https://redmine.postgresql.org/issues/4169>`_ - Omit the geometry viewer in the Query Tool from layout saving.
| `Bug #4307 <https://redmine.postgresql.org/issues/4307>`_ - Improve the performance of explain plan by embedding the images only when downloading it.
| `Bug #4308 <https://redmine.postgresql.org/issues/4308>`_ - Fix the issue of accessing the SQL for Views and Materialized Views. Regression of pluralisation of folder names.
+2 -2
View File
@@ -8,9 +8,9 @@
<key>CFBundleName</key>
<string>pgAdmin 4</string>
<key>CFBundleShortVersionString</key>
<string>4.7.0</string>
<string>4.8.0</string>
<key>CFBundleVersion</key>
<string>4.7.0</string>
<string>4.8.0</string>
<key>LSMinimumSystemVersion</key>
<string>10.10</string>
<key>NSHumanReadableCopyright</key>
+1 -1
View File
@@ -1,4 +1,4 @@
VERSION = 4.7.0.0
VERSION = 4.8.0.0
QMAKE_TARGET_COMPANY = "The pgAdmin Development Team"
QMAKE_TARGET_PRODUCT = "pgAdmin 4"
QMAKE_TARGET_DESCRIPTION = "pgAdmin 4 Desktop Runtime"
+2 -2
View File
@@ -50,7 +50,7 @@ APP_ICON = 'pg-icon'
# Application version number components
APP_RELEASE = 4
APP_REVISION = 7
APP_REVISION = 8
# Application version suffix, e.g. 'beta1', 'dev'. Usually an empty string
# for GA releases.
@@ -59,7 +59,7 @@ APP_SUFFIX = ''
# Numeric application version for upgrade checks. Should be in the format:
# [X]XYYZZ, where X is the release version, Y is the revision, with a leading
# zero if needed, and Z represents the suffix, with a leading zero if needed
APP_VERSION_INT = 40700
APP_VERSION_INT = 40800
# DO NOT CHANGE!
# The application version string, constructed from the components
+1 -1
View File
@@ -89,7 +89,7 @@
"underscore": "^1.9.1",
"underscore.string": "^3.3.5",
"watchify": "~3.11.1",
"webcabin-docker": "git+https://github.com/EnterpriseDB/wcDocker/#e191a1063c111d5542ff26a7c163940be180333d",
"webcabin-docker": "git+https://github.com/EnterpriseDB/wcDocker/#f3c79e4b9a9e76a8e34736fc33473e65c524050c",
"wkx": "^0.4.6"
},
"scripts": {
+1
View File
@@ -236,6 +236,7 @@ def create_app(app_name=None):
os.environ['PGADMIN_TESTING_MODE'] == '1'
):
config.SQLITE_PATH = config.TEST_SQLITE_PATH
config.MASTER_PASSWORD_REQUIRED = False
# Ensure the various working directories exist
from pgadmin.setup import create_app_data_directory, db_upgrade
+52 -1
View File
@@ -39,6 +39,7 @@ from pgadmin.utils import PgAdminModule
from pgadmin.utils.ajax import make_json_response
from pgadmin.utils.csrf import pgCSRFProtect
from pgadmin.utils.preferences import Preferences
from pgadmin.utils.menu import MenuItem
from pgadmin.browser.register_browser_preferences import \
register_browser_preferences
from pgadmin.utils.master_password import validate_master_password, \
@@ -215,6 +216,40 @@ class BrowserModule(PgAdminModule):
scripts.extend(module.get_own_javascripts())
return scripts
def get_own_menuitems(self):
return {
'file_items': [
MenuItem(
name='mnu_locklayout',
module='pgAdmin.Browser',
label=gettext('Lock Layout'),
priority=999,
menu_items=[MenuItem(
name='mnu_lock_none',
module='pgAdmin.Browser',
callback='mnu_lock_none',
priority=0,
label=gettext('None'),
checked=True
), MenuItem(
name='mnu_lock_docking',
module='pgAdmin.Browser',
callback='mnu_lock_docking',
priority=1,
label=gettext('Prevent Docking'),
checked=False
), MenuItem(
name='mnu_lock_full',
module='pgAdmin.Browser',
callback='mnu_lock_full',
priority=2,
label=gettext('Full Lock'),
checked=False
)]
)
]
}
def register_preferences(self):
register_browser_preferences(self)
@@ -226,7 +261,8 @@ class BrowserModule(PgAdminModule):
return ['browser.index', 'browser.nodes',
'browser.check_master_password',
'browser.set_master_password',
'browser.reset_master_password']
'browser.reset_master_password',
'browser.lock_layout']
blueprint = BrowserModule(MODULE_NAME, __name__)
@@ -815,6 +851,21 @@ def set_master_password():
)
@blueprint.route("/lock_layout", endpoint="lock_layout", methods=["PUT"])
def lock_layout():
data = None
if hasattr(request.data, 'decode'):
data = request.data.decode('utf-8')
if data != '':
data = json.loads(data)
blueprint.lock_layout.set(data['value'])
return make_json_response()
# Only register route if SECURITY_CHANGEABLE is set to True
# We can't access app context here so cannot
# use app.config['SECURITY_CHANGEABLE']
@@ -8,6 +8,12 @@
##########################################################################
from flask_babelex import gettext
LOCK_LAYOUT_LEVEL = {
'PREVENT_DOCKING': 'docking',
'FULL': 'full',
'NONE': 'none'
}
def register_browser_preferences(self):
self.show_system_objects = self.preference.register(
@@ -58,6 +64,21 @@ def register_browser_preferences(self):
)
)
self.lock_layout = self.preference.register(
'display', 'lock_layout',
gettext('Lock Layout'), 'radioModern', LOCK_LAYOUT_LEVEL['NONE'],
category_label=gettext('Display'), options=[
{'label': gettext('None'), 'value': LOCK_LAYOUT_LEVEL['NONE']},
{'label': gettext('Prevent Docking'),
'value': LOCK_LAYOUT_LEVEL['PREVENT_DOCKING']},
{'label': gettext('Full Lock'),
'value': LOCK_LAYOUT_LEVEL['FULL']},
],
help_str=gettext(
'Lock the UI layout at different levels'
)
)
self.table_row_count_threshold = self.preference.register(
'properties', 'table_row_count_threshold',
gettext("Count rows if estimated less than"), 'integer', 2000,
@@ -1540,7 +1540,7 @@ class FunctionView(PGChildNodeView, DataTypeReader):
# Get schema name
status, schema_name = self.conn.execute_scalar(
render_template(
'schema/pg/#{0}#/sql/get_name.sql'.format(
'schemas/pg/#{0}#/sql/get_name.sql'.format(
self.manager.version),
scid=scid
)
@@ -17,7 +17,7 @@ SELECT pg_proc.oid,
WHEN proaccess = '-' THEN 'Private'
ELSE 'Unknown' END AS visibility
FROM pg_proc, pg_namespace, pg_language lng
WHERE pr.prokind IN ('f', 'w')
WHERE prokind IN ('f', 'w')
AND pronamespace = {{pkgid}}::oid
AND pg_proc.pronamespace = pg_namespace.oid
AND lng.oid=prolang
@@ -17,7 +17,7 @@ SELECT pg_proc.oid,
WHEN proaccess = '-' THEN 'Private'
ELSE 'Unknown' END AS visibility
FROM pg_proc, pg_namespace, pg_language lng
WHERE pr.prokind = 'p'
WHERE prokind = 'p'
AND pronamespace = {{pkgid}}::oid
AND pg_proc.pronamespace = pg_namespace.oid
AND lng.oid=prolang
@@ -844,7 +844,7 @@ class SequenceView(PGChildNodeView):
# Get schema name
status, schema_name = self.conn.execute_scalar(
render_template(
'schema/pg/#{0}#/sql/get_name.sql'.format(
'schemas/pg/#{0}#/sql/get_name.sql'.format(
self.manager.version
),
scid=scid
@@ -83,7 +83,7 @@ class IndexesModule(CollectionNodeModule):
if 'vid' not in kwargs:
return True
template_path = 'index/sql/#{0}#'.format(manager.version)
template_path = 'indexes/sql/#{0}#'.format(manager.version)
SQL = render_template(
"/".join([template_path, 'backend_support.sql']),
vid=kwargs['vid']
@@ -771,7 +771,7 @@ class ViewNode(PGChildNodeView, VacuumSettings):
Updated properties data with column details
"""
self.index_temp_path = 'index'
self.index_temp_path = 'indexes'
SQL = render_template("/".join([self.index_temp_path,
'sql/#{0}#/column_details.sql'.format(
self.manager.version)]), idx=idx)
@@ -825,7 +825,7 @@ class ViewNode(PGChildNodeView, VacuumSettings):
Updated properties data with column
"""
self.trigger_temp_path = 'schema/trigger'
self.trigger_temp_path = 'schemas/triggers'
SQL = render_template("/".join([self.trigger_temp_path,
'get_columns.sql']),
tid=tid, clist=clist)
@@ -887,7 +887,7 @@ class ViewNode(PGChildNodeView, VacuumSettings):
import trigger_definition
# Define template path
self.trigger_temp_path = 'trigger'
self.trigger_temp_path = 'triggers'
SQL_data = ''
SQL = render_template("/".join(
@@ -970,7 +970,7 @@ class ViewNode(PGChildNodeView, VacuumSettings):
into sql tab
"""
self.index_temp_path = 'index'
self.index_temp_path = 'indexes'
SQL_data = ''
SQL = render_template("/".join(
[self.index_temp_path,
+17 -49
View File
@@ -15,7 +15,7 @@ define('pgadmin.browser', [
'sources/csrf', 'sources/keyboard_shortcuts', 'pgadmin.browser.utils',
'wcdocker', 'jquery.contextmenu', 'jquery.aciplugin', 'jquery.acitree',
'pgadmin.browser.preferences', 'pgadmin.browser.messages',
'pgadmin.browser.menu', 'pgadmin.browser.panel',
'pgadmin.browser.menu', 'pgadmin.browser.panel', 'pgadmin.browser.layout',
'pgadmin.browser.error', 'pgadmin.browser.frame',
'pgadmin.browser.node', 'pgadmin.browser.collection',
'sources/codemirror/addon/fold/pgadmin-sqlfoldcode',
@@ -282,22 +282,6 @@ define('pgadmin.browser', [
scripts[n].push({'name': m, 'path': p, loaded: false});
},
masterpass_callback_queue: [],
// Build the default layout
buildDefaultLayout: function(docker) {
var browserPanel = docker.addPanel('browser', wcDocker.DOCK.LEFT);
var dashboardPanel = docker.addPanel(
'dashboard', wcDocker.DOCK.RIGHT, browserPanel);
docker.addPanel('properties', wcDocker.DOCK.STACKED, dashboardPanel, {
tabOrientation: wcDocker.TAB.TOP,
});
docker.addPanel('sql', wcDocker.DOCK.STACKED, dashboardPanel);
docker.addPanel(
'statistics', wcDocker.DOCK.STACKED, dashboardPanel);
docker.addPanel(
'dependencies', wcDocker.DOCK.STACKED, dashboardPanel);
docker.addPanel(
'dependents', wcDocker.DOCK.STACKED, dashboardPanel);
},
// Enable/disable menu options
enable_disable_menus: function(item) {
// Mechanism to enable/disable menus depending on the condition.
@@ -354,35 +338,6 @@ define('pgadmin.browser', [
$obj_mnu.append(create_submenu.$el);
}
},
save_current_layout: function(layout_id, docker) {
if(docker) {
var layout = docker.save(),
settings = { setting: layout_id, value: layout };
$.ajax({
type: 'POST',
url: url_for('settings.store_bulk'),
data: settings,
});
}
},
restore_layout: function(docker, layout, defaultLayoutCallback) {
// Try to restore the layout if there is one
if (layout != '') {
try {
docker.restore(layout);
}
catch(err) {
docker.clear();
if(defaultLayoutCallback) {
defaultLayoutCallback(docker);
}
}
} else {
if(defaultLayoutCallback) {
defaultLayoutCallback(docker);
}
}
},
init: function() {
var obj=this;
if (obj.initialized) {
@@ -799,8 +754,8 @@ define('pgadmin.browser', [
menus = pgMenu[a];
}
if (!_.has(menus, m.name)) {
menus[m.name] = new MenuItem({
let get_menuitem_obj = function(m) {
return new MenuItem({
name: m.name, label: m.label, module: m.module,
category: m.category, callback: m.callback,
priority: m.priority, data: m.data, url: m.url || '#',
@@ -808,8 +763,21 @@ define('pgadmin.browser', [
enable: (m.enable == '' ? true : (_.isString(m.enable) &&
m.enable.toLowerCase() == 'false') ?
false : m.enable),
node: m.node,
node: m.node, checked: m.checked,
});
};
if (!_.has(menus, m.name)) {
menus[m.name] = get_menuitem_obj(m);
if(m.menu_items) {
let sub_menu_items = [];
for(let i=0; i<m.menu_items.length; i++) {
sub_menu_items.push(get_menuitem_obj(m.menu_items[i]));
}
menus[m.name]['menu_items'] = sub_menu_items;
}
}
} else {
console.warn(
+161
View File
@@ -0,0 +1,161 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2019, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import pgAdmin from 'sources/pgadmin';
import url_for from 'sources/url_for';
import $ from 'jquery';
import * as Alertify from 'pgadmin.alertifyjs';
import gettext from 'sources/gettext';
import 'wcdocker';
const pgBrowser = pgAdmin.Browser = pgAdmin.Browser || {};
var wcDocker = window.wcDocker;
/* Add cache related methods and properties */
_.extend(pgBrowser, {
lock_layout_levels : {
PREVENT_DOCKING: 'docking',
FULL: 'full',
NONE: 'none',
},
// Build the default layout
buildDefaultLayout: function(docker) {
var browserPanel = docker.addPanel('browser', wcDocker.DOCK.LEFT);
var dashboardPanel = docker.addPanel(
'dashboard', wcDocker.DOCK.RIGHT, browserPanel);
docker.addPanel('properties', wcDocker.DOCK.STACKED, dashboardPanel, {
tabOrientation: wcDocker.TAB.TOP,
});
docker.addPanel('sql', wcDocker.DOCK.STACKED, dashboardPanel);
docker.addPanel(
'statistics', wcDocker.DOCK.STACKED, dashboardPanel);
docker.addPanel(
'dependencies', wcDocker.DOCK.STACKED, dashboardPanel);
docker.addPanel(
'dependents', wcDocker.DOCK.STACKED, dashboardPanel);
},
save_current_layout: function(layout_id, docker) {
if(docker) {
var layout = docker.save(),
settings = { setting: layout_id, value: layout };
$.ajax({
type: 'POST',
url: url_for('settings.store_bulk'),
data: settings,
});
}
},
restore_layout: function(docker, layout, defaultLayoutCallback) {
// Try to restore the layout if there is one
if (layout != '') {
try {
docker.restore(layout);
}
catch(err) {
docker.clear();
if(defaultLayoutCallback) {
defaultLayoutCallback(docker);
}
}
} else {
if(defaultLayoutCallback) {
defaultLayoutCallback(docker);
}
}
/* preference available only with top/opener browser. */
let browser = window.opener ?
window.opener.pgAdmin.Browser : window.top.pgAdmin.Browser;
/* interval required initially as preference load may take time */
let cacheIntervalId = setInterval(()=> {
let browserPref = browser.get_preferences_for_module('browser');
if(browserPref) {
clearInterval(cacheIntervalId);
browser.reflectLocklayoutChange(docker);
browser.onPreferencesChange('browser', function() {
browser.reflectLocklayoutChange(docker);
});
}
}, 500);
},
reflectLocklayoutChange: function(docker) {
let browser = window.opener ?
window.opener.pgAdmin.Browser : window.top.pgAdmin.Browser;
let browserPref = browser.get_preferences_for_module('browser');
browser.lock_layout(docker, browserPref.lock_layout);
},
lock_layout: function(docker, op) {
let menu_items = this.menus['file']['mnu_locklayout']['menu_items'];
switch(op) {
case this.lock_layout_levels.PREVENT_DOCKING:
docker.lockLayout(wcDocker.LOCK_LAYOUT_LEVEL.PREVENT_DOCKING);
break;
case this.lock_layout_levels.FULL:
docker.lockLayout(wcDocker.LOCK_LAYOUT_LEVEL.FULL);
break;
case this.lock_layout_levels.NONE:
docker.lockLayout(wcDocker.LOCK_LAYOUT_LEVEL.NONE);
break;
}
_.each(menu_items, function(menu_item) {
if(menu_item.name != ('mnu_lock_'+op)) {
menu_item.change_checked(false);
} else {
menu_item.change_checked(true);
}
});
},
save_lock_layout: function(op) {
let browser = window.opener ?
window.opener.pgAdmin.Browser : window.top.pgAdmin.Browser;
$.ajax({
url: url_for('browser.lock_layout'),
method: 'PUT',
contentType: 'application/json',
data: JSON.stringify({
'value': op,
}),
}).done(function() {
browser.cache_preferences('browser');
}).fail(function(xhr, error) {
Alertify.pgNotifier(error, xhr, gettext('Failed to save the lock layout setting.'));
});
},
mnu_lock_docking: function() {
this.lock_layout(this.docker, this.lock_layout_levels.PREVENT_DOCKING);
this.save_lock_layout(this.lock_layout_levels.PREVENT_DOCKING);
},
mnu_lock_full: function() {
this.lock_layout(this.docker, this.lock_layout_levels.FULL);
this.save_lock_layout(this.lock_layout_levels.FULL);
},
mnu_lock_none: function() {
this.lock_layout(this.docker, this.lock_layout_levels.NONE);
this.save_lock_layout(this.lock_layout_levels.NONE);
},
});
export {pgBrowser};
+54 -22
View File
@@ -19,6 +19,7 @@ define([
var menu_opts = [
'name', 'label', 'priority', 'module', 'callback', 'data', 'enable',
'category', 'target', 'url' /* Do not show icon in the menus, 'icon' */ , 'node',
'checked', 'menu_items',
],
defaults = {
url: '#',
@@ -55,31 +56,48 @@ define([
* Create the jquery element for the menu-item.
*/
create_el: function(node, item) {
var url = $('<a></a>', {
'id': this.name,
'href': this.url,
'target': this.target,
'data-toggle': 'pg-menu',
}).data('pgMenu', {
module: this.module || pgAdmin.Browser,
cb: this.callback,
data: this.data,
}).addClass('dropdown-item');
this.is_disabled = this.disabled(node, item);
if (this.icon) {
url.append($('<i></i>', {
'class': this.icon,
}));
if(this.menu_items) {
_.each(this.menu_items, function(submenu_item){
submenu_item.generate(node, item);
});
var create_submenu = pgAdmin.Browser.MenuGroup({
'label': this.label,
'id': this.name,
}, this.menu_items);
this.$el = create_submenu.$el;
} else {
var url = $('<a></a>', {
'id': this.name,
'href': this.url,
'target': this.target,
'data-toggle': 'pg-menu',
}).data('pgMenu', {
module: this.module || pgAdmin.Browser,
cb: this.callback,
data: this.data,
}).addClass('dropdown-item');
this.is_disabled = this.disabled(node, item);
if (this.icon) {
url.append($('<i></i>', {
'class': this.icon,
}));
} else if(!_.isUndefined(this.checked)) {
url.append($('<i></i>', {
'class': 'fa fa-check '+ (this.checked?'':'visibility-hidden'),
}));
}
url.addClass((this.is_disabled ? ' disabled' : ''));
var textSpan = $('<span data-test="menu-item-text"></span>').text(' ' + this.label);
url.append(textSpan);
this.$el = $('<li/>').append(url);
}
url.addClass((this.is_disabled ? ' disabled' : ''));
var textSpan = $('<span data-test="menu-item-text"></span>').text(' ' + this.label);
url.append(textSpan);
this.$el = $('<li/>').append(url);
},
/*
* Updates the enable/disable state of the menu-item based on the current
@@ -154,6 +172,20 @@ define([
return false;
},
/*
* Change the checked value and update the checked icon on the menu
*/
change_checked(isChecked) {
if(!_.isUndefined(this.checked)) {
this.checked = isChecked;
if(this.checked) {
this.$el.find('.fa-check').removeClass('visibility-hidden');
} else {
this.$el.find('.fa-check').addClass('visibility-hidden');
}
}
},
});
/*
@@ -7,6 +7,32 @@
//
//////////////////////////////////////////////////////////////
{% macro A_MENU_ITEM(key, item) -%}
{
name: "{{ item.name }}",
{% if item.module %}module: {{ item.module }},
{% endif %}{% if item.url %}url: "{{ item.url }}",
{% endif %}{% if item.target %}target: "{{ item.target }}",
{% endif %}{% if item.callback %}callback: "{{ item.callback }}",
{% endif %}{% if item.category %}category: "{{ item.category }}",
{% endif %}{% if item.icon %}icon: '{{ item.icon }}',
{% endif %}{% if item.data %}data: {{ item.data }},
{% endif %}label: '{{ item.label }}', applies: ['{{ key.lower() }}'],
priority: {{ item.priority }},
enable: '{{ item.enable }}',
{% if item.checked is defined %}checked: {% if item.checked %}true{% else %}false{% endif %},
{% endif %}
{% if item.menu_items %}menu_items: {{MENU_ITEMS(key, item.menu_items)}}
{% endif %}
}
{%- endmacro %}
{% macro MENU_ITEMS(key, items) -%}
[
{% for item in items %}{% if loop.index != 1 %}, {% endif %}
{{ A_MENU_ITEM(key, item) }}{% set hasMenus = True %}{% endfor %}
]
{%- endmacro %}
define('pgadmin.browser.utils',
['sources/pgadmin'], function(pgAdmin) {
@@ -56,19 +82,7 @@ define('pgadmin.browser.utils',
var self = this;
if (this.counter.total == this.counter.loaded) {
{% for key in ('File', 'Edit', 'Object' 'Tools', 'Management', 'Help') %}
obj.add_menus([{% for item in current_app.menu_items['%s_items' % key.lower()] %}{% if loop.index != 1 %}, {% endif %}{
name: "{{ item.name }}",
{% if item.module %}module: {{ item.module }},
{% endif %}{% if item.url %}url: "{{ item.url }}",
{% endif %}{% if item.target %}target: "{{ item.target }}",
{% endif %}{% if item.callback %}callback: "{{ item.callback }}",
{% endif %}{% if item.category %}category: "{{ item.category }}",
{% endif %}{% if item.icon %}icon: '{{ item.icon }}',
{% endif %}{% if item.data %}data: {{ item.data }},
{% endif %}label: '{{ item.label }}', applies: ['{{ key.lower() }}'],
priority: {{ item.priority }},
enable: '{{ item.enable }}'
}{% set hasMenus = True %}{% endfor %}]);
obj.add_menus({{ MENU_ITEMS(key, current_app.menu_items['%s_items' % key.lower()])}});
{% endfor %}
obj.create_menus();
} else {
+245 -212
View File
File diff suppressed because it is too large. Load diff
+261 -314
View File
@@ -10,11 +10,13 @@
define('pgadmin.misc.explain', [
'sources/url_for', 'jquery', 'underscore', 'underscore.string',
'sources/pgadmin', 'backbone', 'snapsvg', 'explain_statistics',
'svg_downloader',
], function(url_for, $, _, S, pgAdmin, Backbone, Snap, StatisticsModel, svgDownloader) {
'svg_downloader', 'image_maper',
], function(url_for, $, _, S, pgAdmin, Backbone, Snap, StatisticsModel,
svgDownloader, imageMapper) {
pgAdmin = pgAdmin || window.pgAdmin || {};
svgDownloader = svgDownloader.default;
var pgBrowser = pgAdmin.Browser;
// Snap.svg plug-in to write multitext as image name
Snap.plugin(function(Snap, Element, Paper) {
@@ -97,279 +99,9 @@ define('pgadmin.misc.explain', [
var pgExplain = pgAdmin.Explain = {
// Prefix path where images are stored
prefix: url_for('misc.index') + 'static/explain/img/',
};
/*
* A map which is used to fetch the image to be drawn and
* text which will appear below it
*/
var imageMapper = {
'Aggregate': {
'image': 'ex_aggregate.svg',
'image_text': 'Aggregate',
},
'Append': {
'image': 'ex_append.svg',
'image_text': 'Append',
},
'Bitmap Index Scan': function(data) {
return {
'image': 'ex_bmp_index.svg',
'image_text': data['Index Name'],
};
},
'Bitmap Heap Scan': function(data) {
return {
'image': 'ex_bmp_heap.svg',
'image_text': data['Relation Name'],
};
},
'BitmapAnd': {
'image': 'ex_bmp_and.svg',
'image_text': 'Bitmap AND',
},
'BitmapOr': {
'image': 'ex_bmp_or.svg',
'image_text': 'Bitmap OR',
},
'CTE Scan': {
'image': 'ex_cte_scan.svg',
'image_text': 'CTE Scan',
},
'Function Scan': {
'image': 'ex_result.svg',
'image_text': 'Function Scan',
},
'Foreign Scan': {
'image': 'ex_foreign_scan.svg',
'image_text': 'Foreign Scan',
},
'Gather': {
'image': 'ex_gather_motion.svg',
'image_text': 'Gather',
},
'Group': {
'image': 'ex_group.svg',
'image_text': 'Group',
},
'GroupAggregate': {
'image': 'ex_aggregate.svg',
'image_text': 'Group Aggregate',
},
'Hash': {
'image': 'ex_hash.svg',
'image_text': 'Hash',
},
'Hash Join': function(data) {
if (!data['Join Type']) return {
'image': 'ex_join.svg',
'image_text': 'Join',
};
switch (data['Join Type']) {
case 'Anti':
return {
'image': 'ex_hash_anti_join.svg',
'image_text': 'Hash Anti Join',
};
case 'Semi':
return {
'image': 'ex_hash_semi_join.svg',
'image_text': 'Hash Semi Join',
};
default:
return {
'image': 'ex_hash.svg',
'image_text': String('Hash ' + data['Join Type'] + ' Join'),
};
}
},
'HashAggregate': {
'image': 'ex_aggregate.svg',
'image_text': 'Hash Aggregate',
},
'Index Only Scan': function(data) {
return {
'image': 'ex_index_only_scan.svg',
'image_text': data['Index Name'],
};
},
'Index Scan': function(data) {
return {
'image': 'ex_index_scan.svg',
'image_text': data['Index Name'],
};
},
'Index Scan Backword': {
'image': 'ex_index_scan.svg',
'image_text': 'Index Backward Scan',
},
'Limit': {
'image': 'ex_limit.svg',
'image_text': 'Limit',
},
'LockRows': {
'image': 'ex_lock_rows.svg',
'image_text': 'Lock Rows',
},
'Materialize': {
'image': 'ex_materialize.svg',
'image_text': 'Materialize',
},
'Merge Append': {
'image': 'ex_merge_append.svg',
'image_text': 'Merge Append',
},
'Merge Join': function(data) {
switch (data['Join Type']) {
case 'Anti':
return {
'image': 'ex_merge_anti_join.svg',
'image_text': 'Merge Anti Join',
};
case 'Semi':
return {
'image': 'ex_merge_semi_join.svg',
'image_text': 'Merge Semi Join',
};
default:
return {
'image': 'ex_merge.svg',
'image_text': String('Merge ' + data['Join Type'] + ' Join'),
};
}
},
'ModifyTable': function(data) {
switch (data['Operation']) {
case 'Insert':
return {
'image': 'ex_insert.svg',
'image_text': 'Insert',
};
case 'Update':
return {
'image': 'ex_update.svg',
'image_text': 'Update',
};
case 'Delete':
return {
'image': 'ex_delete.svg',
'image_text': 'Delete',
};
}
},
'Nested Loop': function(data) {
switch (data['Join Type']) {
case 'Anti':
return {
'image': 'ex_nested_loop_anti_join.svg',
'image_text': 'Nested Loop Anti Join',
};
case 'Semi':
return {
'image': 'ex_nested_loop_semi_join.svg',
'image_text': 'Nested Loop Semi Join',
};
default:
return {
'image': 'ex_nested.svg',
'image_text': 'Nested Loop ' + data['Join Type'] + ' Join',
};
}
},
'Recursive Union': {
'image': 'ex_recursive_union.svg',
'image_text': 'Recursive Union',
},
'Result': {
'image': 'ex_result.svg',
'image_text': 'Result',
},
'Sample Scan': {
'image': 'ex_scan.svg',
'image_text': 'Sample Scan',
},
'Scan': {
'image': 'ex_scan.svg',
'image_text': 'Scan',
},
'Seek': {
'image': 'ex_seek.svg',
'image_text': 'Seek',
},
'SetOp': function(data) {
var strategy = data['Strategy'],
command = data['Command'];
if (strategy == 'Hashed') {
if (S.startsWith(command, 'Intersect')) {
if (command == 'Intersect All')
return {
'image': 'ex_hash_setop_intersect_all.svg',
'image_text': 'Hashed Intersect All',
};
return {
'image': 'ex_hash_setop_intersect.svg',
'image_text': 'Hashed Intersect',
};
} else if (S.startsWith(command, 'Except')) {
if (command == 'Except All')
return {
'image': 'ex_hash_setop_except_all.svg',
'image_text': 'Hashed Except All',
};
return {
'image': 'ex_hash_setop_except.svg',
'image_text': 'Hash Except',
};
}
return {
'image': 'ex_hash_setop_unknown.svg',
'image_text': 'Hashed SetOp Unknown',
};
}
return {
'image': 'ex_setop.svg',
'image_text': 'SetOp',
};
},
'Seq Scan': function(data) {
return {
'image': 'ex_scan.svg',
'image_text': data['Relation Name'],
};
},
'Subquery Scan': {
'image': 'ex_subplan.svg',
'image_text': 'SubQuery Scan',
},
'Sort': {
'image': 'ex_sort.svg',
'image_text': 'Sort',
},
'Tid Scan': {
'image': 'ex_tid_scan.svg',
'image_text': 'Tid Scan',
},
'Unique': {
'image': 'ex_unique.svg',
'image_text': 'Unique',
},
'Values Scan': {
'image': 'ex_values_scan.svg',
'image_text': 'Values Scan',
},
'WindowAgg': {
'image': 'ex_window_aggregate.svg',
'image_text': 'Window Aggregate',
},
'WorkTable Scan': {
'image': 'ex_worktable_scan.svg',
'image_text': 'WorkTable Scan',
},
'Undefined': {
'image': 'ex_unknown.svg',
'image_text': 'Undefined',
},
totalNodes: 0,
totalDownloadedNodes: 0,
isDownloaded: false,
};
// Some predefined constants used to calculate image location and its border
@@ -433,9 +165,10 @@ define('pgadmin.misc.explain', [
node_type = node_type.substring(0, 7);
// Get the image information for current node
var mappedImage = (_.isFunction(imageMapper[node_type]) &&
imageMapper[node_type].apply(undefined, [data])) ||
imageMapper[node_type] || {
let imageStore = imageMapper.default;
var mappedImage = (_.isFunction(imageStore[node_type]) &&
imageStore[node_type].apply(undefined, [data])) ||
imageStore[node_type] || {
'image': 'ex_unknown.svg',
'image_text': node_type,
};
@@ -576,34 +309,7 @@ define('pgadmin.misc.explain', [
});
}
/* Check the current browser, if it is Internet Explorer then we will not
* embed the SVG files for download feature as we are not bale to figure
* out the solution for IE.
*/
var current_browser = pgAdmin.Browser.get_browser();
if (current_browser.name === 'IE' ||
(current_browser.name === 'Safari' && parseInt(current_browser.version) < 10)) {
this.draw_image(g, pgExplain.prefix + this.get('image'), currentXpos, currentYpos, graphContainer, toolTipContainer);
} else {
/* This function is a callback function called when we load any svg file
* using Snap. In this function we append the SVG binary data to the new
* temporary Snap object and then embedded it to the original Snap() object.
*/
var that = this;
var onSVGLoaded = function(data) {
var svg_image = Snap();
svg_image.append(data);
that.draw_image(g, svg_image.toDataURL(), currentXpos, currentYpos, graphContainer, toolTipContainer);
// This attribute is required to download the file as SVG image.
s.parent().attr({'xmlns:xlink':'http://www.w3.org/1999/xlink'});
};
var svg_file = pgExplain.prefix + this.get('image');
// Load the SVG file for explain plan
Snap.load(svg_file, onSVGLoaded);
}
this.draw_image(g, pgExplain.prefix + this.get('image'), currentXpos, currentYpos, graphContainer, toolTipContainer);
// Draw text below the node
var node_label = this.get('Schema') == undefined ?
@@ -760,6 +466,224 @@ define('pgadmin.misc.explain', [
},
});
/*
* NOTE: embedding using .toDataURL() method hits the performance of the
* plan rendering a lot, that is why we have written seprate Model for the same
* which is used only when downloading of SVG is called
*/
// We override the PlanModel's draw() function so that we can embbed all the
// svg in to main one SVG so that we can download it.
let DownloadPlanModel = PlanModel.extend({
// Draw image, its name and its tooltip
parse: function(data) {
var idx = 1,
lvl = data.level = data.level || [idx],
plans = [],
node_type = data['Node Type'],
// Calculating relative xpos of current node from top node
xpos = data.xpos = data.xpos - pWIDTH,
// Calculating relative ypos of current node from top node
ypos = data.ypos,
maxChildWidth = 0;
data['width'] = pWIDTH;
data['height'] = pHEIGHT;
/*
* calculating xpos, ypos, width and height if current node is a subplan
*/
if (data['Parent Relationship'] === 'SubPlan') {
data['width'] += (xMargin * 2) + (xMargin / 2);
data['height'] += (yMargin * 2);
data['ypos'] += yMargin;
xpos -= xMargin;
ypos += yMargin;
}
if (S.startsWith(node_type, '(slice'))
node_type = node_type.substring(0, 7);
// Get the image information for current node
let imageStore = imageMapper.default;
var mappedImage = (_.isFunction(imageStore[node_type]) &&
imageStore[node_type].apply(undefined, [data])) ||
imageStore[node_type] || {
'image': 'ex_unknown.svg',
'image_text': node_type,
};
data['image'] = mappedImage['image'];
data['image_text'] = mappedImage['image_text'];
pgExplain.totalNodes++;
// Start calculating xpos, ypos, width and height for child plans if any
if ('Plans' in data) {
data['width'] += offsetX;
_.each(data['Plans'], function(p) {
var level = _.clone(lvl),
plan = new DownloadPlanModel({ 'parse': true });
level.push(idx);
plan.set(plan.parse(_.extend(
p, {
'level': level,
xpos: xpos - offsetX,
ypos: ypos,
})));
if (maxChildWidth < plan.get('width')) {
maxChildWidth = plan.get('width');
}
var childHeight = plan.get('height');
if (idx !== 1) {
data['height'] = data['height'] + childHeight + offsetY;
} else if (childHeight > data['height']) {
data['height'] = childHeight;
}
ypos += childHeight + offsetY;
plans.push(plan);
idx++;
});
}
// Final Width and Height of current node
data['width'] += maxChildWidth;
data['Plans'] = plans;
return data;
},
draw: function(s, xpos, ypos, pXpos, pYpos, graphContainer, toolTipContainer) {
var g = s.g();
var currentXpos = xpos + this.get('xpos'),
currentYpos = ypos + this.get('ypos'),
isSubPlan = (this.get('Parent Relationship') === 'SubPlan');
// Draw the subplan rectangle
if (isSubPlan) {
g.rect(
currentXpos - this.get('width') + pWIDTH + xMargin,
currentYpos - this.get('height') + pHEIGHT + yMargin - TXT_ALIGN,
this.get('width') - xMargin,
this.get('height') + (currentYpos - yMargin),
5
).attr({
stroke: '#444444',
'strokeWidth': 1.2,
fill: 'gray',
fillOpacity: 0.2,
});
// Provide subplan name
g.text(
currentXpos + pWIDTH - (this.get('width') / 2) - xMargin,
currentYpos + pHEIGHT - (this.get('height') / 2) - yMargin,
this.get('Subplan Name')
).attr({
fontSize: TXT_SIZE,
'text-anchor': 'start',
fill: 'red',
});
}
/* Check the current browser, if it is Internet Explorer then we will not
* embed the SVG files for download feature as we are not bale to figure
* out the solution for IE.
*/
var current_browser = pgAdmin.Browser.get_browser();
if (current_browser.name === 'IE' ||
(current_browser.name === 'Safari' && parseInt(current_browser.version) < 10)) {
this.draw_image(g, pgExplain.prefix + this.get('image'), currentXpos, currentYpos, graphContainer, toolTipContainer);
} else {
/* This function is a callback function called when we load any svg file
* using Snap. In this function we append the SVG binary data to the new
* temporary Snap object and then embedded it to the original Snap() object.
*/
var that = this;
var onSVGLoaded = function(data) {
var svg_image = Snap();
svg_image.append(data);
that.draw_image(g, svg_image.toDataURL(), currentXpos, currentYpos, graphContainer, toolTipContainer);
pgExplain.totalDownloadedNodes++;
// This attribute is required to download the file as SVG image.
s.parent().attr({'xmlns:xlink':'http://www.w3.org/1999/xlink'});
setTimeout(() => {
pgBrowser.Events.trigger('pga:explain_plan:node_icon:fetched');
}, 100);
};
var svg_file = pgExplain.prefix + this.get('image');
// Load the SVG file for explain plan
Snap.load(svg_file, onSVGLoaded);
}
// Draw text below the node
var node_label = this.get('Schema') == undefined ?
this.get('image_text') :
(this.get('Schema') + '.' + this.get('image_text'));
g.multitext(
currentXpos + (pWIDTH / 2) + TXT_ALIGN,
currentYpos + pHEIGHT - TXT_ALIGN,
node_label,
150, {
'font-size': TXT_SIZE,
'text-anchor': 'middle',
}
);
// Draw Arrow to parent only its not the first node
if (!_.isUndefined(pYpos)) {
var startx = currentXpos + pWIDTH;
var starty = currentYpos + (pHEIGHT / 2);
var endx = pXpos - ARROW_WIDTH;
var endy = pYpos + (pHEIGHT / 2);
var start_cost = this.get('Startup Cost'),
total_cost = this.get('Total Cost');
var arrow_size = DEFAULT_ARROW_SIZE;
// Calculate arrow width according to cost of a particular plan
if (start_cost != undefined && total_cost != undefined) {
arrow_size = Math.round(Math.log((start_cost + total_cost) / 2 + start_cost));
arrow_size = arrow_size < 1 ? 1 : arrow_size > 10 ? 10 : arrow_size;
}
var arrow_view_box = [0, 0, 2 * ARROW_WIDTH, 2 * ARROW_HEIGHT];
var opts = {
stroke: '#000000',
strokeWidth: arrow_size + 2,
},
subplanOpts = {
stroke: '#866486',
strokeWidth: arrow_size + 2,
},
arrowOpts = {
viewBox: arrow_view_box.join(' '),
};
// Draw an arrow from current node to its parent
this.drawPolyLine(
g, startx, starty, endx, endy,
isSubPlan ? subplanOpts : opts, arrowOpts
);
}
var plans = this.get('Plans');
// Draw nodes for current plan's children
_.each(plans, function(p) {
p.draw(s, xpos, ypos, currentXpos, currentYpos, graphContainer, toolTipContainer);
});
},
});
// Main backbone model to store JSON object
var MainPlanModel = Backbone.Model.extend({
defaults: {
@@ -840,8 +764,12 @@ define('pgadmin.misc.explain', [
// Parse and draw full graphical explain
_.extend(pgExplain, {
// Assumption container is a jQuery object
DrawJSONPlan: function(container, plan) {
DrawJSONPlan: function(container, plan, isDownload) {
pgExplain.totalNodes = 0;
pgExplain.totalDownloadedNodes = 0;
pgExplain.isDownloaded = false;
container.empty();
var orignalPlan = $.extend(true, [], plan);
var curr_zoom_factor = 1.0;
var zoomArea = $('<div></div>', {
@@ -931,7 +859,20 @@ define('pgadmin.misc.explain', [
h = yMargin;
_.each(plan, function(p) {
var main_plan = new MainPlanModel();
var main_plan;
if(isDownload) {
// If user opt to download then we will use the DownloadPlanModel model
// so that it will embed the images while regenrating the plan
let DownloadMainPlanModel = MainPlanModel.extend({
initialize: function() {
this.set('Plan', new DownloadPlanModel({ parse: true }));
this.set('Statistics', new StatisticsModel());
},
});
main_plan = new DownloadMainPlanModel({ 'parse': true });
} else {
main_plan = new MainPlanModel();
}
// Parse JSON data to backbone model
main_plan.set(main_plan.parse(p));
@@ -1018,14 +959,20 @@ define('pgadmin.misc.explain', [
});
downloadBtn.on('click', function() {
var s = Snap('.pgadmin-explain-container svg');
var today = new Date();
var filename = 'explain_plan_' + today.getTime() + '.svg';
svgDownloader.downloadSVG(s.toString(), filename);
downloadBtn.trigger('blur');
// Lets regenrate the plan with embedded images
pgExplain.DrawJSONPlan(container, orignalPlan, true);
pgBrowser.Events.on('pga:explain_plan:node_icon:fetched', function() {
if (!pgExplain.isDownloaded && pgExplain.totalNodes === pgExplain.totalDownloadedNodes) {
pgExplain.isDownloaded = true;
var s = Snap('.pgadmin-explain-container svg');
var today = new Date();
var filename = 'explain_plan_' + today.getTime() + '.svg';
svgDownloader.downloadSVG(s.toString(), filename);
downloadBtn.trigger('blur');
}
});
});
});
},
});
@@ -0,0 +1,285 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2019, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import S from 'underscore.string';
/*
* A map which is used to fetch the image to be drawn and
* text which will appear below it
*/
let imageMapper = {
'Aggregate': {
'image': 'ex_aggregate.svg',
'image_text': 'Aggregate',
},
'Append': {
'image': 'ex_append.svg',
'image_text': 'Append',
},
'Bitmap Index Scan': function(data) {
return {
'image': 'ex_bmp_index.svg',
'image_text': data['Index Name'],
};
},
'Bitmap Heap Scan': function(data) {
return {
'image': 'ex_bmp_heap.svg',
'image_text': data['Relation Name'],
};
},
'BitmapAnd': {
'image': 'ex_bmp_and.svg',
'image_text': 'Bitmap AND',
},
'BitmapOr': {
'image': 'ex_bmp_or.svg',
'image_text': 'Bitmap OR',
},
'CTE Scan': {
'image': 'ex_cte_scan.svg',
'image_text': 'CTE Scan',
},
'Function Scan': {
'image': 'ex_result.svg',
'image_text': 'Function Scan',
},
'Foreign Scan': {
'image': 'ex_foreign_scan.svg',
'image_text': 'Foreign Scan',
},
'Gather': {
'image': 'ex_gather_motion.svg',
'image_text': 'Gather',
},
'Group': {
'image': 'ex_group.svg',
'image_text': 'Group',
},
'GroupAggregate': {
'image': 'ex_aggregate.svg',
'image_text': 'Group Aggregate',
},
'Hash': {
'image': 'ex_hash.svg',
'image_text': 'Hash',
},
'Hash Join': function(data) {
if (!data['Join Type']) return {
'image': 'ex_join.svg',
'image_text': 'Join',
};
switch (data['Join Type']) {
case 'Anti':
return {
'image': 'ex_hash_anti_join.svg',
'image_text': 'Hash Anti Join',
};
case 'Semi':
return {
'image': 'ex_hash_semi_join.svg',
'image_text': 'Hash Semi Join',
};
default:
return {
'image': 'ex_hash.svg',
'image_text': String('Hash ' + data['Join Type'] + ' Join'),
};
}
},
'HashAggregate': {
'image': 'ex_aggregate.svg',
'image_text': 'Hash Aggregate',
},
'Index Only Scan': function(data) {
return {
'image': 'ex_index_only_scan.svg',
'image_text': data['Index Name'],
};
},
'Index Scan': function(data) {
return {
'image': 'ex_index_scan.svg',
'image_text': data['Index Name'],
};
},
'Index Scan Backword': {
'image': 'ex_index_scan.svg',
'image_text': 'Index Backward Scan',
},
'Limit': {
'image': 'ex_limit.svg',
'image_text': 'Limit',
},
'LockRows': {
'image': 'ex_lock_rows.svg',
'image_text': 'Lock Rows',
},
'Materialize': {
'image': 'ex_materialize.svg',
'image_text': 'Materialize',
},
'Merge Append': {
'image': 'ex_merge_append.svg',
'image_text': 'Merge Append',
},
'Merge Join': function(data) {
switch (data['Join Type']) {
case 'Anti':
return {
'image': 'ex_merge_anti_join.svg',
'image_text': 'Merge Anti Join',
};
case 'Semi':
return {
'image': 'ex_merge_semi_join.svg',
'image_text': 'Merge Semi Join',
};
default:
return {
'image': 'ex_merge.svg',
'image_text': String('Merge ' + data['Join Type'] + ' Join'),
};
}
},
'ModifyTable': function(data) {
switch (data['Operation']) {
case 'Insert':
return {
'image': 'ex_insert.svg',
'image_text': 'Insert',
};
case 'Update':
return {
'image': 'ex_update.svg',
'image_text': 'Update',
};
case 'Delete':
return {
'image': 'ex_delete.svg',
'image_text': 'Delete',
};
}
},
'Nested Loop': function(data) {
switch (data['Join Type']) {
case 'Anti':
return {
'image': 'ex_nested_loop_anti_join.svg',
'image_text': 'Nested Loop Anti Join',
};
case 'Semi':
return {
'image': 'ex_nested_loop_semi_join.svg',
'image_text': 'Nested Loop Semi Join',
};
default:
return {
'image': 'ex_nested.svg',
'image_text': 'Nested Loop ' + data['Join Type'] + ' Join',
};
}
},
'Recursive Union': {
'image': 'ex_recursive_union.svg',
'image_text': 'Recursive Union',
},
'Result': {
'image': 'ex_result.svg',
'image_text': 'Result',
},
'Sample Scan': {
'image': 'ex_scan.svg',
'image_text': 'Sample Scan',
},
'Scan': {
'image': 'ex_scan.svg',
'image_text': 'Scan',
},
'Seek': {
'image': 'ex_seek.svg',
'image_text': 'Seek',
},
'SetOp': function(data) {
let strategy = data['Strategy'],
command = data['Command'];
if (strategy == 'Hashed') {
if (S.startsWith(command, 'Intersect')) {
if (command == 'Intersect All')
return {
'image': 'ex_hash_setop_intersect_all.svg',
'image_text': 'Hashed Intersect All',
};
return {
'image': 'ex_hash_setop_intersect.svg',
'image_text': 'Hashed Intersect',
};
} else if (S.startsWith(command, 'Except')) {
if (command == 'Except All')
return {
'image': 'ex_hash_setop_except_all.svg',
'image_text': 'Hashed Except All',
};
return {
'image': 'ex_hash_setop_except.svg',
'image_text': 'Hash Except',
};
}
return {
'image': 'ex_hash_setop_unknown.svg',
'image_text': 'Hashed SetOp Unknown',
};
}
return {
'image': 'ex_setop.svg',
'image_text': 'SetOp',
};
},
'Seq Scan': function(data) {
return {
'image': 'ex_scan.svg',
'image_text': data['Relation Name'],
};
},
'Subquery Scan': {
'image': 'ex_subplan.svg',
'image_text': 'SubQuery Scan',
},
'Sort': {
'image': 'ex_sort.svg',
'image_text': 'Sort',
},
'Tid Scan': {
'image': 'ex_tid_scan.svg',
'image_text': 'Tid Scan',
},
'Unique': {
'image': 'ex_unique.svg',
'image_text': 'Unique',
},
'Values Scan': {
'image': 'ex_values_scan.svg',
'image_text': 'Values Scan',
},
'WindowAgg': {
'image': 'ex_window_aggregate.svg',
'image_text': 'Window Aggregate',
},
'WorkTable Scan': {
'image': 'ex_worktable_scan.svg',
'image_text': 'WorkTable Scan',
},
'Undefined': {
'image': 'ex_unknown.svg',
'image_text': 'Undefined',
},
};
export default imageMapper;
+1 -1
View File
@@ -48,7 +48,7 @@ class PreferencesModule(PgAdminModule):
return {
'file_items': [
MenuItem(name='mnu_preferences',
priority=999,
priority=997,
module="pgAdmin.Preferences",
callback='show',
icon='fa fa-cog',
@@ -271,6 +271,8 @@ define('pgadmin.preferences', [
return 'switch';
case 'keyboardshortcut':
return 'keyboardShortcut';
case 'radioModern':
return 'radioModern';
default:
if (console && console.warn) {
// Warning for developer only.
+1 -1
View File
@@ -39,7 +39,7 @@ class SettingsModule(PgAdminModule):
'file_items': [
MenuItem(
name='mnu_resetlayout',
priority=999,
priority=998,
module="pgAdmin.Settings",
callback='show',
icon='fa fa-retweet',
+54
View File
@@ -60,6 +60,7 @@ define([
'select2': 'select2',
'note': 'note',
'color': 'color',
'radioModern': 'radioModern',
};
Backform.getMappedControl = function(type, mode) {
@@ -95,6 +96,17 @@ define([
return type;
};
/* Returns raw data as it is */
var RawFormatter = Backform.RawFormatter = function() {};
_.extend(RawFormatter.prototype, {
fromRaw: function(rawData) {
return rawData;
},
toRaw: function(formattedData) {
return formattedData;
},
});
var BackformControlInit = Backform.Control.prototype.initialize,
BackformControlRemove = Backform.Control.prototype.remove;
@@ -422,6 +434,48 @@ define([
},
});
Backform.RadioModernControl = Backform.RadioControl.extend({
defaults: {
controlLabelClassName: Backform.controlLabelClassName,
controlsClassName: Backform.controlsClassName,
extraClasses: [],
helpMessage: '',
name: '',
},
template: _.template([
'<label class="<%=controlLabelClassName%>"><%=label%></label>',
'<div class="<%=controlsClassName%> <%=extraClasses.join(\' \')%>">',
' <div class="btn-group pgadmin-controls-radio-none" data-toggle="buttons">',
' <% for (var i=0; i < options.length; i++) { %>',
' <% var option = options[i]; %>',
' <label class="btn btn-primary<% if (option.value == value) { %> active<%}%>" tabindex="0">',
' <input type="radio" name="<%=name%>" autocomplete="off" value=<%-formatter.fromRaw(option.value)%> <% if (option.value == value) { %> checked<%}%> > <%-option.label%>',
' </label>',
' <% } %>',
' </div>',
' <% if (helpMessage && helpMessage.length) { %>',
' <span class="<%=Backform.helpMessageClassName%>"><%=helpMessage%></span>',
' <% } %>',
'</div>',
].join('\n')),
formatter: RawFormatter,
getValueFromDOM: function() {
return this.formatter.toRaw(this.$el.find('input[type="radio"]:checked').attr('value'), this.model);
},
render: function() {
Backform.RadioControl.prototype.render.apply(this, arguments);
this.$el.find('.btn').on('keyup', (e)=>{
switch(e.keyCode) {
case 32: /* Spacebar click */
$(e.currentTarget).trigger('click');
break;
}
});
return this;
},
});
// Requires the Bootstrap Switch to work.
Backform.SwitchControl = Backform.InputControl.extend({
defaults: {
@@ -310,3 +310,8 @@ td.switch-cell > div.toggle {
.btn.disabled, .btn:disabled, .btn[disabled] {
opacity: $btn-disabled-opacity;
}
.btn-group label.btn.btn-primary.active {
background-color: $color-primary-light;
color: $color-primary;
}
@@ -51,8 +51,11 @@ define([
id: 'op',
label: gettext('Maintenance operation'),
cell: 'string',
type: 'text',
type: 'radioModern',
controlsClassName: 'pgadmin-controls col-12 col-sm-8',
controlLabelClassName: 'control-label col-sm-4 col-12',
group: gettext('Options'),
value: 'VACUUM',
options: [{
'label': 'VACUUM',
'value': 'VACUUM',
@@ -70,30 +73,6 @@ define([
'value': 'CLUSTER',
},
],
control: Backform.RadioControl.extend({
template: _.template([
'<label class="control-label col-sm-4 col-12"><%=label%></label>',
'<div class="pgadmin-controls col-12 col-sm-8 btn-group pg-maintenance-op pgadmin-controls-radio-none" data-toggle="buttons">',
' <% for (var i=0; i < options.length; i++) { %>',
' <% var option = options[i]; %>',
' <label class="btn btn-primary<% if (i == 0) { %> active<%}%>" tabindex="0">',
' <input type="radio" name="op" id="op" autocomplete="off" value=<%-formatter.fromRaw(option.value)%><% if (i == 0) { %> selected<%}%> > <%-option.label%>',
' </label>',
' <% } %>',
'</div>',
].join('\n')),
render: function() {
Backform.RadioControl.prototype.render.apply(this, arguments);
this.$el.find('.pg-maintenance-op .btn').on('keyup', (e)=>{
switch(e.keyCode) {
case 32: /* Spacebar click */
$(e.currentTarget).trigger('click');
break;
}
});
return this;
},
}),
},
{
type: 'nested',
@@ -1,4 +0,0 @@
.btn-group.pg-maintenance-op label.btn.btn-primary.active {
background-color: $color-primary-light;
color: $color-primary;
}
@@ -276,6 +276,7 @@ define('tools.querytool', [
height: '100%',
isCloseable: true,
isPrivate: true,
isLayoutMember: false,
content: '<div id ="geometry_viewer_panel" class="sql-editor-geometry-viewer" tabindex: "0"></div>',
});
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -428,7 +428,7 @@ class Preferences(object):
assert _type is not None, "Type for a preference cannot be none!"
assert _type in (
'boolean', 'integer', 'numeric', 'date', 'datetime',
'options', 'multiline', 'switch', 'node', 'text',
'options', 'multiline', 'switch', 'node', 'text', 'radioModern',
'keyboardshortcut'
), "Type cannot be found in the defined list!"
@@ -0,0 +1,109 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2019, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import {pgBrowser} from 'pgadmin.browser.layout';
import 'wcdocker';
var wcDocker = window.wcDocker;
describe('layout related functions test', function() {
let menu_items = null;
let dummy_cache = [{
id: 2,
mid: 1,
module:'browser',
name:'lock_layout',
value: 'none',
}];
beforeEach(function(){
pgBrowser.preferences_cache = dummy_cache;
pgBrowser.docker = {
'lockLayout': ()=>{},
};
_.extend(pgBrowser,{
'menus': {
'file': {
'mnu_locklayout': {
'menu_items': [
{'name': 'mnu_lock_none', change_checked: ()=> {}},
{'name': 'mnu_lock_docking', change_checked: ()=> {}},
{'name': 'mnu_lock_full', change_checked: ()=> {}},
],
},
},
},
});
menu_items = pgBrowser.menus.file.mnu_locklayout.menu_items;
});
describe('for menu actions', function() {
beforeEach(function(){
spyOn(pgBrowser, 'lock_layout');
spyOn(pgBrowser, 'save_lock_layout');
});
it('mnu_lock_none', function() {
pgBrowser.mnu_lock_none();
expect(pgBrowser.lock_layout).toHaveBeenCalledWith(pgBrowser.docker, 'none');
expect(pgBrowser.save_lock_layout).toHaveBeenCalledWith('none');
});
it('mnu_lock_docking', function() {
pgBrowser.mnu_lock_docking();
expect(pgBrowser.lock_layout).toHaveBeenCalledWith(pgBrowser.docker, 'docking');
expect(pgBrowser.save_lock_layout).toHaveBeenCalledWith('docking');
});
it('mnu_lock_full', function() {
pgBrowser.mnu_lock_full();
expect(pgBrowser.lock_layout).toHaveBeenCalledWith(pgBrowser.docker, 'full');
expect(pgBrowser.save_lock_layout).toHaveBeenCalledWith('full');
});
});
describe('lock_layout', function() {
let change_checked_test= function(menu_name) {
for(let i=0; i<menu_items.length; i++) {
if(menu_items[i].name == menu_name) {
expect(menu_items[i].change_checked).toHaveBeenCalledWith(true);
} else {
expect(menu_items[i].change_checked).toHaveBeenCalledWith(false);
}
}
};
beforeEach(function(){
spyOn(pgBrowser.docker, 'lockLayout');
for(let i=0; i<menu_items.length; i++) {
spyOn(menu_items[i], 'change_checked');
}
});
it('none', function() {
pgBrowser.lock_layout(pgBrowser.docker, 'none');
expect(pgBrowser.docker.lockLayout).toHaveBeenCalledWith(wcDocker.LOCK_LAYOUT_LEVEL.NONE);
change_checked_test('mnu_lock_none');
});
it('docking', function() {
pgBrowser.lock_layout(pgBrowser.docker, 'docking');
expect(pgBrowser.docker.lockLayout).toHaveBeenCalledWith(wcDocker.LOCK_LAYOUT_LEVEL.PREVENT_DOCKING);
change_checked_test('mnu_lock_docking');
});
it('full', function() {
pgBrowser.lock_layout(pgBrowser.docker, 'full');
expect(pgBrowser.docker.lockLayout).toHaveBeenCalledWith(wcDocker.LOCK_LAYOUT_LEVEL.FULL);
change_checked_test('mnu_lock_full');
});
});
});
+1
View File
@@ -190,6 +190,7 @@ var webpackShimConfig = {
'pgadmin.browser.error': path.join(__dirname, './pgadmin/browser/static/js/error'),
'pgadmin.browser.frame': path.join(__dirname, './pgadmin/browser/static/js/frame'),
'pgadmin.browser.keyboard': path.join(__dirname, './pgadmin/browser/static/js/keyboard'),
'pgadmin.browser.layout': path.join(__dirname, './pgadmin/browser/static/js/layout'),
'pgadmin.browser.preferences': path.join(__dirname, './pgadmin/browser/static/js/preferences'),
'pgadmin.browser.menu': path.join(__dirname, './pgadmin/browser/static/js/menu'),
'pgadmin.browser.messages': '/browser/js/messages',
+2
View File
@@ -72,6 +72,7 @@ module.exports = {
alias: {
'top': path.join(__dirname, './pgadmin'),
'jquery': path.join(__dirname, './node_modules/jquery/dist/jquery'),
'wcdocker': path.join(__dirname, './node_modules/webcabin-docker/Build/wcDocker'),
'alertify': path.join(__dirname, './node_modules/alertifyjs/build/alertify'),
'jquery.event.drag': path.join(__dirname, './node_modules/slickgrid/lib/jquery.event.drag-2.3.0'),
'jquery.ui': path.join(__dirname, './node_modules/slickgrid/lib/jquery-ui-1.11.3'),
@@ -100,6 +101,7 @@ module.exports = {
'pgadmin.backform': sourcesDir + '/js/backform.pgadmin',
'pgbrowser': path.resolve(__dirname, 'regression/javascript/fake_browser'),
'pgadmin.schema.dir': path.resolve(__dirname, 'pgadmin/browser/server_groups/servers/databases/schemas/static/js'),
'pgadmin.browser.layout': path.join(__dirname, './pgadmin/browser/static/js/layout'),
'pgadmin.browser.preferences': path.join(__dirname, './pgadmin/browser/static/js/preferences'),
'bundled_codemirror': path.join(__dirname, './pgadmin/static/bundle/codemirror'),
},
+2 -2
View File
@@ -8508,9 +8508,9 @@ watchpack@^1.5.0:
graceful-fs "^4.1.2"
neo-async "^2.5.0"
"webcabin-docker@git+https://github.com/EnterpriseDB/wcDocker/#e191a1063c111d5542ff26a7c163940be180333d":
"webcabin-docker@git+https://github.com/EnterpriseDB/wcDocker/#f3c79e4b9a9e76a8e34736fc33473e65c524050c":
version "2.2.4-dev"
resolved "git+https://github.com/EnterpriseDB/wcDocker/#e191a1063c111d5542ff26a7c163940be180333d"
resolved "git+https://github.com/EnterpriseDB/wcDocker/#f3c79e4b9a9e76a8e34736fc33473e65c524050c"
dependencies:
FileSaver "^0.10.0"
font-awesome "^4.7.0"