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

RedisCli improvements

parent 25fc7b8d
...@@ -3,7 +3,6 @@ import shlex ...@@ -3,7 +3,6 @@ import shlex
import warnings import warnings
from flask import request from flask import request
from flask.json import jsonify
from jinja2 import Markup from jinja2 import Markup
...@@ -12,7 +11,28 @@ from flask.ext.admin.babel import gettext ...@@ -12,7 +11,28 @@ from flask.ext.admin.babel import gettext
from flask.ext.admin._compat import VER 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): class RedisCli(BaseView):
"""
Simple redis console.
To use it, simply pass `Redis` connection object to the constructor.
"""
shlex_check = True shlex_check = True
""" """
shlex from stdlib does not work with unicode on 2.7.2 and lower. shlex from stdlib does not work with unicode on 2.7.2 and lower.
...@@ -26,6 +46,11 @@ class RedisCli(BaseView): ...@@ -26,6 +46,11 @@ class RedisCli(BaseView):
List of redis remapped commands. List of redis remapped commands.
""" """
excluded_commands = set(('pubsub', 'set_response_callback', 'from_url'))
"""
List of excluded commands.
"""
def __init__(self, redis, def __init__(self, redis,
name=None, category=None, endpoint=None, url=None): name=None, category=None, endpoint=None, url=None):
""" """
...@@ -51,6 +76,7 @@ class RedisCli(BaseView): ...@@ -51,6 +76,7 @@ class RedisCli(BaseView):
self.commands = {} self.commands = {}
self._inspect_commands() self._inspect_commands()
self._contribute_commands()
if self.shlex_check and VER < (2, 7, 3): 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. ' + 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): ...@@ -58,14 +84,34 @@ class RedisCli(BaseView):
'to False.') 'to False.')
def _inspect_commands(self): def _inspect_commands(self):
"""
Inspect connection object and extract command names.
"""
for name in dir(self.redis): for name in dir(self.redis):
if not name.startswith('_'): if not name.startswith('_'):
attr = getattr(self.redis, name) attr = getattr(self.redis, name)
if callable(attr): if callable(attr) and name not in self.excluded_commands:
doc = (getattr(attr, '__doc__', '') or '').strip() doc = (getattr(attr, '__doc__', '') or '').strip()
self.commands[name] = (attr, doc) 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): def _execute_command(self, name, args):
"""
Execute single command.
:param name:
Command name
:param args:
Command arguments
"""
# Do some remapping # Do some remapping
new_cmd = self.remapped_commands.get(name) new_cmd = self.remapped_commands.get(name)
if new_cmd: if new_cmd:
...@@ -79,6 +125,12 @@ class RedisCli(BaseView): ...@@ -79,6 +125,12 @@ class RedisCli(BaseView):
return self._result(handler(*args)) return self._result(handler(*args))
def _parse_cmd(self, cmd): def _parse_cmd(self, cmd):
"""
Parse command by using shlex module.
:param cmd:
Command to parse
"""
if VER < (2, 7, 3): if VER < (2, 7, 3):
# shlex can't work with unicode until 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'))) return tuple(x.decode('utf-8') for x in shlex.split(cmd.encode('utf-8')))
...@@ -86,19 +138,58 @@ class RedisCli(BaseView): ...@@ -86,19 +138,58 @@ class RedisCli(BaseView):
return tuple(shlex.split(cmd)) return tuple(shlex.split(cmd))
def _error(self, msg): def _error(self, msg):
"""
Format error message as HTTP response.
:param msg:
Message to format
"""
return Markup('<div class="error">%s</div>' % msg) return Markup('<div class="error">%s</div>' % msg)
def _result(self, result): def _result(self, result):
"""
Format result message as HTTP response.
:param msg:
Result to format.
"""
return self.render('admin/rediscli/response.html', return self.render('admin/rediscli/response.html',
type_name=lambda d: type(d).__name__, type_name=lambda d: type(d).__name__,
result=result) 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('/') @expose('/')
def console_view(self): def console_view(self):
"""
Console view.
"""
return self.render('admin/rediscli/console.html') return self.render('admin/rediscli/console.html')
@expose('/run/', methods=('POST',)) @expose('/run/', methods=('POST',))
def execute_view(self): def execute_view(self):
"""
AJAX API.
"""
try: try:
cmd = request.form.get('cmd').lower() cmd = request.form.get('cmd').lower()
if not cmd: if not cmd:
...@@ -109,6 +200,8 @@ class RedisCli(BaseView): ...@@ -109,6 +200,8 @@ class RedisCli(BaseView):
return self._error('Cli: Failed to parse command.') return self._error('Cli: Failed to parse command.')
return self._execute_command(parts[0], parts[1:]) return self._execute_command(parts[0], parts[1:])
except CommandError as err:
return self._error('Cli: %s' % err)
except Exception as ex: except Exception as ex:
logging.exception(ex) logging.exception(ex)
return self._error('Cli: %s' % ex) return self._error('Cli: %s' % ex)
...@@ -33,10 +33,14 @@ ...@@ -33,10 +33,14 @@
.console .cmd { .console .cmd {
background-color: #f5f5f5; background-color: #f5f5f5;
padding: 2px;
margin: 1px;
} }
.console .response { .console .response {
background-color: #f0f0f0; background-color: #f0f0f0;
padding: 2px;
margin: 1px;
} }
.console .error { .console .error {
......
var RedisCli = function(postUrl) { var RedisCli = function(postUrl) {
// Constants
var KEY_UP = 38;
var KEY_DOWN = 40;
var MAX_ITEMS = 128;
var $con = $('.console'); var $con = $('.console');
var $container = $con.find('.console-container'); var $container = $con.find('.console-container');
var $input = $con.find('input'); var $input = $con.find('input');
var history = [];
var historyPos = 0;
function resizeConsole() { function resizeConsole() {
var height = $(window).height(); var height = $(window).height();
...@@ -11,12 +19,16 @@ var RedisCli = function(postUrl) { ...@@ -11,12 +19,16 @@ var RedisCli = function(postUrl) {
} }
function scrollBottom() { function scrollBottom() {
$container.animate({scrollTop: $container.height()}, 100); $container.animate({scrollTop: $container[0].scrollHeight}, 100);
} }
function createEntry(cmd) { function createEntry(cmd) {
var $entry = $('<div>').addClass('entry').appendTo($container); var $entry = $('<div>').addClass('entry').appendTo($container);
$entry.append($('<div>').addClass('cmd').html(cmd)); $entry.append($('<div>').addClass('cmd').html(cmd));
if ($container.find('>div').length > MAX_ITEMS)
$container.find('>div:first-child').remove();
scrollBottom(); scrollBottom();
return $entry; return $entry;
} }
...@@ -25,14 +37,25 @@ var RedisCli = function(postUrl) { ...@@ -25,14 +37,25 @@ var RedisCli = function(postUrl) {
$entry.append($('<div>').addClass('response').html(response)); $entry.append($('<div>').addClass('response').html(response));
scrollBottom(); scrollBottom();
} }
function addError($entry, response) {
$entry.append($('<div>').addClass('response').addClass('error').html(response));
scrollBottom();
}
function submitCommand() { function addHistory(cmd) {
var val = $input.val().trim(); history.push(cmd);
if (!val.length)
return false;
if (history > MAX_ITEMS)
history.splice(0, 1);
historyPos = history.length;
}
function sendCommand(val) {
var $entry = createEntry('> ' + val); var $entry = createEntry('> ' + val);
addHistory(val);
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: postUrl, url: postUrl,
...@@ -41,20 +64,53 @@ var RedisCli = function(postUrl) { ...@@ -41,20 +64,53 @@ var RedisCli = function(postUrl) {
addResponse($entry, response); addResponse($entry, response);
}, },
error: function() { 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(''); $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 // Setup
$con.find('form').submit(submitCommand); $con.find('form').submit(submitCommand);
$input.keydown(onKeyPress);
$(window).resize(resizeConsole); $(window).resize(resizeConsole);
resizeConsole(); resizeConsole();
$input.focus(); $input.focus();
sendCommand('ping');
}; };
...@@ -19,6 +19,12 @@ ...@@ -19,6 +19,12 @@
"{{ item }}" "{{ item }}"
{% elif type == 'binary' %} {% elif type == 'binary' %}
"{{ item.decode('utf-8') }}" "{{ 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 %} {% else %}
{{ item }} {{ item }}
{% endif %} {% 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