Commit d01561fc authored by Serge S. Koval's avatar Serge S. Koval

RedisCli improvements

parent 25fc7b8d
......@@ -3,7 +3,6 @@ import shlex
import warnings
from flask import request
from flask.json import jsonify
from jinja2 import Markup
......@@ -12,7 +11,28 @@ from flask.ext.admin.babel import gettext
from flask.ext.admin._compat import VER
class CommandError(Exception):
"""
RedisCli error exception.
"""
pass
class TextWrapper(str):
"""
Small text wrapper for result formatter to distinguish between
different string types.
"""
pass
class RedisCli(BaseView):
"""
Simple redis console.
To use it, simply pass `Redis` connection object to the constructor.
"""
shlex_check = True
"""
shlex from stdlib does not work with unicode on 2.7.2 and lower.
......@@ -26,6 +46,11 @@ class RedisCli(BaseView):
List of redis remapped commands.
"""
excluded_commands = set(('pubsub', 'set_response_callback', 'from_url'))
"""
List of excluded commands.
"""
def __init__(self, redis,
name=None, category=None, endpoint=None, url=None):
"""
......@@ -51,6 +76,7 @@ class RedisCli(BaseView):
self.commands = {}
self._inspect_commands()
self._contribute_commands()
if self.shlex_check and VER < (2, 7, 3):
warnings.warn('Warning: rediscli uses shlex library and it does not work with unicode until Python 2.7.3. ' +
......@@ -58,14 +84,34 @@ class RedisCli(BaseView):
'to False.')
def _inspect_commands(self):
"""
Inspect connection object and extract command names.
"""
for name in dir(self.redis):
if not name.startswith('_'):
attr = getattr(self.redis, name)
if callable(attr):
if callable(attr) and name not in self.excluded_commands:
doc = (getattr(attr, '__doc__', '') or '').strip()
self.commands[name] = (attr, doc)
for old, new in self.remapped_commands:
self.commands[new] = self.commands[old]
def _contribute_commands(self):
"""
Contribute custom commands.
"""
self.commands['help'] = (self._cmd_help, 'Help!')
def _execute_command(self, name, args):
"""
Execute single command.
:param name:
Command name
:param args:
Command arguments
"""
# Do some remapping
new_cmd = self.remapped_commands.get(name)
if new_cmd:
......@@ -79,6 +125,12 @@ class RedisCli(BaseView):
return self._result(handler(*args))
def _parse_cmd(self, cmd):
"""
Parse command by using shlex module.
:param cmd:
Command to parse
"""
if VER < (2, 7, 3):
# shlex can't work with unicode until 2.7.3
return tuple(x.decode('utf-8') for x in shlex.split(cmd.encode('utf-8')))
......@@ -86,19 +138,58 @@ class RedisCli(BaseView):
return tuple(shlex.split(cmd))
def _error(self, msg):
"""
Format error message as HTTP response.
:param msg:
Message to format
"""
return Markup('<div class="error">%s</div>' % msg)
def _result(self, result):
"""
Format result message as HTTP response.
:param msg:
Result to format.
"""
return self.render('admin/rediscli/response.html',
type_name=lambda d: type(d).__name__,
result=result)
# Commands
def _cmd_help(self, *args):
"""
Help command implementation.
"""
if not args:
help = 'Usage: help <command>.\nList of supported commands: '
help += ', '.join(n for n in sorted(self.commands))
return TextWrapper(help)
cmd = args[0]
if cmd not in self.commands:
raise CommandError('Invalid command.')
help = self.commands[cmd][1]
if not help:
return TextWrapper('Command does not have any help.')
return TextWrapper(help)
# Views
@expose('/')
def console_view(self):
"""
Console view.
"""
return self.render('admin/rediscli/console.html')
@expose('/run/', methods=('POST',))
def execute_view(self):
"""
AJAX API.
"""
try:
cmd = request.form.get('cmd').lower()
if not cmd:
......@@ -109,6 +200,8 @@ class RedisCli(BaseView):
return self._error('Cli: Failed to parse command.')
return self._execute_command(parts[0], parts[1:])
except CommandError as err:
return self._error('Cli: %s' % err)
except Exception as ex:
logging.exception(ex)
return self._error('Cli: %s' % ex)
......@@ -33,10 +33,14 @@
.console .cmd {
background-color: #f5f5f5;
padding: 2px;
margin: 1px;
}
.console .response {
background-color: #f0f0f0;
padding: 2px;
margin: 1px;
}
.console .error {
......
var RedisCli = function(postUrl) {
// Constants
var KEY_UP = 38;
var KEY_DOWN = 40;
var MAX_ITEMS = 128;
var $con = $('.console');
var $container = $con.find('.console-container');
var $input = $con.find('input');
var history = [];
var historyPos = 0;
function resizeConsole() {
var height = $(window).height();
......@@ -11,12 +19,16 @@ var RedisCli = function(postUrl) {
}
function scrollBottom() {
$container.animate({scrollTop: $container.height()}, 100);
$container.animate({scrollTop: $container[0].scrollHeight}, 100);
}
function createEntry(cmd) {
var $entry = $('<div>').addClass('entry').appendTo($container);
$entry.append($('<div>').addClass('cmd').html(cmd));
if ($container.find('>div').length > MAX_ITEMS)
$container.find('>div:first-child').remove();
scrollBottom();
return $entry;
}
......@@ -25,14 +37,25 @@ var RedisCli = function(postUrl) {
$entry.append($('<div>').addClass('response').html(response));
scrollBottom();
}
function addError($entry, response) {
$entry.append($('<div>').addClass('response').addClass('error').html(response));
scrollBottom();
}
function submitCommand() {
var val = $input.val().trim();
if (!val.length)
return false;
function addHistory(cmd) {
history.push(cmd);
if (history > MAX_ITEMS)
history.splice(0, 1);
historyPos = history.length;
}
function sendCommand(val) {
var $entry = createEntry('> ' + val);
addHistory(val);
$.ajax({
type: 'POST',
url: postUrl,
......@@ -41,20 +64,53 @@ var RedisCli = function(postUrl) {
addResponse($entry, response);
},
error: function() {
addResponse($entry, 'Failed to communicate with server.');
addError($entry, 'Failed to communicate with server.');
}
});
return false;
}
function submitCommand() {
var val = $input.val().trim();
if (!val.length)
return false;
sendCommand(val);
$input.val('');
}
return false;
function onKeyPress(e) {
if (e.keyCode == KEY_UP) {
historyPos -= 1;
if (historyPos < 0)
historyPos = 0;
if (historyPos < history.length)
$input.val(history[historyPos]);
} else
if (e.keyCode == KEY_DOWN) {
historyPos += 1;
if (historyPos >= history.length) {
$input.val('');
historyPos = history.length;
} else {
$input.val(history[historyPos]);
}
}
}
// Setup
$con.find('form').submit(submitCommand);
$input.keydown(onKeyPress);
$(window).resize(resizeConsole);
resizeConsole();
$input.focus();
sendCommand('ping');
};
......@@ -19,6 +19,12 @@
"{{ item }}"
{% elif type == 'binary' %}
"{{ item.decode('utf-8') }}"
{% elif type == 'TextWrapper' %}
<pre>{{ item }}</pre>
{% elif type == 'dict' %}
{% for k, v in item.items() %}
{{ loop.index }}) {{ k }} - {{ render(v, depth + 1) }}<br/>
{% endfor %}
{% else %}
{{ item }}
{% endif %}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment