Dashboards v1

pull/3/head
Dave Page 2016-05-05 16:42:16 +01:00
parent 07e0de5399
commit 0628ee0425
26 changed files with 7624 additions and 103 deletions

View File

@ -96,7 +96,7 @@ class ServerGroupView(NodeView):
for sg in ServerGroup.query.filter_by(
user_id=current_user.id
).order_by(name):
).order_by('name'):
res.append({
'id': sg.id,
'name': sg.name
@ -255,7 +255,7 @@ class ServerGroupView(NodeView):
for group in groups:
nodes.append(
self.generate_browser_node(
self.blueprint.generate_browser_node(
"%d" % (group.id), None,
group.name,
"icon-%s" % self.node_type,

View File

@ -333,8 +333,9 @@ class ServerNode(PGChildNodeView):
from pgadmin.utils.driver import get_driver
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid)
conn = manager.connection()
connected = conn.connected()
if conn.connected():
if connected:
for arg in (
'host', 'port', 'db', 'username', 'sslmode', 'role'
):
@ -373,11 +374,16 @@ class ServerNode(PGChildNodeView):
return make_json_response(
success=1,
data={
'id': server.id,
'gid': server.servergroup_id,
'icon': 'icon-server-not-connected'
}
node=self.blueprint.generate_browser_node(
"%d" % (server.id), server.servergroup_id,
server.name,
"icon-server-not-connected" if not connected else
"icon-{0}".format(manager.server_type),
True,
self.node_type,
connected=False,
server_type='pg' # default server type
)
)
def list(self, gid):
@ -490,7 +496,7 @@ class ServerNode(PGChildNodeView):
try:
server = Server(
user_id=current_user.id,
servergroup_id=gid,
servergroup_id=data[u'gid'] if u'gid' in data else gid,
name=data[u'name'],
host=data[u'host'],
port=data[u'port'],
@ -505,7 +511,7 @@ class ServerNode(PGChildNodeView):
return jsonify(
node=self.blueprint.generate_browser_node(
"%d" % (server.id), gid,
"%d" % (server.id), server.servergroup_id,
server.name,
"icon-server-not-connected",
True,

View File

@ -148,6 +148,7 @@ function($, _, S, pgAdmin, pgBrowser, alertify) {
},
model: pgAdmin.Browser.Node.Model.extend({
defaults: {
gid: undefined,
id: undefined,
name: null,
sslmode: 'prefer',
@ -157,8 +158,21 @@ function($, _, S, pgAdmin, pgBrowser, alertify) {
username: '{{ username }}',
role: null
},
// Default values!
initialize: function(attrs, args) {
var isNew = (_.size(attrs) === 0);
if (isNew) {
this.set({'gid': args.node_info['server-group']._id});
}
pgAdmin.Browser.Node.Model.prototype.initialize.apply(this, arguments);
},
schema: [{
id: 'id', label: '{{ _('ID') }}', type: 'int', mode: ['properties']
},{
id: 'gid', label: '{{ _('Server Group') }}', type: 'int',
control: 'node-list-by-id', node: 'server-group',
mode: ['create', 'edit'], select2: {allowClear: false}
},{
id: 'name', label:'{{ _('Name') }}', type: 'text',
mode: ['properties', 'edit', 'create']

View File

@ -394,8 +394,8 @@ function(require, $, _, S, Bootstrap, pgAdmin, alertify, CodeMirror) {
* We can remove it from the Browser.scripts object as
* these're about to be loaded.
*
* This will make sure that - we do check for the script for
* loading only once.
* This will make sure that we check for the script to be
* loaded only once.
*
*/
delete obj.scripts[d._type];
@ -405,7 +405,7 @@ function(require, $, _, S, Bootstrap, pgAdmin, alertify, CodeMirror) {
if (!s.loaded) {
require([s.name], function(m) {
s.loaded = true;
// Call the initialize (if present)
// Call the initializer (if present)
if (m && m.init && typeof m.init == 'function') {
try {
m.init();

View File

@ -10,14 +10,20 @@
"""A blueprint module implementing the dashboard frame."""
MODULE_NAME = 'dashboard'
from functools import wraps
from config import PG_DEFAULT_DRIVER
from flask import render_template, url_for, Response
from flask.ext.babel import gettext
from flask.ext.security import login_required
from pgadmin.utils import PgAdminModule
from pgadmin.utils.ajax import make_response as ajax_response, internal_server_error
from pgadmin.utils.ajax import precondition_required
from pgadmin.utils.driver import get_driver
from pgadmin.utils.menu import Panel
from pgadmin.utils.preferences import Preferences
server_info = {}
class DashboardModule(PgAdminModule):
@ -34,6 +40,16 @@ class DashboardModule(PgAdminModule):
'when': None
}]
def get_own_stylesheets(self):
"""
Returns:
list: the stylesheets used by this module.
"""
stylesheets = [
url_for('dashboard.static', filename='css/dashboard.css')
]
return stylesheets
def get_panels(self):
return [
Panel(
@ -41,11 +57,60 @@ class DashboardModule(PgAdminModule):
priority=1,
title=gettext('Dashboard'),
icon='fa fa-tachometer',
content=url_for('dashboard.index'),
content='',
isCloseable=False,
isPrivate=True)
isPrivate=True,
isIframe=False)
]
def register_preferences(self):
"""
register_preferences
Register preferences for this module.
"""
# Register options for the PG and PPAS help paths
self.dashboard_preference = Preferences('dashboards', gettext('Dashboards'))
self.session_stats_refresh = self.dashboard_preference.register(
'dashboards', 'session_stats_refresh',
gettext("Session statistics refresh rate"), 'integer',
1, min_val=1, max_val=999999,
category_label=gettext('Graphs'),
help_str=gettext('The number of seconds between graph samples.')
)
self.session_stats_refresh = self.dashboard_preference.register(
'dashboards', 'tps_stats_refresh',
gettext("Transaction throughput refresh rate"), 'integer',
1, min_val=1, max_val=999999,
category_label=gettext('Graphs'),
help_str=gettext('The number of seconds between graph samples.')
)
self.session_stats_refresh = self.dashboard_preference.register(
'dashboards', 'ti_stats_refresh',
gettext("Tuples in refresh rate"), 'integer',
1, min_val=1, max_val=999999,
category_label=gettext('Graphs'),
help_str=gettext('The number of seconds between graph samples.')
)
self.session_stats_refresh = self.dashboard_preference.register(
'dashboards', 'to_stats_refresh',
gettext("Tuples out refresh rate"), 'integer',
1, min_val=1, max_val=999999,
category_label=gettext('Graphs'),
help_str=gettext('The number of seconds between graph samples.')
)
self.session_stats_refresh = self.dashboard_preference.register(
'dashboards', 'bio_stats_refresh',
gettext("Block I/O statistics refresh rate"), 'integer',
1, min_val=1, max_val=999999,
category_label=gettext('Graphs'),
help_str=gettext('The number of seconds between graph samples.')
)
blueprint = DashboardModule(MODULE_NAME, __name__)
@ -58,21 +123,28 @@ def check_precondition(f):
"""
@wraps(f)
def wrap(**kwargs):
def wrap(*args, **kwargs):
# Here args[0] will hold self & kwargs will hold gid,sid,did
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(
server_info.clear()
server_info['manager'] = get_driver(
PG_DEFAULT_DRIVER).connection_manager(
kwargs['sid']
)
conn = manager.connection(did=kwargs['did'] if 'did' in kwargs and kwargs['did'] != 0 else None)
server_info['conn'] = server_info['manager'].connection()
# If DB not connected then return error to browser
if not conn.connected():
if not server_info['conn'].connected():
return precondition_required(
gettext(
"Connection to the server has been lost!"
)
gettext("Connection to the server has been lost!")
)
return f(obj, **kwargs)
# Set template path for sql scripts
server_info['server_type'] = server_info['manager'].server_type
server_info['version'] = server_info['manager'].version
server_info['template_path'] = 'dashboard/sql/9.1_plus'
return f(*args, **kwargs)
return wrap
@ -91,10 +163,193 @@ def script():
@blueprint.route('/<int:sid>/<int:did>')
@login_required
def index(sid=None, did=None):
"""
Renders the welcome, server or database dashboard
Args:
sid: Server ID
did: Database ID
Returns: Welcome/Server/database dashboard
"""
rates = {}
prefs = Preferences.module('dashboards')
session_stats_refresh_pref = prefs.preference('session_stats_refresh')
rates['session_stats_refresh'] = session_stats_refresh_pref.get()
tps_stats_refresh_pref = prefs.preference('tps_stats_refresh')
rates['tps_stats_refresh'] = tps_stats_refresh_pref.get()
ti_stats_refresh_pref = prefs.preference('ti_stats_refresh')
rates['ti_stats_refresh'] = ti_stats_refresh_pref.get()
to_stats_refresh_pref = prefs.preference('to_stats_refresh')
rates['to_stats_refresh'] = to_stats_refresh_pref.get()
bio_stats_refresh_pref = prefs.preference('bio_stats_refresh')
rates['bio_stats_refresh'] = bio_stats_refresh_pref.get()
# Show the appropriate dashboard based on the identifiers passed to us
if sid is None and did is None:
return render_template('/dashboard/welcome_dashboard.html')
if did is None:
return render_template('/dashboard/server_dashboard.html', sid=sid)
return render_template('/dashboard/server_dashboard.html', sid=sid, rates=rates)
else:
return render_template('/dashboard/database_dashboard.html', sid=sid, did=did)
return render_template('/dashboard/database_dashboard.html', sid=sid, did=did, rates=rates)
def get_data(sid, did, template):
"""
Generic function to get server stats based on an SQL template
Args:
sid: The server ID
did: The database ID
template: The SQL template name
Returns:
"""
# Allow no server ID to be specified (so we can generate a route in JS)
# but throw an error if it's actually called.
if not sid:
return internal_server_error(errormsg='Server ID not specified.')
# Get the db connection
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid)
conn = manager.connection()
sql = render_template(
"/".join([server_info['template_path'], template]), did=did
)
status, res = conn.execute_dict(sql)
if not status:
return internal_server_error(errormsg=res)
return ajax_response(
response=res['rows'],
status=200
)
@blueprint.route('/session_stats/')
@blueprint.route('/session_stats/<int:sid>')
@blueprint.route('/session_stats/<int:sid>/<int:did>')
@login_required
@check_precondition
def session_stats(sid=None, did=None):
"""
This function returns server session statistics
:param sid: server id
:return:
"""
return get_data(sid, did, 'session_stats.sql')
@blueprint.route('/tps_stats/')
@blueprint.route('/tps_stats/<int:sid>')
@blueprint.route('/tps_stats/<int:sid>/<int:did>')
@login_required
@check_precondition
def tps_stats(sid=None, did=None):
"""
This function returns server TPS throughput
:param sid: server id
:return:
"""
return get_data(sid, did, 'tps_stats.sql')
@blueprint.route('/ti_stats/')
@blueprint.route('/ti_stats/<int:sid>')
@blueprint.route('/ti_stats/<int:sid>/<int:did>')
@login_required
@check_precondition
def ti_stats(sid=None, did=None):
"""
This function returns server tuple input statistics
:param sid: server id
:return:
"""
return get_data(sid, did, 'ti_stats.sql')
@blueprint.route('/to_stats/')
@blueprint.route('/to_stats/<int:sid>')
@blueprint.route('/to_stats/<int:sid>/<int:did>')
@login_required
@check_precondition
def to_stats(sid=None, did=None):
"""
This function returns server tuple output statistics
:param sid: server id
:return:
"""
return get_data(sid, did, 'to_stats.sql')
@blueprint.route('/bio_stats/')
@blueprint.route('/bio_stats/<int:sid>')
@blueprint.route('/bio_stats/<int:sid>/<int:did>')
@login_required
@check_precondition
def bio_stats(sid=None, did=None):
"""
This function returns server block IO statistics
:param sid: server id
:return:
"""
return get_data(sid, did, 'bio_stats.sql')
@blueprint.route('/activity/')
@blueprint.route('/activity/<int:sid>')
@blueprint.route('/activity/<int:sid>/<int:did>')
@login_required
@check_precondition
def activity(sid=None, did=None):
"""
This function returns server activity information
:param sid: server id
:return:
"""
return get_data(sid, did, 'activity.sql')
@blueprint.route('/locks/')
@blueprint.route('/locks/<int:sid>')
@blueprint.route('/locks/<int:sid>/<int:did>')
@login_required
@check_precondition
def locks(sid=None, did=None):
"""
This function returns server lock information
:param sid: server id
:return:
"""
return get_data(sid, did, 'locks.sql')
@blueprint.route('/prepared/')
@blueprint.route('/prepared/<int:sid>')
@blueprint.route('/prepared/<int:sid>/<int:did>')
@login_required
@check_precondition
def prepared(sid=None, did=None):
"""
This function returns prepared XACT information
:param sid: server id
:return:
"""
return get_data(sid, did, 'prepared.sql')
@blueprint.route('/config/')
@blueprint.route('/config/<int:sid>')
@login_required
@check_precondition
def config(sid=None):
"""
This function returns server config information
:param sid: server id
:return:
"""
return get_data(sid, None, 'config.sql')

View File

@ -0,0 +1,55 @@
.dashboard-container {
padding: 15px;
}
.dashboard-header-spacer {
padding-top: 15px;
}
.dashboard-link {
text-align: center;
}
.dashboard-icon {
color: black;
}
.dashboard-tab-container {
border-left: 1px solid #E2E2E2;
border-right: 1px solid #E2E2E2;
border-bottom: 1px solid #E2E2E2;
}
.dashboard-tab-panel > li > a {
padding: 0px 15px !important;
}
.dashboard-tab {
border: 0px;
border-radius: 4px 4px 0px 0px;
margin-right: 1px;
font-size: 13px;
line-height: 25px;
margin-top: 5px;
}
.dashboard-tab.active {
margin-top: 0px;
line-height: 30px;
}
.graph-container {
margin-top: 10px;
height: 150px;
}
.graph-error {
background-color: #E2E2E2;
padding-top: 20px
}
.grid-error {
background-color: #E2E2E2;
padding-top: 20px;
padding-bottom: 40px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -1,4 +1,89 @@
<h1>Server Dashboard</h1>
<div class="dashboard-container">
<div class="row">
<div class="col-xs-6">
<div class="obj_properties">
<legend class="badge">{{ gettext('Database sessions') }}</legend>
</div>
</div>
<div class="col-xs-6">
<div class="obj_properties">
<legend class="badge">{{ gettext('Transactions per second') }}</legend>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-6">
<div id="graph-sessions" class="graph-container"></div>
</div>
<div class="col-xs-6">
<div id="graph-tps" class="graph-container"></div>
</div>
</div>
<b>Server ID: </b>{{ sid }}<br />
<b>Database ID: </b>{{ did }}
<div class="row dashboard-header-spacer">
<div class="col-xs-4">
<div class="obj_properties">
<legend class="badge">{{ gettext('Tuples in') }}</legend>
</div>
</div>
<div class="col-xs-4">
<div class="obj_properties">
<legend class="badge">{{ gettext('Tuples out') }}</legend>
</div>
</div>
<div class="col-xs-4">
<div class="obj_properties">
<legend class="badge">{{ gettext('Block I/O') }}</legend>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-4">
<div id="graph-ti" class="graph-container"></div>
</div>
<div class="col-xs-4">
<div id="graph-to" class="graph-container"></div>
</div>
<div class="col-xs-4">
<div id="graph-bio" class="graph-container"></div>
</div>
</div>
<div class="row dashboard-header-spacer">
<div class="col-xs-12">
<div class="obj_properties">
<legend class="badge">{{ gettext('Database activity') }}</legend>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<div class="dashboard-tab-container">
<!-- Nav tabs -->
<ul class="nav nav-tabs dashboard-tab-panel" role="tablist">
<li role="presentation" class="active dashboard-tab"><a href="#tab_panel_database_activity" aria-controls="tab_database_activity" role="tab" data-toggle="tab">Sessions</a></li>
<li role="presentation" class="dashboard-tab"><a href="#tab_panel_database_locks" aria-controls="tab_database_locks" role="tab" data-toggle="tab">Locks</a></li>
<li role="presentation" class="dashboard-tab"><a href="#tab_panel_database_prepared" aria-controls="tab_database_prepared" role="tab" data-toggle="tab">Prepared Transactions</a></li>
</ul>
<!-- Tab panes -->
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="tab_panel_database_activity">
<div id="database_activity" class="grid-container"></div>
</div>
<div role="tabpanel" class="tab-pane" id="tab_panel_database_locks">
<div id="database_locks" class="grid-container"></div>
</div>
<div role="tabpanel" class="tab-pane" id="tab_panel_database_prepared">
<div id="database_prepared" class="grid-container"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript">
pgAdmin.Dashboard.init_database_dashboard({{ sid }}, {{ did }}, {{ rates.session_stats_refresh }}, {{ rates.tps_stats_refresh }}, {{ rates.ti_stats_refresh }}, {{ rates.to_stats_refresh }}, {{ rates.bio_stats_refresh }} );
</script>

View File

@ -1,84 +1,783 @@
define(['jquery', 'pgadmin', 'underscore', 'wcdocker', 'pgadmin.browser', 'bootstrap'],
function($, pgAdmin, _) {
define([
'require', 'jquery', 'pgadmin', 'underscore', 'backbone', 'flotr2', 'wcdocker',
'pgadmin.browser', 'bootstrap'
],
function(r, $, pgAdmin, _, Backbone) {
var wcDocker = window.wcDocker,
pgBrowser = pgAdmin.Browser;
var wcDocker = window.wcDocker,
pgBrowser = pgAdmin.Browser;
/* Return back, this has been called more than once */
if (pgAdmin.Dashboard)
return;
/* Return back, this has been called more than once */
if (pgAdmin.Dashboard)
return;
pgAdmin.Dashboard = {
init: function() {
if (this.initialized)
return;
pgAdmin.Dashboard = {
init: function() {
if (this.initialized)
return;
this.initialized = true;
this.initialized = true;
// Bind the Dashboard object with the 'object_selected' function
var selected = this.object_selected.bind(this);
// Bind the Dashboard object with the 'object_selected' function
var selected = this.object_selected.bind(this);
// Listen for selection of any of object
pgBrowser.Events.on('pgadmin-browser:tree:selected', selected);
},
// Listen for selection of any of object
pgBrowser.Events.on('pgadmin-browser:tree:selected', selected);
object_selected: function(item, itemData, node) {
var treeHierarchy = node.getTreeNodeHierarchy(item)
if (itemData && itemData._type)
{
switch(itemData._type) {
case ('server-group'):
url = '{{ url_for('dashboard.index') }}';
break;
// Load the default welcome dashboard
url = '{{ url_for('dashboard.index') }}';
case ('server'):
case ('coll-database'):
case ('coll-role'):
case ('role'):
case ('coll-tablespace'):
case ('tablespace'):
url = '{{ url_for('dashboard.index') }}'
+ treeHierarchy.server._id;
break;
var dashboardPanel = pgBrowser.panels['dashboard'].panel;
if (dashboardPanel) {
var div = dashboardPanel.layout().scene().find('.pg-panel-content');
default:
url = '{{ url_for('dashboard.index') }}'
+ treeHierarchy.server._id
+ '/' + treeHierarchy.database._id;
break;
}
}
if (div) {
$.ajax({
url: url,
type: "GET",
dataType: "html",
success: function (data) {
$(div).html(data);
},
error: function (xhr, status) {
$(div).html(
'<div class="alert alert-danger pg-panel-message" role="alert">{{ gettext('An error occurred whilst loading the dashboard.') }}</div>'
);
}
});
var dashboardPanel = pgBrowser.frames['dashboard'].panel;
if (dashboardPanel) {
var frame = $(dashboardPanel).data('embeddedFrame');
// Cache the current IDs for next time
$(dashboardPanel).data('sid', -1)
$(dashboardPanel).data('did', -1)
}
}
},
if (frame) {
// Avoid unnecessary reloads
if (_.isUndefined(treeHierarchy.server) || _.isUndefined(treeHierarchy.server._id))
sid = -1
else
sid = treeHierarchy.server._id
// Handle treeview clicks
object_selected: function(item, itemData, node) {
var treeHierarchy = node.getTreeNodeHierarchy(item)
if (itemData && itemData._type)
{
switch(itemData._type) {
case ('server-group'):
url = '{{ url_for('dashboard.index') }}';
break;
if (_.isUndefined(treeHierarchy.database) || _.isUndefined(treeHierarchy.database._id))
did = -1
else
did = treeHierarchy.database._id
case ('server'):
case ('coll-database'):
case ('coll-role'):
case ('role'):
case ('coll-tablespace'):
case ('tablespace'):
url = '{{ url_for('dashboard.index') }}'
+ treeHierarchy.server._id;
break;
if (sid != $(dashboardPanel).data('sid') ||
did != $(dashboardPanel).data('did')) {
frame.openURL(url);
default:
url = '{{ url_for('dashboard.index') }}'
+ treeHierarchy.server._id
+ '/' + treeHierarchy.database._id;
break;
}
}
// Cache the current IDs for next time
$(dashboardPanel).data('sid', sid)
$(dashboardPanel).data('did', did)
}
}
}
}
};
var dashboardPanel = pgBrowser.panels['dashboard'].panel;
if (dashboardPanel) {
var div = dashboardPanel.layout().scene().find('.pg-panel-content');
return pgAdmin.Dashboard;
if (div) {
// Avoid unnecessary reloads
if (_.isUndefined(treeHierarchy.server) || _.isUndefined(treeHierarchy.server._id))
sid = -1
else
sid = treeHierarchy.server._id
if (_.isUndefined(treeHierarchy.database) || _.isUndefined(treeHierarchy.database._id))
did = -1
else
did = treeHierarchy.database._id
if (sid != $(dashboardPanel).data('sid') ||
did != $(dashboardPanel).data('did')) {
// Clear out everything so any existing timers die off
$(div).empty();
$.ajax({
url: url,
type: "GET",
dataType: "html",
success: function (data) {
$(div).html(data);
},
error: function (xhr, status) {
$(div).html(
'<div class="alert alert-danger pg-panel-message" role="alert">{{ gettext('An error occurred whilst loading the dashboard.') }}</div>'
);
}
});
// Cache the current IDs for next time
$(dashboardPanel).data('sid', sid)
$(dashboardPanel).data('did', did)
}
}
}
},
// Render a chart
render_chart: function(container, data, dataset, sid, did, url, options, counter, refresh) {
// Data format:
// [
// { data: [[0, y0], [1, y1]...], label: 'Label 1', [options] },
// { data: [[0, y0], [1, y1]...], label: 'Label 2', [options] },
// { data: [[0, y0], [1, y1]...], label: 'Label 3', [options] }
// ]
y = 0;
if (dataset.length == 0) {
if (counter == true)
{
// Have we stashed initial values?
if (_.isUndefined($(container).data('counter_previous_vals'))) {
$(container).data('counter_previous_vals', data[0])
} else {
// Create the initial data structure
for (var x in data[0]) {
dataset.push({ 'data': [[0, data[0][x] - $(container).data('counter_previous_vals')[x]]], 'label': x });
}
}
} else {
// Create the initial data structure
for (var x in data[0]) {
dataset.push({ 'data': [[0, data[0][x]]], 'label': x });
}
}
} else {
for (var x in data[0]) {
// Push new values onto the existing data structure
// If this is a counter stat, we need to subtract the previous value
if (counter == false) {
dataset[y]['data'].unshift([0, data[0][x]]);
} else {
// Store the current value, minus the previous one we stashed.
// It's possible the tab has been reloaded, in which case out previous values are gone
if (_.isUndefined($(container).data('counter_previous_vals')))
return
dataset[y]['data'].unshift([0, data[0][x] - $(container).data('counter_previous_vals')[x]]);
}
// Reset the time index to get a proper scrolling display
for (z = 0; z < dataset[y]['data'].length; z++) {
dataset[y]['data'][z][0] = z;
}
y++;
}
$(container).data('counter_previous_vals', data[0])
}
// Remove uneeded elements
for (x = 0; x < dataset.length; x++) {
// Remove old data points
if (dataset[x]['data'].length > 101) {
dataset[x]['data'].pop();
}
}
// Draw Graph, if the container still exists and has a size
var dashboardPanel = pgBrowser.panels['dashboard'].panel;
var div = dashboardPanel.layout().scene().find('.pg-panel-content');
if ($(div).find(container).length) { // Exists?
if (container.clientHeight > 0 && container.clientWidth > 0) { // Not hidden?
Flotr.draw(container, dataset, options);
}
} else {
return;
}
// Animate
var setTimeoutFunc = function () {
path = url + sid;
if (did != -1) {
path += '/' + did;
}
$.ajax({
url: path,
type: "GET",
dataType: "html",
success: function (resp) {
$(container).removeClass('graph-error')
data = JSON.parse(resp);
pgAdmin.Dashboard.render_chart(container, data, dataset, sid, did, url, options, counter, refresh);
},
error: function (xhr, status, msg) {
// If we get a 428, it means the server isn't connected
if (xhr.status == 428) {
msg = '{{ gettext('Please connect to the selected server to view the graph.') }}';
cls = 'info';
} else {
msg = '{{ gettext('An error occurred whilst rendering the graph.') }}';
cls = 'danger';
}
$(container).addClass('graph-error');
$(container).html(
'<div class="alert alert-' + cls + ' pg-panel-message" role="alert">' + msg + '</div>'
);
// Try again...
if (container.clientHeight > 0 && container.clientWidth > 0) {
setTimeout(setTimeoutFunc, refresh * 1000);
}
},
});
};
setTimeout(setTimeoutFunc, refresh * 1000);
},
// Handler function to support the "Add Server" link
add_new_server: function() {
if (pgBrowser && pgBrowser.tree) {
var i = pgBrowser.tree.first(null, false),
serverModule = r('pgadmin.node.server');
if (serverModule) {
serverModule.callbacks.show_obj_properties.apply(
serverModule, [{action: 'create'}, i]
);
}
}
},
// Render a grid
render_grid: function(container, sid, did, url, columns) {
var Datum = Backbone.Model.extend({});
path = url + sid;
if (did != -1) {
path += '/' + did;
}
var Data = Backbone.Collection.extend({
model: Datum,
url: path,
mode: "client"
});
var data = new Data();
// Set up the grid
var grid = new Backgrid.Grid({
columns: columns,
collection: data,
className: "backgrid table-bordered table-striped"
});
// Render the grid
$(container).append(grid.render().el)
// Initialize a client-side filter to filter on the client
// mode pageable collection's cache.
var filter = new Backgrid.Extension.ClientSideFilter({
collection: data
});
// Render the filter
$(container).before(filter.render().el);
// Add some space to the filter and move it to the right
$(filter.el).css({float: "right", margin: "5px", "margin-right": "0px"});
// Stash objects for future use
$(container).data('data', data);
$(container).data('grid', grid);
$(container).data('filter', filter);
},
// Render the data in a grid
render_grid_data: function(container) {
data = $(container).data('data');
grid = $(container).data('grid');
filter = $(container).data('filter');
data.fetch({
reset: true,
success: function() {
// If we're showing an error, remove it, and replace the grid & filter
if ($(container).hasClass('grid-error')) {
$(container).removeClass('grid-error');
$(container).html(grid.render().el)
$(filter.el).show();
}
// Re-apply search criteria
filter.search();
},
error: function(model, xhr, options) {
// If we get a 428, it means the server isn't connected
if (xhr.status == 428) {
msg = '{{ gettext('Please connect to the selected server to view the table.') }}';
cls = 'info';
} else {
msg = '{{ gettext('An error occurred whilst rendering the table.') }}';
cls = 'danger';
}
// Stash the old content, and replace with the error, if not already present
if (!$(container).hasClass('grid-error')) {
$(filter.el).hide();
$(container).addClass('grid-error');
$(container).html(
'<div class="alert alert-' + cls + ' pg-panel-message" role="alert">' + msg + '</div>'
);
}
// Try again
setTimeout(function() {
pgAdmin.Dashboard.render_grid_data(container, data);
}, 5000)
}
});
},
// Rock n' roll on the server dashboard
init_server_dashboard: function(sid, session_stats_refresh, tps_stats_refresh, ti_stats_refresh, to_stats_refresh, bio_stats_refresh) {
var div_sessions = document.getElementById('graph-sessions');
var div_tps = document.getElementById('graph-tps');
var div_ti = document.getElementById('graph-ti');
var div_to = document.getElementById('graph-to');
var div_bio = document.getElementById('graph-bio');
var div_server_activity = document.getElementById('server_activity');
var div_server_locks = document.getElementById('server_locks');
var div_server_prepared = document.getElementById('server_prepared');
var div_server_config = document.getElementById('server_config');
var dataset_sessions = [];
var data_sessions = [];
var dataset_tps = [];
var data_tps = [];
var dataset_ti = [];
var data_ti = [];
var dataset_to = [];
var data_to = [];
var dataset_bio = [];
var data_bio = [];
// Fake DB ID
did = -1;
var options_line = {
parseFloat: false,
xaxis: {
min: 100,
max: 0,
autoscale: 0
},
yaxis : {
autoscale: 1
},
legend : {
position : 'nw',
backgroundColor : '#D2E8FF'
}
}
var server_activity_columns = [{
name: "pid",
label: "{{ _('Process ID') }}",
editable: false,
cell: "string"
}, {
name: "datname",
label: "{{ _('Database') }}",
editable: false,
cell: "string"
}, {
name: "usename",
label: "{{ _('User') }}",
editable: false,
cell: "string"
}, {
name: "application_name",
label: "{{ _('Application name') }}",
editable: false,
cell: "string"
}, {
name: "client_addr",
label: "{{ _('Client address') }}",
editable: false,
cell: "string"
}, {
name: "backend_start",
label: "{{ _('Backend start') }}",
editable: false,
cell: "string"
}, {
name: "state",
label: "{{ _('State') }}",
editable: false,
cell: "string"
}];
var server_locks_columns = [{
name: "pid",
label: "{{ _('Process ID') }}",
editable: false,
cell: "string"
}, {
name: "datname",
label: "{{ _('Database') }}",
editable: false,
cell: "string"
}, {
name: "locktype",
label: "{{ _('Lock type') }}",
editable: false,
cell: "string"
}, {
name: "relation",
label: "{{ _('Target relation') }}",
editable: false,
cell: "string"
}, {
name: "page",
label: "{{ _('Page') }}",
editable: false,
cell: "string"
}, {
name: "tuple",
label: "{{ _('Tuple') }}",
editable: false,
cell: "string"
}, {
name: "virtualxid",
label: "{{ _('Virtual XID (target)') }}",
editable: false,
cell: "string"
}, {
name: "transactionid",
label: "{{ _('XID (target)') }}",
editable: false,
cell: "string"
},{
name: "classid",
label: "{{ _('Class') }}",
editable: false,
cell: "string"
},{
name: "objid",
label: "{{ _('Object ID') }}",
editable: false,
cell: "string"
},{
name: "virtualtransaction",
label: "{{ _('Virtual XID (owner)') }}",
editable: false,
cell: "string"
},{
name: "mode",
label: "{{ _('Mode') }}",
editable: false,
cell: "string"
},{
name: "granted",
label: "{{ _('Granted?') }}",
editable: false,
cell: "string"
},{
name: "fastpath",
label: "{{ _('Fast path?') }}",
editable: false,
cell: "string"
}];
var server_prepared_columns = [{
name: "git",
label: "{{ _('Name') }}",
editable: false,
cell: "string"
}, {
name: "database",
label: "{{ _('Database') }}",
editable: false,
cell: "string"
}, {
name: "Owner",
label: "{{ _('Owner') }}",
editable: false,
cell: "string"
}, {
name: "transaction",
label: "{{ _('XID') }}",
editable: false,
cell: "string"
}, {
name: "prepared",
label: "{{ _('Prepared at') }}",
editable: false,
cell: "string"
}];
var server_config_columns = [{
name: "name",
label: "{{ _('Name') }}",
editable: false,
cell: "string"
}, {
name: "category",
label: "{{ _('Category') }}",
editable: false,
cell: "string"
}, {
name: "setting",
label: "{{ _('Setting') }}",
editable: false,
cell: "string"
}, {
name: "unit",
label: "{{ _('Unit') }}",
editable: false,
cell: "string"
}, {
name: "short_desc",
label: "{{ _('Description') }}",
editable: false,
cell: "string"
}];
// Render the graphs
pgAdmin.Dashboard.render_chart(div_sessions, data_sessions, dataset_sessions, sid, did, '{{ url_for('dashboard.session_stats') }}', options_line, false, session_stats_refresh);
pgAdmin.Dashboard.render_chart(div_tps, data_tps, dataset_tps, sid, did, '{{ url_for('dashboard.tps_stats') }}', options_line, true, tps_stats_refresh);
pgAdmin.Dashboard.render_chart(div_ti, data_ti, dataset_ti, sid, did, '{{ url_for('dashboard.ti_stats') }}', options_line, true, ti_stats_refresh);
pgAdmin.Dashboard.render_chart(div_to, data_to, dataset_to, sid, did, '{{ url_for('dashboard.to_stats') }}', options_line, true, to_stats_refresh);
pgAdmin.Dashboard.render_chart(div_bio, data_bio, dataset_bio, sid, did, '{{ url_for('dashboard.bio_stats') }}', options_line, true, bio_stats_refresh);
// Render the tabs, but only get data for the activity tab for now
pgAdmin.Dashboard.render_grid(div_server_activity, sid, did, '{{ url_for('dashboard.activity') }}', server_activity_columns);
pgAdmin.Dashboard.render_grid(div_server_locks, sid, did, '{{ url_for('dashboard.locks') }}', server_locks_columns);
pgAdmin.Dashboard.render_grid(div_server_prepared, sid, did, '{{ url_for('dashboard.prepared') }}', server_prepared_columns);
pgAdmin.Dashboard.render_grid(div_server_config, sid, did, '{{ url_for('dashboard.config') }}', server_config_columns);
pgAdmin.Dashboard.render_grid_data(div_server_activity);
// (Re)render the appropriate tab
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
switch ($(e.target).attr('aria-controls')) {
case "tab_server_activity":
pgAdmin.Dashboard.render_grid_data(div_server_activity);
break;
case "tab_server_locks":
pgAdmin.Dashboard.render_grid_data(div_server_locks);
break;
case "tab_server_prepared":
pgAdmin.Dashboard.render_grid_data(div_server_prepared);
break;
case "tab_server_config":
pgAdmin.Dashboard.render_grid_data(div_server_config);
break;
}
});
},
// Rock n' roll on the database dashboard
init_database_dashboard: function(sid, did, session_stats_refresh, tps_stats_refresh, ti_stats_refresh, to_stats_refresh, bio_stats_refresh) {
var div_sessions = document.getElementById('graph-sessions');
var div_tps = document.getElementById('graph-tps');
var div_ti = document.getElementById('graph-ti');
var div_to = document.getElementById('graph-to');
var div_bio = document.getElementById('graph-bio');
var div_database_activity = document.getElementById('database_activity');
var div_database_locks = document.getElementById('database_locks');
var div_database_prepared = document.getElementById('database_prepared');
var dataset_sessions = [];
var data_sessions = [];
var dataset_tps = [];
var data_tps = [];
var dataset_ti = [];
var data_ti = [];
var dataset_to = [];
var data_to = [];
var dataset_bio = [];
var data_bio = [];
var options_line = {
parseFloat: false,
xaxis: {
min: 100,
max: 0,
autoscale: 0
},
yaxis : {
autoscale: 1
},
legend : {
position : 'nw',
backgroundColor : '#D2E8FF'
}
}
var database_activity_columns = [{
name: "pid",
label: "{{ _('Process ID') }}",
editable: false,
cell: "string"
}, {
name: "usename",
label: "{{ _('User') }}",
editable: false,
cell: "string"
}, {
name: "application_name",
label: "{{ _('Application name') }}",
editable: false,
cell: "string"
}, {
name: "client_addr",
label: "{{ _('Client address') }}",
editable: false,
cell: "string"
}, {
name: "backend_start",
label: "{{ _('Backend start') }}",
editable: false,
cell: "string"
}, {
name: "state",
label: "{{ _('State') }}",
editable: false,
cell: "string"
}];
var database_locks_columns = [{
name: "pid",
label: "{{ _('Process ID') }}",
editable: false,
cell: "string"
}, {
name: "locktype",
label: "{{ _('Lock type') }}",
editable: false,
cell: "string"
}, {
name: "relation",
label: "{{ _('Target relation') }}",
editable: false,
cell: "string"
}, {
name: "page",
label: "{{ _('Page') }}",
editable: false,
cell: "string"
}, {
name: "tuple",
label: "{{ _('Tuple') }}",
editable: false,
cell: "string"
}, {
name: "virtualxid",
label: "{{ _('Virtual XID (target)') }}",
editable: false,
cell: "string"
}, {
name: "transactionid",
label: "{{ _('XID (target)') }}",
editable: false,
cell: "string"
},{
name: "classid",
label: "{{ _('Class') }}",
editable: false,
cell: "string"
},{
name: "objid",
label: "{{ _('Object ID') }}",
editable: false,
cell: "string"
},{
name: "virtualtransaction",
label: "{{ _('Virtual XID (owner)') }}",
editable: false,
cell: "string"
},{
name: "mode",
label: "{{ _('Mode') }}",
editable: false,
cell: "string"
},{
name: "granted",
label: "{{ _('Granted?') }}",
editable: false,
cell: "string"
},{
name: "fastpath",
label: "{{ _('Fast path?') }}",
editable: false,
cell: "string"
}];
var database_prepared_columns = [{
name: "git",
label: "{{ _('Name') }}",
editable: false,
cell: "string"
}, {
name: "Owner",
label: "{{ _('Owner') }}",
editable: false,
cell: "string"
}, {
name: "transaction",
label: "{{ _('XID') }}",
editable: false,
cell: "string"
}, {
name: "prepared",
label: "{{ _('Prepared at') }}",
editable: false,
cell: "string"
}];
// Render the graphs
pgAdmin.Dashboard.render_chart(div_sessions, data_sessions, dataset_sessions, sid, did, '{{ url_for('dashboard.session_stats') }}', options_line, false, session_stats_refresh);
pgAdmin.Dashboard.render_chart(div_tps, data_tps, dataset_tps, sid, did, '{{ url_for('dashboard.tps_stats') }}', options_line, true, tps_stats_refresh);
pgAdmin.Dashboard.render_chart(div_ti, data_ti, dataset_ti, sid, did, '{{ url_for('dashboard.ti_stats') }}', options_line, true, ti_stats_refresh);
pgAdmin.Dashboard.render_chart(div_to, data_to, dataset_to, sid, did, '{{ url_for('dashboard.to_stats') }}', options_line, true, to_stats_refresh);
pgAdmin.Dashboard.render_chart(div_bio, data_bio, dataset_bio, sid, did, '{{ url_for('dashboard.bio_stats') }}', options_line, true, bio_stats_refresh);
// Render the tabs, but only get data for the activity tab for now
pgAdmin.Dashboard.render_grid(div_database_activity, sid, did, '{{ url_for('dashboard.activity') }}', database_activity_columns);
pgAdmin.Dashboard.render_grid(div_database_locks, sid, did, '{{ url_for('dashboard.locks') }}', database_locks_columns);
pgAdmin.Dashboard.render_grid(div_database_prepared, sid, did, '{{ url_for('dashboard.prepared') }}', database_prepared_columns);
pgAdmin.Dashboard.render_grid_data(div_database_activity);
// (Re)render the appropriate tab
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
switch ($(e.target).attr('aria-controls')) {
case "tab_database_activity":
pgAdmin.Dashboard.render_grid_data(div_database_activity);
break;
case "tab_database_locks":
pgAdmin.Dashboard.render_grid_data(div_database_locks);
break;
case "tab_database_prepared":
pgAdmin.Dashboard.render_grid_data(div_database_prepared);
break;
}
});
}
};
return pgAdmin.Dashboard;
});

View File

@ -1,3 +1,93 @@
<h1>Server Dashboard</h1>
<div class="dashboard-container">
<div class="row">
<div class="col-xs-6">
<div class="obj_properties">
<legend class="badge">{{ gettext('Server sessions') }}</legend>
</div>
</div>
<div class="col-xs-6">
<div class="obj_properties">
<legend class="badge">{{ gettext('Transactions per second') }}</legend>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-6">
<div id="graph-sessions" class="graph-container"></div>
</div>
<div class="col-xs-6">
<div id="graph-tps" class="graph-container"></div>
</div>
</div>
<b>Server ID: </b>{{ sid }}<br />
<div class="row dashboard-header-spacer">
<div class="col-xs-4">
<div class="obj_properties">
<legend class="badge">{{ gettext('Tuples in') }}</legend>
</div>
</div>
<div class="col-xs-4">
<div class="obj_properties">
<legend class="badge">{{ gettext('Tuples out') }}</legend>
</div>
</div>
<div class="col-xs-4">
<div class="obj_properties">
<legend class="badge">{{ gettext('Block I/O') }}</legend>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-4">
<div id="graph-ti" class="graph-container"></div>
</div>
<div class="col-xs-4">
<div id="graph-to" class="graph-container"></div>
</div>
<div class="col-xs-4">
<div id="graph-bio" class="graph-container"></div>
</div>
</div>
<div class="row dashboard-header-spacer">
<div class="col-xs-12">
<div class="obj_properties">
<legend class="badge">{{ gettext('Server activity') }}</legend>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<div class="dashboard-tab-container">
<!-- Nav tabs -->
<ul class="nav nav-tabs dashboard-tab-panel" role="tablist">
<li role="presentation" class="active dashboard-tab"><a href="#tab_panel_server_activity" aria-controls="tab_server_activity" role="tab" data-toggle="tab">Sessions</a></li>
<li role="presentation" class="dashboard-tab"><a href="#tab_panel_server_locks" aria-controls="tab_server_locks" role="tab" data-toggle="tab">Locks</a></li>
<li role="presentation" class="dashboard-tab"><a href="#tab_panel_server_prepared" aria-controls="tab_server_prepared" role="tab" data-toggle="tab">Prepared Transactions</a></li>
<li role="presentation" class="dashboard-tab"><a href="#tab_panel_server_config" aria-controls="tab_server_config" role="tab" data-toggle="tab">Configuration</a></li>
</ul>
<!-- Tab panes -->
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="tab_panel_server_activity">
<div id="server_activity" class="grid-container"></div>
</div>
<div role="tabpanel" class="tab-pane" id="tab_panel_server_locks">
<div id="server_locks" class="grid-container"></div>
</div>
<div role="tabpanel" class="tab-pane" id="tab_panel_server_prepared">
<div id="server_prepared" class="grid-container"></div>
</div>
<div role="tabpanel" class="tab-pane" id="tab_panel_server_config">
<div id="server_config" class="grid-container"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript">
pgAdmin.Dashboard.init_server_dashboard({{ sid }}, {{ rates.session_stats_refresh }}, {{ rates.tps_stats_refresh }}, {{ rates.ti_stats_refresh }}, {{ rates.to_stats_refresh }}, {{ rates.bio_stats_refresh }} );
</script>

View File

@ -0,0 +1,13 @@
SELECT
pid,
datname,
usename,
application_name,
client_addr,
backend_start,
state
FROM
pg_stat_activity
{% if did %}WHERE
datname = (SELECT datname FROM pg_database WHERE oid = {{ did }}){% endif %}
ORDER BY pid

View File

@ -0,0 +1,3 @@
SELECT
(SELECT sum(blks_read) FROM pg_stat_database{% if did %} WHERE datname = (SELECT datname FROM pg_database WHERE oid = {{ did }}){% endif %}) AS "{{ _('Reads') }}",
(SELECT sum(blks_hit) FROM pg_stat_database{% if did %} WHERE datname = (SELECT datname FROM pg_database WHERE oid = {{ did }}){% endif %}) AS "{{ _('Hits') }}"

View File

@ -0,0 +1,10 @@
SELECT
name,
category,
setting,
unit,
short_desc
FROM
pg_settings
ORDER BY
category

View File

@ -0,0 +1,23 @@
SELECT
pid,
locktype,
datname,
relation::regclass,
page,
tuple,
virtualxid
transactionid,
classid::regclass,
objid,
objsubid,
virtualtransaction,
mode,
granted,
fastpath
FROM
pg_locks l
LEFT OUTER JOIN pg_database d ON (l.database = d.oid)
{% if did %}WHERE
datname = (SELECT datname FROM pg_database WHERE oid = {{ did }}){% endif %}
ORDER BY
pid, locktype

View File

@ -0,0 +1,12 @@
SELECT
gid,
database,
owner,
transaction,
prepared
FROM
pg_prepared_xacts
{% if did %}WHERE
database = (SELECT datname FROM pg_database WHERE oid = {{ did }}){% endif %}
ORDER BY
gid, database, owner

View File

@ -0,0 +1,4 @@
SELECT
(SELECT count(*) FROM pg_stat_activity{% if did %} WHERE datname = (SELECT datname FROM pg_database WHERE oid = {{ did }}){% endif %}) AS "{{ _('Total') }}",
(SELECT count(*) FROM pg_stat_activity WHERE state = 'active'{% if did %} AND datname = (SELECT datname FROM pg_database WHERE oid = {{ did }}){% endif %}) AS "{{ _('Active') }}",
(SELECT count(*) FROM pg_stat_activity WHERE state = 'idle'{% if did %} AND datname = (SELECT datname FROM pg_database WHERE oid = {{ did }}){% endif %}) AS "{{ _('Idle') }}"

View File

@ -0,0 +1,4 @@
SELECT
(SELECT sum(tup_inserted) FROM pg_stat_database{% if did %} WHERE datname = (SELECT datname FROM pg_database WHERE oid = {{ did }}){% endif %}) AS "{{ _('Inserts') }}",
(SELECT sum(tup_updated) FROM pg_stat_database{% if did %} WHERE datname = (SELECT datname FROM pg_database WHERE oid = {{ did }}){% endif %}) AS "{{ _('Updates') }}",
(SELECT sum(tup_deleted) FROM pg_stat_database{% if did %} WHERE datname = (SELECT datname FROM pg_database WHERE oid = {{ did }}){% endif %}) AS "{{ _('Deletes') }}"

View File

@ -0,0 +1,3 @@
SELECT
(SELECT sum(tup_fetched) FROM pg_stat_database{% if did %} WHERE datname = (SELECT datname FROM pg_database WHERE oid = {{ did }}){% endif %}) AS "{{ _('Fetched') }}",
(SELECT sum(tup_returned) FROM pg_stat_database{% if did %} WHERE datname = (SELECT datname FROM pg_database WHERE oid = {{ did }}){% endif %}) AS "{{ _('Returned') }}"

View File

@ -0,0 +1,4 @@
SELECT
(SELECT sum(xact_commit) + sum(xact_rollback) FROM pg_stat_database{% if did %} WHERE datname = (SELECT datname FROM pg_database WHERE oid = {{ did }}){% endif %}) AS "{{ _('Transactions') }}",
(SELECT sum(xact_commit) FROM pg_stat_database{% if did %} WHERE datname = (SELECT datname FROM pg_database WHERE oid = {{ did }}){% endif %}) AS "{{ _('Commits') }}",
(SELECT sum(xact_rollback) FROM pg_stat_database{% if did %} WHERE datname = (SELECT datname FROM pg_database WHERE oid = {{ did }}){% endif %}) AS "{{ _('Rollbacks') }}"

View File

@ -1 +1,77 @@
<h1>Welcome Dashboard</h1>
<div class="dashboard-container">
<div class="row">
<div class="col-xs-12">
<div class="obj_properties">
<legend class="badge">Welcome</legend>
</div>
<div class="well well-lg">
<img src="{{ url_for('dashboard.static', filename='img/welcome_logo.png') }}" alt="pgAdmin 4">
<h4>Feature rich | Maximises PostgreSQL | Open Source </h4>
<p>
pgAdmin is an open source administration and management tool for the PostgreSQL
database. The tools include a graphical administration interface, an SQL query
tool, a procedural code debugger and much more. The tool is designed to answer
the needs of developers, DBAs and system administrators alike.
</p>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<div class="obj_properties">
<legend class="badge">Quick Links</legend>
</div>
<div class="well well-lg">
<div class="row">
<div class="col-xs-6 dashboard-link">
<a onclick="pgAdmin.Dashboard.add_new_server()">
<span class="fa fa-4x dashboard-icon fa-server" aria-hidden="true"></span><br />
Add New Server
</a>
</div>
<div class="col-xs-6 dashboard-link">
<a onclick="pgAdmin.Preferences.show()">
<span id="mnu_preferences" class="fa fa-4x dashboard-icon fa-cogs" aria-hidden="true"></span><br />
Configure pgAdmin
</a>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<div class="obj_properties">
<legend class="badge">Getting Started</legend>
</div>
<div class="well well-lg">
<div class="row">
<div class="col-xs-3 dashboard-link">
<a href="http://www.postgresql.org/docs" target="_new">
<span class="fa fa-4x dashboard-icon icon-postgres" aria-hidden="true"></span><br />
PostgreSQL Documentation
</a>
</div>
<div class="col-xs-3 dashboard-link">
<a href="http://www.pgadmin.org" target="_new">
<span class="fa fa-4x dashboard-icon fa-globe" aria-hidden="true"></span><br />
pgAdmin Website
</a>
</div>
<div class="col-xs-3 dashboard-link">
<a href="http://planet.postgresql.org" target="_new">
<span class="fa fa-4x dashboard-icon fa-book" aria-hidden="true"></span><br />
Planet PostgreSQL
</a>
</div>
<div class="col-xs-3 dashboard-link">
<a href="http://www.postgresql.org/community" target="_new">
<span class="fa fa-4x dashboard-icon fa-users" aria-hidden="true"></span><br />
Community Support
</a>
</div>
</div>
</div>
</div>
</div>

View File

@ -809,7 +809,7 @@ td.edit-cell.editable.sortable.renderable.editor {
/* Message panel shown on browser tabs */
.pg-panel-message {
margin-top: 25px !important;
width: 600px;
width: 80%;
margin: 0 auto;
position: relative;
text-align: center;

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,503 @@
/*!
* bean.js - copyright Jacob Thornton 2011
* https://github.com/fat/bean
* MIT License
* special thanks to:
* dean edwards: http://dean.edwards.name/
* dperini: https://github.com/dperini/nwevents
* the entire mootools team: github.com/mootools/mootools-core
*/
/*global module:true, define:true*/
!function (name, context, definition) {
if (typeof module !== 'undefined') module.exports = definition(name, context);
else if (typeof define === 'function' && typeof define.amd === 'object') define(definition);
else context[name] = definition(name, context);
}('bean', this, function (name, context) {
var win = window
, old = context[name]
, overOut = /over|out/
, namespaceRegex = /[^\.]*(?=\..*)\.|.*/
, nameRegex = /\..*/
, addEvent = 'addEventListener'
, attachEvent = 'attachEvent'
, removeEvent = 'removeEventListener'
, detachEvent = 'detachEvent'
, doc = document || {}
, root = doc.documentElement || {}
, W3C_MODEL = root[addEvent]
, eventSupport = W3C_MODEL ? addEvent : attachEvent
, slice = Array.prototype.slice
, mouseTypeRegex = /click|mouse|menu|drag|drop/i
, touchTypeRegex = /^touch|^gesture/i
, ONE = { one: 1 } // singleton for quick matching making add() do one()
, nativeEvents = (function (hash, events, i) {
for (i = 0; i < events.length; i++)
hash[events[i]] = 1
return hash
})({}, (
'click dblclick mouseup mousedown contextmenu ' + // mouse buttons
'mousewheel DOMMouseScroll ' + // mouse wheel
'mouseover mouseout mousemove selectstart selectend ' + // mouse movement
'keydown keypress keyup ' + // keyboard
'orientationchange ' + // mobile
'focus blur change reset select submit ' + // form elements
'load unload beforeunload resize move DOMContentLoaded readystatechange ' + // window
'error abort scroll ' + // misc
(W3C_MODEL ? // element.fireEvent('onXYZ'... is not forgiving if we try to fire an event
// that doesn't actually exist, so make sure we only do these on newer browsers
'show ' + // mouse buttons
'input invalid ' + // form elements
'touchstart touchmove touchend touchcancel ' + // touch
'gesturestart gesturechange gestureend ' + // gesture
'message readystatechange pageshow pagehide popstate ' + // window
'hashchange offline online ' + // window
'afterprint beforeprint ' + // printing
'dragstart dragenter dragover dragleave drag drop dragend ' + // dnd
'loadstart progress suspend emptied stalled loadmetadata ' + // media
'loadeddata canplay canplaythrough playing waiting seeking ' + // media
'seeked ended durationchange timeupdate play pause ratechange ' + // media
'volumechange cuechange ' + // media
'checking noupdate downloading cached updateready obsolete ' + // appcache
'' : '')
).split(' ')
)
, customEvents = (function () {
function isDescendant(parent, node) {
while ((node = node.parentNode) !== null) {
if (node === parent) return true
}
return false
}
function check(event) {
var related = event.relatedTarget
if (!related) return related === null
return (related !== this && related.prefix !== 'xul' && !/document/.test(this.toString()) && !isDescendant(this, related))
}
return {
mouseenter: { base: 'mouseover', condition: check }
, mouseleave: { base: 'mouseout', condition: check }
, mousewheel: { base: /Firefox/.test(navigator.userAgent) ? 'DOMMouseScroll' : 'mousewheel' }
}
})()
, fixEvent = (function () {
var commonProps = 'altKey attrChange attrName bubbles cancelable ctrlKey currentTarget detail eventPhase getModifierState isTrusted metaKey relatedNode relatedTarget shiftKey srcElement target timeStamp type view which'.split(' ')
, mouseProps = commonProps.concat('button buttons clientX clientY dataTransfer fromElement offsetX offsetY pageX pageY screenX screenY toElement'.split(' '))
, keyProps = commonProps.concat('char charCode key keyCode'.split(' '))
, touchProps = commonProps.concat('touches targetTouches changedTouches scale rotation'.split(' '))
, preventDefault = 'preventDefault'
, createPreventDefault = function (event) {
return function () {
if (event[preventDefault])
event[preventDefault]()
else
event.returnValue = false
}
}
, stopPropagation = 'stopPropagation'
, createStopPropagation = function (event) {
return function () {
if (event[stopPropagation])
event[stopPropagation]()
else
event.cancelBubble = true
}
}
, createStop = function (synEvent) {
return function () {
synEvent[preventDefault]()
synEvent[stopPropagation]()
synEvent.stopped = true
}
}
, copyProps = function (event, result, props) {
var i, p
for (i = props.length; i--;) {
p = props[i]
if (!(p in result) && p in event) result[p] = event[p]
}
}
return function (event, isNative) {
var result = { originalEvent: event, isNative: isNative }
if (!event)
return result
var props
, type = event.type
, target = event.target || event.srcElement
result[preventDefault] = createPreventDefault(event)
result[stopPropagation] = createStopPropagation(event)
result.stop = createStop(result)
result.target = target && target.nodeType === 3 ? target.parentNode : target
if (isNative) { // we only need basic augmentation on custom events, the rest is too expensive
if (type.indexOf('key') !== -1) {
props = keyProps
result.keyCode = event.which || event.keyCode
} else if (mouseTypeRegex.test(type)) {
props = mouseProps
result.rightClick = event.which === 3 || event.button === 2
result.pos = { x: 0, y: 0 }
if (event.pageX || event.pageY) {
result.clientX = event.pageX
result.clientY = event.pageY
} else if (event.clientX || event.clientY) {
result.clientX = event.clientX + doc.body.scrollLeft + root.scrollLeft
result.clientY = event.clientY + doc.body.scrollTop + root.scrollTop
}
if (overOut.test(type))
result.relatedTarget = event.relatedTarget || event[(type === 'mouseover' ? 'from' : 'to') + 'Element']
} else if (touchTypeRegex.test(type)) {
props = touchProps
}
copyProps(event, result, props || commonProps)
}
return result
}
})()
// if we're in old IE we can't do onpropertychange on doc or win so we use doc.documentElement for both
, targetElement = function (element, isNative) {
return !W3C_MODEL && !isNative && (element === doc || element === win) ? root : element
}
// we use one of these per listener, of any type
, RegEntry = (function () {
function entry(element, type, handler, original, namespaces) {
this.element = element
this.type = type
this.handler = handler
this.original = original
this.namespaces = namespaces
this.custom = customEvents[type]
this.isNative = nativeEvents[type] && element[eventSupport]
this.eventType = W3C_MODEL || this.isNative ? type : 'propertychange'
this.customType = !W3C_MODEL && !this.isNative && type
this.target = targetElement(element, this.isNative)
this.eventSupport = this.target[eventSupport]
}
entry.prototype = {
// given a list of namespaces, is our entry in any of them?
inNamespaces: function (checkNamespaces) {
var i, j
if (!checkNamespaces)
return true
if (!this.namespaces)
return false
for (i = checkNamespaces.length; i--;) {
for (j = this.namespaces.length; j--;) {
if (checkNamespaces[i] === this.namespaces[j])
return true
}
}
return false
}
// match by element, original fn (opt), handler fn (opt)
, matches: function (checkElement, checkOriginal, checkHandler) {
return this.element === checkElement &&
(!checkOriginal || this.original === checkOriginal) &&
(!checkHandler || this.handler === checkHandler)
}
}
return entry
})()
, registry = (function () {
// our map stores arrays by event type, just because it's better than storing
// everything in a single array. uses '$' as a prefix for the keys for safety
var map = {}
// generic functional search of our registry for matching listeners,
// `fn` returns false to break out of the loop
, forAll = function (element, type, original, handler, fn) {
if (!type || type === '*') {
// search the whole registry
for (var t in map) {
if (t.charAt(0) === '$')
forAll(element, t.substr(1), original, handler, fn)
}
} else {
var i = 0, l, list = map['$' + type], all = element === '*'
if (!list)
return
for (l = list.length; i < l; i++) {
if (all || list[i].matches(element, original, handler))
if (!fn(list[i], list, i, type))
return
}
}
}
, has = function (element, type, original) {
// we're not using forAll here simply because it's a bit slower and this
// needs to be fast
var i, list = map['$' + type]
if (list) {
for (i = list.length; i--;) {
if (list[i].matches(element, original, null))
return true
}
}
return false
}
, get = function (element, type, original) {
var entries = []
forAll(element, type, original, null, function (entry) { return entries.push(entry) })
return entries
}
, put = function (entry) {
(map['$' + entry.type] || (map['$' + entry.type] = [])).push(entry)
return entry
}
, del = function (entry) {
forAll(entry.element, entry.type, null, entry.handler, function (entry, list, i) {
list.splice(i, 1)
if (list.length === 0)
delete map['$' + entry.type]
return false
})
}
// dump all entries, used for onunload
, entries = function () {
var t, entries = []
for (t in map) {
if (t.charAt(0) === '$')
entries = entries.concat(map[t])
}
return entries
}
return { has: has, get: get, put: put, del: del, entries: entries }
})()
// add and remove listeners to DOM elements
, listener = W3C_MODEL ? function (element, type, fn, add) {
element[add ? addEvent : removeEvent](type, fn, false)
} : function (element, type, fn, add, custom) {
if (custom && add && element['_on' + custom] === null)
element['_on' + custom] = 0
element[add ? attachEvent : detachEvent]('on' + type, fn)
}
, nativeHandler = function (element, fn, args) {
return function (event) {
event = fixEvent(event || ((this.ownerDocument || this.document || this).parentWindow || win).event, true)
return fn.apply(element, [event].concat(args))
}
}
, customHandler = function (element, fn, type, condition, args, isNative) {
return function (event) {
if (condition ? condition.apply(this, arguments) : W3C_MODEL ? true : event && event.propertyName === '_on' + type || !event) {
if (event)
event = fixEvent(event || ((this.ownerDocument || this.document || this).parentWindow || win).event, isNative)
fn.apply(element, event && (!args || args.length === 0) ? arguments : slice.call(arguments, event ? 0 : 1).concat(args))
}
}
}
, once = function (rm, element, type, fn, originalFn) {
// wrap the handler in a handler that does a remove as well
return function () {
rm(element, type, originalFn)
fn.apply(this, arguments)
}
}
, removeListener = function (element, orgType, handler, namespaces) {
var i, l, entry
, type = (orgType && orgType.replace(nameRegex, ''))
, handlers = registry.get(element, type, handler)
for (i = 0, l = handlers.length; i < l; i++) {
if (handlers[i].inNamespaces(namespaces)) {
if ((entry = handlers[i]).eventSupport)
listener(entry.target, entry.eventType, entry.handler, false, entry.type)
// TODO: this is problematic, we have a registry.get() and registry.del() that
// both do registry searches so we waste cycles doing this. Needs to be rolled into
// a single registry.forAll(fn) that removes while finding, but the catch is that
// we'll be splicing the arrays that we're iterating over. Needs extra tests to
// make sure we don't screw it up. @rvagg
registry.del(entry)
}
}
}
, addListener = function (element, orgType, fn, originalFn, args) {
var entry
, type = orgType.replace(nameRegex, '')
, namespaces = orgType.replace(namespaceRegex, '').split('.')
if (registry.has(element, type, fn))
return element // no dupe
if (type === 'unload')
fn = once(removeListener, element, type, fn, originalFn) // self clean-up
if (customEvents[type]) {
if (customEvents[type].condition)
fn = customHandler(element, fn, type, customEvents[type].condition, true)
type = customEvents[type].base || type
}
entry = registry.put(new RegEntry(element, type, fn, originalFn, namespaces[0] && namespaces))
entry.handler = entry.isNative ?
nativeHandler(element, entry.handler, args) :
customHandler(element, entry.handler, type, false, args, false)
if (entry.eventSupport)
listener(entry.target, entry.eventType, entry.handler, true, entry.customType)
}
, del = function (selector, fn, $) {
return function (e) {
var target, i, array = typeof selector === 'string' ? $(selector, this) : selector
for (target = e.target; target && target !== this; target = target.parentNode) {
for (i = array.length; i--;) {
if (array[i] === target) {
return fn.apply(target, arguments)
}
}
}
}
}
, remove = function (element, typeSpec, fn) {
var k, m, type, namespaces, i
, rm = removeListener
, isString = typeSpec && typeof typeSpec === 'string'
if (isString && typeSpec.indexOf(' ') > 0) {
// remove(el, 't1 t2 t3', fn) or remove(el, 't1 t2 t3')
typeSpec = typeSpec.split(' ')
for (i = typeSpec.length; i--;)
remove(element, typeSpec[i], fn)
return element
}
type = isString && typeSpec.replace(nameRegex, '')
if (type && customEvents[type])
type = customEvents[type].type
if (!typeSpec || isString) {
// remove(el) or remove(el, t1.ns) or remove(el, .ns) or remove(el, .ns1.ns2.ns3)
if (namespaces = isString && typeSpec.replace(namespaceRegex, ''))
namespaces = namespaces.split('.')
rm(element, type, fn, namespaces)
} else if (typeof typeSpec === 'function') {
// remove(el, fn)
rm(element, null, typeSpec)
} else {
// remove(el, { t1: fn1, t2, fn2 })
for (k in typeSpec) {
if (typeSpec.hasOwnProperty(k))
remove(element, k, typeSpec[k])
}
}
return element
}
, add = function (element, events, fn, delfn, $) {
var type, types, i, args
, originalFn = fn
, isDel = fn && typeof fn === 'string'
if (events && !fn && typeof events === 'object') {
for (type in events) {
if (events.hasOwnProperty(type))
add.apply(this, [ element, type, events[type] ])
}
} else {
args = arguments.length > 3 ? slice.call(arguments, 3) : []
types = (isDel ? fn : events).split(' ')
isDel && (fn = del(events, (originalFn = delfn), $)) && (args = slice.call(args, 1))
// special case for one()
this === ONE && (fn = once(remove, element, events, fn, originalFn))
for (i = types.length; i--;) addListener(element, types[i], fn, originalFn, args)
}
return element
}
, one = function () {
return add.apply(ONE, arguments)
}
, fireListener = W3C_MODEL ? function (isNative, type, element) {
var evt = doc.createEvent(isNative ? 'HTMLEvents' : 'UIEvents')
evt[isNative ? 'initEvent' : 'initUIEvent'](type, true, true, win, 1)
element.dispatchEvent(evt)
} : function (isNative, type, element) {
element = targetElement(element, isNative)
// if not-native then we're using onpropertychange so we just increment a custom property
isNative ? element.fireEvent('on' + type, doc.createEventObject()) : element['_on' + type]++
}
, fire = function (element, type, args) {
var i, j, l, names, handlers
, types = type.split(' ')
for (i = types.length; i--;) {
type = types[i].replace(nameRegex, '')
if (names = types[i].replace(namespaceRegex, ''))
names = names.split('.')
if (!names && !args && element[eventSupport]) {
fireListener(nativeEvents[type], type, element)
} else {
// non-native event, either because of a namespace, arguments or a non DOM element
// iterate over all listeners and manually 'fire'
handlers = registry.get(element, type)
args = [false].concat(args)
for (j = 0, l = handlers.length; j < l; j++) {
if (handlers[j].inNamespaces(names))
handlers[j].handler.apply(element, args)
}
}
}
return element
}
, clone = function (element, from, type) {
var i = 0
, handlers = registry.get(from, type)
, l = handlers.length
for (;i < l; i++)
handlers[i].original && add(element, handlers[i].type, handlers[i].original)
return element
}
, bean = {
add: add
, one: one
, remove: remove
, clone: clone
, fire: fire
, noConflict: function () {
context[name] = old
return this
}
}
if (win[attachEvent]) {
// for IE, clean up on unload to avoid leaks
var cleanup = function () {
var i, entries = registry.entries()
for (i in entries) {
if (entries[i].type && entries[i].type !== 'unload')
remove(entries[i].element, entries[i].type)
}
win[detachEvent]('onunload', cleanup)
win.CollectGarbage && win.CollectGarbage()
}
win[attachEvent]('onunload', cleanup)
}
return bean
});

File diff suppressed because it is too large Load Diff

View File

@ -89,6 +89,13 @@
},
"pgadmin.backform": {
"deps": ['backform', "pgadmin.backgrid", "select2"],
},
"flotr2": {
deps: ['bean'],
exports: function(bean) {
Flotr.bean = bean;
return this.Flotr;
}
}{% for script in current_app.javascripts %}{% if 'deps' in script or 'exports' in script %},
'{{ script.name }}': {
{% if 'deps' in script %}"deps": [ {% set comma = False %}{% for dep in script['deps'] %} {% if comma %},{% else %}{% set comma = True %}{% endif %} '{{ dep }}'{% endfor %}],{% endif %}
@ -116,7 +123,9 @@
"backgrid.filter": "{{ url_for('static', filename='js/backgrid/' + ('backgrid-filter' if config.DEBUG else 'backgrid-filter.min')) }}",
"backbone.undo": "{{ url_for('static', filename='js/' + ('backbone.undo' if config.DEBUG else 'backbone.undo.min')) }}",
"pgadmin.backgrid": "{{ url_for('static', filename='js/backgrid/backgrid.pgadmin') }}",
'pgadmin.backform': "{{ url_for('static', filename='js/backform.pgadmin') }}"{% for script in current_app.javascripts %},
'pgadmin.backform': "{{ url_for('static', filename='js/backform.pgadmin') }}",
bean :"{{ url_for('static', filename='js/flotr2/' + ('bean' if config.DEBUG else 'bean-min')) }}",
flotr2 :"{{ url_for('static', filename='js/flotr2/flotr2.amd') }}"{% for script in current_app.javascripts %},
'{{ script.name }}': "{{ script.path }}"{% endfor %}
}
});

View File

@ -20,7 +20,7 @@ class Panel(object):
def __init__(self, name, title, content='', width=500, height=600, isIframe=True,
showTitle=True, isCloseable=True, isPrivate=False, priority=None,
icon=None, data=None):
icon=None, data=None, events=None):
self.name = name
self.title = title
self.content = content
@ -31,7 +31,8 @@ class Panel(object):
self.isCloseable = isCloseable
self.isPrivate = isPrivate
self.icon = icon
self.data = None
self.data = data
self.events = events
if priority is None:
global PRIORITY
PRIORITY += 100