mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2024-11-23 09:16:29 -06:00
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:
parent
ca62825c90
commit
c34e62207a
231
web/pgadmin/tools/maintenance/__init__.py
Normal file
231
web/pgadmin/tools/maintenance/__init__.py
Normal 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'}
|
||||
)
|
4
web/pgadmin/tools/maintenance/static/css/maintenance.css
Normal file
4
web/pgadmin/tools/maintenance/static/css/maintenance.css
Normal file
@ -0,0 +1,4 @@
|
||||
.btn-group.pg-maintenance-op label.btn.btn-primary.active {
|
||||
background-color: aliceblue;
|
||||
color: #3174AF;
|
||||
}
|
@ -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;
|
||||
});
|
@ -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 %}
|
Loading…
Reference in New Issue
Block a user