Added support different maintenance operations

i.e. vacuum, analyze, reindex, cluster.

Tweaked by Ashesh for:
* Integrate it with the background process executor, and observer.
* Changed the UI for operation selection from select2 to custom radio
  group.
* Made it consistent with other tools like backup, restore, etc.
This commit is contained in:
Neel Patel 2016-05-16 00:16:38 +05:30 committed by Ashesh Vashi
parent ca62825c90
commit c34e62207a
4 changed files with 549 additions and 0 deletions

View File

@ -0,0 +1,231 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2016, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
"""A blueprint module implementing the maintenance tool for vacuum"""
import cgi
import json
from flask import url_for, Response, render_template, request, current_app
from flask.ext.babel import gettext as _
from flask.ext.security import login_required
from config import PG_DEFAULT_DRIVER
from pgadmin.misc.bgprocess.processes import BatchProcess, IProcessDesc
from pgadmin.model import Server
from pgadmin.utils import PgAdminModule
from pgadmin.utils.ajax import bad_request, make_json_response
from pgadmin.utils.driver import get_driver
MODULE_NAME = 'maintenance'
class MaintenanceModule(PgAdminModule):
"""
class MaintenanceModule(PgAdminModule)
A module class for maintenance tools of vacuum which is derived from
PgAdminModule.
Methods:
-------
* get_own_javascripts()
- Method is used to load the required javascript files for maintenance
tool module
* get_own_stylesheets()
- Returns the list of CSS file used by Maintenance module
"""
LABEL = _('Maintenance')
def get_own_javascripts(self):
scripts = list()
for name, script in [
['pgadmin.tools.maintenance', 'js/maintenance']
]:
scripts.append({
'name': name,
'path': url_for('maintenance.index') + script,
'when': None
})
return scripts
def get_own_stylesheets(self):
"""
Returns:
list: the stylesheets used by this module.
"""
stylesheets = [
url_for('maintenance.static', filename='css/maintenance.css')
]
return stylesheets
blueprint = MaintenanceModule(MODULE_NAME, __name__)
class Message(IProcessDesc):
def __init__(self, _sid, _data, _query):
self.sid = _sid
self.data = _data
self.query = _query
@property
def message(self):
res = _("Maintenance ({0})")
if self.data['op'] == "VACUUM":
return res.format(_('Vacuum'))
if self.data['op'] == "ANALYZE":
return res.format(_('Analyze'))
if self.data['op'] == "REINDEX":
return res.format(_('Reindex'))
if self.data['op'] == "CLUSTER":
return res.format(_('Cluster'))
def details(self, cmd, args):
res = None
if self.data['op'] == "VACUUM":
res = _('VACUUM ({0})')
opts = []
if self.data['vacuum_full']:
opts.append(_('FULL'))
if self.data['vacuum_freeze']:
opts.append(_('FREEZE'))
if self.data['verbose']:
opts.append(_('VERBOSE'))
res = res.format(', '.join(str(x) for x in opts))
if self.data['op'] == "ANALYZE":
res = _('ANALYZE')
if self.data['verbose']:
res += '(' + _('VERBOSE') + ')'
if self.data['op'] == "REINDEX":
if 'schema' in self.data and self.data['schema']:
return _('REINDEX TABLE')
res = _('REINDEX')
if self.data['op'] == "CLUSTER":
res = _('CLUSTER')
res = '<div class="h5">' + cgi.escape(res).encode(
'ascii', 'xmlcharrefreplace'
)
res += '</div><div class="h5">'
res += cgi.escape(
_("Running Query:")
).encode('ascii', 'xmlcharrefreplace')
res += '</b><br><i>'
res += cgi.escape(self.query).encode('ascii', 'xmlcharrefreplace')
res += '</i></div>'
return res
@blueprint.route("/")
@login_required
def index():
return bad_request(
errormsg=_("This URL can not be called directly!")
)
@blueprint.route("/js/maintenance.js")
@login_required
def script():
"""render the maintenance tool of vacuum javascript file"""
return Response(
response=render_template("maintenance/js/maintenance.js", _=_),
status=200,
mimetype="application/javascript"
)
@blueprint.route('/create_job/<int:sid>/<int:did>', methods=['POST'])
@login_required
def create_maintenance_job(sid, did):
"""
Args:
sid: Server ID
did: Database ID
Creates a new job for maintenance vacuum operation
Returns:
None
"""
if request.form:
# Convert ImmutableDict to dict
data = dict(request.form)
data = json.loads(data['data'][0])
else:
data = json.loads(request.data.decode())
# Fetch the server details like hostname, port, roles etc
server = Server.query.filter_by(
id=sid).first()
if server is None:
return make_json_response(
success=0,
errormsg=_("Couldn't find the given server")
)
# To fetch MetaData for the server
driver = get_driver(PG_DEFAULT_DRIVER)
manager = driver.connection_manager(server.id)
conn = manager.connection()
connected = conn.connected()
if not connected:
return make_json_response(
success=0,
errormsg=_("Please connect to the server first...")
)
utility = manager.utility('sql')
# Create the command for the vacuum operation
query = render_template(
'maintenance/sql/command.sql', conn=conn, data=data
)
args = [
'--host', server.host, '--port', str(server.port),
'--username', server.username, '--dbname',
driver.qtIdent(conn, data['database']),
'--command', query
]
try:
p = BatchProcess(
desc=Message(sid, data, query),
cmd=utility, args=args
)
manager.export_password_env(p.id)
p.start()
jid = p.id
except Exception as e:
current_app.logger.exception(e)
return make_json_response(
status=410,
success=0,
errormsg=str(e)
)
# Return response
return make_json_response(
data={'job_id': jid, 'status': True, 'info': 'Maintenance job created'}
)

View File

@ -0,0 +1,4 @@
.btn-group.pg-maintenance-op label.btn.btn-primary.active {
background-color: aliceblue;
color: #3174AF;
}

View File

@ -0,0 +1,302 @@
define(
['jquery', 'underscore', 'underscore.string', 'alertify', 'pgadmin',
'pgadmin.browser', 'backbone', 'backgrid', 'backform',
'pgadmin.backform', 'pgadmin.backgrid', 'pgadmin.browser.node.ui'],
function($, _, S, Alertify, pgAdmin, pgBrowser, Backbone, Backgrid, Backform) {
pgAdmin = pgAdmin || window.pgAdmin || {};
var pgTools = pgAdmin.Tools = pgAdmin.Tools || {};
// Return back, this has been called more than once
if (pgAdmin.Tools.maintenance)
return pgAdmin.Tools.maintenance;
var CustomSwitchControl = Backform.CustomSwitchControl = Backform.SwitchControl.extend({
template: _.template([
'<label class="<%=Backform.controlLabelClassName%> custom_switch_label_class"><%=label%></label>',
'<div class="<%=Backform.controlsClassName%> custom_switch_control_class">',
' <div class="checkbox">',
' <label>',
' <input type="checkbox" class="<%=extraClasses.join(\' \')%>" name="<%=name%>" <%=value ? "checked=\'checked\'" : ""%> <%=disabled ? "disabled" : ""%> <%=required ? "required" : ""%> />',
' </label>',
' </div>',
'</div>',
'<% if (helpMessage && helpMessage.length) { %>',
' <span class="<%=Backform.helpMessageClassName%>"><%=helpMessage%></span>',
'<% } %>'
].join("\n")),
className: 'pgadmin-control-group form-group col-xs-6'
});
// Main model for Maintenance functionality
var MaintenanceModel = Backbone.Model.extend({
defaults: {
op: 'VACUUM',
vacuum_full: false,
vacuum_freeze: false,
vacuum_analyze: false,
verbose: true
},
schema: [
{
id: 'op', label:'{{ _('Maintenance operation') }}', cell: 'string',
type: 'text', group: '{{ _('Options') }}',
options:[
{'label': "VACUUM", 'value': "VACUUM"},
{'label': "ANALYZE", 'value': "ANALYZE"},
{'label': "REINDEX", 'value': "REINDEX"},
{'label': "CLUSTER", 'value': "CLUSTER"},
],
control: Backform.RadioControl.extend({
template: _.template([
'<label class="control-label col-sm-4 col-xs-12"><%=label%></label>',
'<div class="pgadmin-controls col-xs-12 col-sm-8 btn-group pg-maintenance-op" data-toggle="buttons">',
' <% for (var i=0; i < options.length; i++) { %>',
' <% var option = options[i]; %>',
' <label class="btn btn-primary<% if (i == 0) { %> active<%}%>">',
' <input type="radio" name="op" id="op" autocomplete="off" value=<%-formatter.fromRaw(option.value)%><% if (i == 0) { %> selected<%}%> > <%-option.label%>',
' </label>',
' <% } %>',
'</div>'
].join("\n"))
}),
select2: {
allowClear: false,
width: "100%",
placeholder: '{{ _('Select from list...') }}'
},
},
{
type: 'nested', control: 'fieldset', label:'{{ _('Vacuum') }}', group: '{{ _('Options') }}',
schema:[{
id: 'vacuum_full', disabled: false, group: '{{ _('Vacuum') }}', disabled: 'isDisabled',
control: Backform.CustomSwitchControl, label: '{{ _('FULL') }}', deps: ['op']
},{
id: 'vacuum_freeze', disabled: false, deps: ['op'], disabled: 'isDisabled',
control: Backform.CustomSwitchControl, label: '{{ _('FREEZE') }}', group: '{{ _('Vacuum') }}'
},{
id: 'vacuum_analyze', disabled: false, deps: ['op'], disabled: 'isDisabled',
control: Backform.CustomSwitchControl, label: '{{ _('ALALYZE') }}', group: '{{ _('Vacuum') }}'
}]
},
{
id: 'verbose', disabled: false, group: '{{ _('Options') }}', deps: ['op'],
control: Backform.CustomSwitchControl, label: '{{ _('Verbose Messages') }}', disabled: 'isDisabled'
}
],
// Enable/Disable the items based on the user maintenance operation selection
isDisabled: function(m) {
name = this.name;
switch(name) {
case 'vacuum_full':
case 'vacuum_freeze':
case 'vacuum_analyze':
if (m.get('op') != 'VACUUM') {
return true;
}
else {
return false;
}
break;
case 'verbose':
if (m.get('op') == 'REINDEX') {
return true;
}
else {
return false;
}
break;
default:
return false;
}
return false;
}
});
pgTools.maintenance = {
init: function() {
// We do not want to initialize the module multiple times.
if (this.initialized)
return;
this.initialized = true;
var maintenance_supported_nodes = [
'database', 'table'
];
/**
Enable/disable Maintenance menu in tools based on node selected.
Maintenance menu will be enabled only when user select table and database node.
*/
menu_enabled = function(itemData, item, data) {
var t = pgBrowser.tree, i = item, d = itemData;
var parent_item = t.hasParent(i) ? t.parent(i): null,
parent_data = parent_item ? t.itemData(parent_item) : null;
if(!_.isUndefined(d) && !_.isNull(d) && !_.isNull(parent_data))
return (
(_.indexOf(maintenance_supported_nodes, d._type) !== -1 &&
parent_data._type != 'catalog') ? true: false
);
else
return false;
};
var menus = [{
name: 'maintenance', module: this,
applies: ['tools'], callback: 'callback_maintenace',
priority: 10, label: '{{ _('Maintenance...') }}',
icon: 'fa fa-wrench', enable: menu_enabled
}];
// Add supported menus into the menus list
for (var idx = 0; idx < maintenance_supported_nodes.length; idx++) {
menus.push({
name: 'maintenance_context_' + maintenance_supported_nodes[idx],
node: maintenance_supported_nodes[idx], module: this,
applies: ['context'], callback: 'callback_maintenace',
priority: 10, label: '{{_("Maintenance...") }}',
icon: 'fa fa-wrench', enable: menu_enabled
});
}
pgBrowser.add_menus(menus);
},
/*
Open the dialog for the maintenance functionality
*/
callback_maintenace: function(args, item) {
var self = this;
var input = args || {},
t = pgBrowser.tree,
i = item || t.selected(),
d = i && i.length == 1 ? t.itemData(i) : undefined,
node = d && pgBrowser.Nodes[d._type];
if (!d)
return;
var objName = d.label;
var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]);
if (!Alertify.MaintenanceDialog) {
Alertify.dialog('MaintenanceDialog', function factory() {
return {
main:function(title) {
this.set('title', title);
},
setup:function() {
return {
buttons:[{ text: "{{ _('OK') }}", key: 27, className: "btn btn-primary fa fa-lg fa-save pg-alertify-button" },
{ text: "{{ _('Cancel') }}", key: 27, className: "btn btn-danger fa fa-lg fa-times pg-alertify-button" }],
options: { modal: 0}
};
},
// Callback functions when click on the buttons of the Alertify dialogs
callback: function(e) {
if (e.button.text === "{{ _('OK') }}") {
var schema = '';
var table = '';
var i = pgBrowser.tree.selected(),
d = i && i.length == 1 ? pgBrowser.tree.itemData(i) : undefined,
node = d && pgBrowser.Nodes[d._type];
if (!d)
return;
var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]);
if (treeInfo.schema != undefined) {
schema = treeInfo.schema.label;
}
if (treeInfo.table != undefined) {
table = treeInfo.table.label;
}
this.view.model.set({'database': treeInfo.database.label,
'schema': schema,
'table': table})
baseUrl = "{{ url_for('maintenance.index') }}" +
"create_job/" + treeInfo.server._id + "/" + treeInfo.database._id,
args = this.view.model.toJSON();
$.ajax({
url: baseUrl,
method: 'POST',
data:{ 'data': JSON.stringify(args) },
success: function(res) {
if (res.data.status) {
//Do nothing as we are creating the job and exiting from the main dialog
Alertify.success(res.data.info);
pgBrowser.Events.trigger('pgadmin-bgprocess:created', self);
}
else {
Alertify.error(res.data.info);
}
},
error: function(e) {
Alertify.alert(
"{{ _('Maintenance job creation failed') }}"
);
}
});
}
},
build:function() {
},
hooks: {
onclose: function() {
if (this.view) {
this.view.remove({data: true, internal: true, silent: true});
}
}
},
prepare:function() {
// Main maintenance tool dialog container
var $container = $("<div class='maintenance_dlg'></div>");
var t = pgBrowser.tree,
i = t.selected(),
d = i && i.length == 1 ? t.itemData(i) : undefined,
node = d && pgBrowser.Nodes[d._type];
if (!d)
return;
var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]);
var newModel = new MaintenanceModel (
{}, {node_info: treeInfo}
),
fields = Backform.generateViewSchema(
treeInfo, newModel, 'create', node, treeInfo.server, true
);
var view = this.view = new Backform.Dialog({
el: $container, model: newModel, schema: fields
});
$(this.elements.body.childNodes[0]).addClass('alertify_tools_dialog_properties obj_properties');
view.render();
this.elements.content.appendChild($container.get(0));
}
};
});
}
// Open the Alertify dialog
Alertify.MaintenanceDialog('Maintenance...').set('resizable',true).resizeTo('60%','80%');
},
};
return pgAdmin.Tools.maintenance;
});

View File

@ -0,0 +1,12 @@
{% if data.op == "VACUUM" %}
VACUUM{% if data.vacuum_full %} FULL{% endif %}{% if data.vacuum_freeze %} FREEZE{% endif %}{% if data.vacuum_analyze %} ANALYZE{% endif %}{% if data.verbose %} VERBOSE{% endif %}{% if data.schema %} {{ conn|qtIdent(data.schema) }}.{{ conn|qtIdent(data.table) }}{% endif %};
{% endif %}
{% if data.op == "ANALYZE" %}
ANALYZE{% if data.verbose %} VERBOSE{% endif %}{% if data.schema %} {{ conn|qtIdent(data.schema, data.table) }}{% endif %};
{% endif %}
{% if data.op == "REINDEX" %}
REINDEX{% if not data.schema %} DATABASE {{ conn|qtIdent(data.database) }}{% else %} TABLE {{ conn|qtIdent(data.schema, data.table) }}{% endif %};
{% endif %}
{% if data.op == "CLUSTER" %}
CLUSTER{% if data.verbose %} VERBOSE {% endif %}{% if data.schema %} {{ conn|qtIdent(data.schema, data.table) }}{% endif %}
{% endif %}