This is a big one campers; Add initial support for treeview nodes.

This commit adds the following:

- Storage of server groups in the configuration database
- Creation of a default server group on in the database
- A mechanism for plugging in treeview node types
- A node type for server groups with:
  - Treeview display
  - Custom per-node javascript implementing a menu option/dialogue to add new groups
  - Custom per-node CSS to style the treeview node
- JSON formatted data in response to AJAX requests, including:
  - Success/failure indication
  - Error message
  - Extra info (e.g. stack trace)
  - The original request data
  - Additional return data, e.g. node ID and label etc.
This commit is contained in:
Dave Page 2015-02-15 17:10:53 -05:00
parent e2832351ed
commit 89cc11fb80
20 changed files with 415 additions and 48 deletions

View File

@ -39,6 +39,10 @@ APP_VERSION = '%s.%s.%s-%s' % (APP_MAJOR, APP_MINOR, APP_REVISION, APP_SUFFIX)
# List of modules to skip when dynamically loading
MODULE_BLACKLIST = [ 'test' ]
# DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING!
# List of treeview browser nodes to skip when dynamically loading
NODE_BLACKLIST = [ ]
##########################################################################
# Log settings
##########################################################################

View File

@ -103,10 +103,10 @@ def create_app(app_name=config.APP_NAME):
# Looks like a module, so import it, and register the blueprint if present
# We rely on the ordering of syspath to ensure we actually get the right
# module here. Note that we also try to load the 'browser' module for
# the browser integration hooks.
# module here. Note that we also try to load the 'hooks' module for
# the browser integration hooks and other similar functions.
app.logger.info('Examining potential module: %s' % d)
module = __import__(f, globals(), locals(), ['browser', 'views'], -1)
module = __import__(f, globals(), locals(), ['hooks', 'views'], -1)
# Add the module to the global module list
modules.append(module)
@ -118,6 +118,11 @@ def create_app(app_name=config.APP_NAME):
app.logger.debug(' - root_path: %s' % module.views.blueprint.root_path)
app.logger.debug(' - static_folder: %s' % module.views.blueprint.static_folder)
app.logger.debug(' - template_folder: %s' % module.views.blueprint.template_folder)
# Register any sub-modules
if 'hooks' in dir(module) and 'register_submodules' in dir(module.hooks):
app.logger.info('Registering sub-modules in %s' % f)
module.hooks.register_submodules(app)
##########################################################################
# Handle the desktop login

View File

@ -0,0 +1,11 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2014, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
# Define the global node list
nodes = [ ]

View File

@ -0,0 +1,51 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2014, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
"""Browser application hooks"""
import os, sys
import config
from . import nodes
def register_submodules(app):
"""Register any child node blueprints"""
path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'nodes')
sys.path.insert(0, path)
files = os.listdir(path)
for f in files:
d = os.path.join(path, f)
if os.path.isdir(d) and os.path.isfile(os.path.join(d, '__init__.py')):
if f in config.NODE_BLACKLIST:
app.logger.info('Skipping blacklisted node: %s' % f)
continue
# Looks like a node, so import it, and register the blueprint if present
# We rely on the ordering of syspath to ensure we actually get the right
# module here.
app.logger.info('Examining potential node: %s' % d)
node = __import__(f, globals(), locals(), ['hooks', 'views'], -1)
# Add the node to the global module list
nodes.append(node)
# Register the blueprint if present
if 'views' in dir(node) and 'blueprint' in dir(node.views):
app.logger.info('Registering blueprint node: %s' % f)
app.register_blueprint(node.views.blueprint)
app.logger.debug(' - root_path: %s' % node.views.blueprint.root_path)
app.logger.debug(' - static_folder: %s' % node.views.blueprint.static_folder)
app.logger.debug(' - template_folder: %s' % node.views.blueprint.template_folder)
# Register any sub-modules
if 'hooks' in dir(node) and 'register_submodules' in dir(node.hooks):
app.logger.info('Registering sub-modules in %s' % f)
node.hooks.register_submodules(app)

View File

View File

@ -0,0 +1,75 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2014, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
"""Integration hooks for server groups."""
from flask import url_for
from flask.ext.security import current_user
from pgadmin.settings.settings_model import db, ServerGroup
def get_nodes():
"""Return a JSON document listing the server groups for the user"""
groups = ServerGroup.query.filter_by(user_id=current_user.id)
value = ''
for group in groups:
value += '{"id":%d,"label":"%s","icon":"icon-server-group","inode":true},' % (group.id, group.name)
value = value[:-1]
return value
def get_file_menu_items():
"""Return a (set) of dicts of file menu items, with name, priority and URL."""
return [
{'name': 'Add a server group...', 'priority': 10, 'url': '#', 'onclick': 'add_server_group()'}
]
def get_script_snippets():
"""Return the script snippets needed to handle treeview node operations."""
script = """function add_server_group() {
var alert = alertify.prompt(
'Add a server group',
'Enter a name for the new server group',
'',
function(evt, value) { $.post("%s", { name: value })
.done(function(data) {
if (data.success == 0) {
report_error(data.errormsg, data.info);
} else {
var item = {
id: data.data.id,
label: data.data.name,
inode: true,
open: false,
icon: 'icon-server-group'
}
treeApi.append(null, {
itemData: item
});
}
})
},
function(evt, value) { }
);
alert.show();
}
""" % url_for('NODE-server-group.add')
return script
def get_css_snippets():
"""Return the CSS needed to display the treeview node image."""
css = ".icon-server-group {\n"
css += " background: url('%s') 0 0 no-repeat !important;\n" % \
url_for('NODE-server-group.static', filename='img/server-group.png')
css += "{"
return css

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 B

View File

@ -0,0 +1,64 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2015, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
"""Defines views for management of server groups"""
NODE_NAME = 'server-group'
NODE_PATH = '/browser/' + NODE_NAME
import traceback
from flask import Blueprint, Response, current_app, request
from flask.ext.security import current_user, login_required
from utils.ajax import make_json_result
from pgadmin.settings.settings_model import db, ServerGroup
import config
# Initialise the module
blueprint = Blueprint("NODE-" + NODE_NAME, __name__, static_folder='static', static_url_path='', template_folder='templates', url_prefix=NODE_PATH)
@blueprint.route('/add/', methods=['POST'])
@login_required
def add():
"""Add a server group node to the settings database"""
success = 1
errormsg = ''
data = { }
if request.form['name'] != '':
servergroup = ServerGroup(user_id=current_user.id, name=request.form['name'])
try:
db.session.add(servergroup)
db.session.commit()
except Exception as e:
success = 0
errormsg = e.message
else:
success = 0
errormsg = "No server group name was specified"
if success == 1:
data['id'] = servergroup.id
data['name'] = servergroup.name
value = make_json_result(success=success,
errormsg=errormsg,
info=traceback.format_exc(),
result=request.form,
data=data)
resp = Response(response=value,
status=200,
mimetype="text/json")
return resp

View File

@ -0,0 +1,39 @@
function report_error(message, info) {
text = '<div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">\
<div class="panel panel-default">\
<div class="panel-heading" role="tab" id="headingOne">\
<h4 class="panel-title">\
<a data-toggle="collapse" data-parent="#accordion" href="#collapseOne" aria-expanded="true" aria-controls="collapseOne">\
Error message\
</a>\
</h4>\
</div>\
<div id="collapseOne" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="headingOne">\
<div class="panel-body" style="overflow: scroll;">' + message + '</div>\
</div>\
</div>'
if (info != '') {
text += '<div class="panel panel-default">\
<div class="panel-heading" role="tab" id="headingTwo">\
<h4 class="panel-title">\
<a class="collapsed" data-toggle="collapse" data-parent="#accordion" href="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo">\
Additional info\
</a>\
</h4>\
</div>\
<div id="collapseTwo" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingTwo">\
<div class="panel-body" style="overflow: scroll;">' + info + '</div>\
</div>\
</div>\
</div>'
}
text += '</div>'
alertify.alert(
'An error has occurred',
text
)
}

View File

@ -153,17 +153,18 @@ $('#dependents a').click(function (e) {
// Syntax highlight the SQL Pane
var editor = CodeMirror.fromTextArea(document.getElementById("sql-textarea"), {
lineNumbers: true,
mode: "text/x-sql",
readOnly: true,
lineNumbers: true,
mode: "text/x-sql",
readOnly: true,
});
// Initialise the treeview
$('#tree').aciTree({
ajax: {
url: '/static/tree.json'
}
ajax: {
url: '{{ url_for('browser.get_nodes') }}'
},
});
var treeApi = $('#tree').aciTree('api');
</script>

View File

@ -66,8 +66,4 @@
{% include 'browser/body.html' %}
{% include 'browser/messages.html' %}
<script>
{{ js_code|safe }}
</script>
{% endblock %}

View File

@ -10,12 +10,13 @@
"""A blueprint module implementing the core pgAdmin browser."""
MODULE_NAME = 'browser'
from flask import Blueprint, current_app, render_template, url_for
from flask import Blueprint, Response, current_app, render_template, url_for
from flaskext.gravatar import Gravatar
from flask.ext.security import login_required
from flask.ext.login import current_user
from inspect import getmoduleinfo, getmembers
from . import nodes
from pgadmin import modules
from pgadmin.settings import get_setting
@ -24,9 +25,6 @@ import config
# Initialise the module
blueprint = Blueprint(MODULE_NAME, __name__, static_folder='static', template_folder='templates', url_prefix='/' + MODULE_NAME)
##########################################################################
# A test page
##########################################################################
@blueprint.route("/")
@login_required
def index():
@ -47,42 +45,48 @@ def index():
help_items = [ ]
stylesheets = [ ]
scripts = [ ]
modules_and_nodes = modules + nodes
# Add browser stylesheets
stylesheets.append(url_for('static', filename='css/codemirror/codemirror.css'))
stylesheets.append(url_for('browser.static', filename='css/browser.css'))
stylesheets.append(url_for('browser.static', filename='css/aciTree/css/aciTree.css'))
stylesheets.append(url_for('browser.browser_css'))
# Add browser scripts
scripts.append(url_for('static', filename='js/codemirror/codemirror.js'))
scripts.append(url_for('static', filename='js/codemirror/mode/sql.js'))
scripts.append(url_for('browser.static', filename='js/utils.js'))
scripts.append(url_for('browser.static', filename='js/aciTree/jquery.aciPlugin.min.js'))
scripts.append(url_for('browser.static', filename='js/aciTree/jquery.aciTree.dom.js'))
scripts.append(url_for('browser.static', filename='js/aciTree/jquery.aciTree.min.js'))
scripts.append(url_for('browser.browser_js'))
for module in modules:
for module in modules_and_nodes:
# Get the edit menu items
if 'browser' in dir(module) and 'get_file_menu_items' in dir(module.browser):
file_items.extend(module.browser.get_file_menu_items())
if 'hooks' in dir(module) and 'get_file_menu_items' in dir(module.hooks):
file_items.extend(module.hooks.get_file_menu_items())
# Get the edit menu items
if 'browser' in dir(module) and 'get_edit_menu_items' in dir(module.browser):
edit_items.extend(module.browser.get_edit_menu_items())
if 'hooks' in dir(module) and 'get_edit_menu_items' in dir(module.hooks):
edit_items.extend(module.hooks.get_edit_menu_items())
# Get the tools menu items
if 'browser' in dir(module) and 'get_tools_menu_items' in dir(module.browser):
tools_items.extend(module.browser.get_tools_menu_items())
if 'hooks' in dir(module) and 'get_tools_menu_items' in dir(module.hooks):
tools_items.extend(module.hooks.get_tools_menu_items())
# Get the help menu items
if 'browser' in dir(module) and 'get_help_menu_items' in dir(module.browser):
help_items.extend(module.browser.get_help_menu_items())
if 'hooks' in dir(module) and 'get_help_menu_items' in dir(module.hooks):
help_items.extend(module.hooks.get_help_menu_items())
# Get any stylesheets
if 'browser' in dir(module) and 'get_stylesheets' in dir(module.browser):
stylesheets += module.browser.get_stylesheets()
if 'hooks' in dir(module) and 'get_stylesheets' in dir(module.hooks):
stylesheets += module.hooks.get_stylesheets()
# Get any scripts
if 'browser' in dir(module) and 'get_scripts' in dir(module.browser):
scripts += module.browser.get_scripts()
if 'hooks' in dir(module) and 'get_scripts' in dir(module.hooks):
scripts += module.hooks.get_scripts()
file_items = sorted(file_items, key=lambda k: k['priority'])
edit_items = sorted(edit_items, key=lambda k: k['priority'])
@ -105,3 +109,61 @@ def index():
stylesheets = stylesheets,
scripts = scripts,
layout_settings = layout_settings)
@blueprint.route("/browser.js")
@login_required
def browser_js():
"""Render and return JS snippets from the nodes and modules."""
snippets = ''
modules_and_nodes = modules + nodes
for module in modules_and_nodes:
if 'hooks' in dir(module) and 'get_script_snippets' in dir(module.hooks):
snippets += module.hooks.get_script_snippets()
resp = Response(response=snippets,
status=200,
mimetype="application/javascript")
return resp
@blueprint.route("/browser.css")
@login_required
def browser_css():
"""Render and return CSS snippets from the nodes and modules."""
snippets = ''
modules_and_nodes = modules + nodes
for module in modules_and_nodes:
if 'hooks' in dir(module) and 'get_css_snippets' in dir(module.hooks):
snippets += module.hooks.get_css_snippets()
resp = Response(response=snippets,
status=200,
mimetype="text/css")
return resp
@blueprint.route("/root-nodes.json")
@login_required
def get_nodes():
"""Build a list of treeview nodes from the child modules."""
value = '['
for node in nodes:
if 'hooks' in dir(node) and 'get_nodes' in dir(node.hooks):
value += node.hooks.get_nodes() + ','
if value[-1:] == ',':
value = value[:-1]
value += ']'
resp = Response(response=value,
status=200,
mimetype="text/json")
return resp

View File

@ -10,10 +10,12 @@
"""Views for setting and storing configuration options."""
MODULE_NAME = 'settings'
import config
import traceback
from flask import Blueprint, Response, abort, request, render_template
from flask.ext.security import login_required
import config
from utils.ajax import make_json_result
from . import get_setting, store_setting
# Initialise the module
@ -33,16 +35,30 @@ def script():
def store(setting=None, value=None):
"""Store a configuration setting, or if this is a POST request and a
count value is present, store multiple settings at once."""
if request.method == 'POST':
if 'count' in request.form:
for x in range(int(request.form['count'])):
store_setting(request.form['setting%d' % (x+1)], request.form['value%d' % (x+1)])
else:
store_setting(request.form['setting'], request.form['value'])
else:
store_setting(setting, value)
success = 1
errorcode = 0
errormsg = ''
return ''
try:
if request.method == 'POST':
if 'count' in request.form:
for x in range(int(request.form['count'])):
store_setting(request.form['setting%d' % (x+1)], request.form['value%d' % (x+1)])
else:
store_setting(request.form['setting'], request.form['value'])
else:
store_setting(setting, value)
except Exception as e:
success = 0
errormsg = e.message
value = make_json_result(success=success, errormsg=errormsg, info=traceback.format_exc(), result=request.form)
resp = Response(response=value,
status=200,
mimetype="text/json")
return resp
@blueprint.route("/get", methods=['POST'])
@blueprint.route("/get/<setting>", methods=['GET'])
@ -53,14 +69,21 @@ def get(setting=None, default=None):
if request.method == 'POST':
setting = request.form['setting']
default = request.form['default']
success = 1
errorcode = 0
errormsg = ''
try:
value = get_setting(setting, default)
except:
return ''
except Exception as e:
success = 0
errormsg = e.message
value = make_json_result(success=success, errormsg=errormsg, info=traceback.format_exc(), result=request.form)
resp = Response(response=value,
status=200,
mimetype="text/plain")
mimetype="text/json")
return resp

View File

@ -15,6 +15,11 @@
color: #fff;
}
.alertify .ajs-content {
padding-left: 0 !important;
padding-right: 0 !important;
}
/* iFrames should have no border */
iframe {
border-width: 0;
@ -30,6 +35,13 @@ iframe {
padding-left: 0;
}
.icon-servers {
background: url('/static/servers.png') 0 0 no-repeat !important;
}
/* Alert info panel */
.alert-info-panel {
border: 2px solid #a1a1a1;
margin-top: 2em;
padding: 5px 5px;
background: #dddddd;
border-radius: 5px;
height: 8em;
overflow: scroll;
}

0
web/utils/__init__.py Normal file
View File

24
web/utils/ajax.py Normal file
View File

@ -0,0 +1,24 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2015, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
"""Utility functions for dealing with AJAX."""
import json
def make_json_result(success=1, errormsg='', info='', result={}, data={}):
"""Create a JSON response document describing the results of a request and
containing the data."""
doc = { }
doc['success'] = success
doc['errormsg'] = errormsg
doc['info'] = info
doc['result'] = result
doc['data'] = data
return json.dumps(doc)