redis-commander
Version:
Redis web-based management tool written in node.js
1,452 lines (1,336 loc) • 63.4 kB
JavaScript
'use strict';
const CmdParser = require('cmdparser');
var cmdparser;
const losslessJSON = require('lossless-json');
const simpleObjRE = /^\s*[{\[]/;
/** wrapper object to hold redis connection related information */
const connections = {
/** list with all redis connections the server has,
* object with
* {
* conId: string,
* label: string,
* foldingChar: string,
* options: {host: string, port: number, type: string, db: number}
* }
*/
list: [],
/** list of all JSTree root tree objects, one per connection */
treeObjects: [],
/** find one connection by their connectionId */
findById: function(id) {
if (Array.isArray(this.list)) {
return this.list.find((c) => c.conId === id)
}
return null;
}
}
const fullMenu = {
'renameKey': {
icon: './images/icon-edit.png',
label: 'Rename Key',
action: renameKey
},
'addKey': {
icon: './images/icon-plus.png',
label: 'Add Key',
action: addKey
},
'custerNodes': {
icon: './images/icon-info.png',
label: 'Cluster Nodes',
action: clusterNodes
},
'refresh': {
icon: './images/icon-refresh.png',
label: 'Refresh',
action: function (obj) {
jQuery.jstree.reference('#keyTree').refresh(obj);
}
},
'export': {
icon: './images/icon-download.png',
label: 'Export Keys',
action: exportKey
},
'remKey': {
icon: './images/icon-trash.png',
label: 'Remove Key',
action: deleteKey
},
'remConnection': {
icon: './images/icon-trash.png',
label: 'Disconnect',
action: removeServer
}
};
function loadTree () {
$.get('apiv2/connection', function (isConnected) {
if (isConnected) {
$('#keyTree').on('loaded.jstree', function () {
var tree = getKeyTree();
if (tree) {
var root = tree.get_container().children('ul:eq(0)').children('li');
tree.open_node(root, null, true);
}
});
$.get('connections', function (data) {
if (data.connections) {
connections.list = data.connections;
data.connections.every(function (instance) {
// build root objects for jstree view on left side
var treeObj = {
id: instance.conId,
text: instance.label + ' (' + instance.options.host + ':' + instance.options.port + ':' + instance.options.db + ')',
state: {opened: false},
icon: getIconForType('root'),
children: true,
rel: 'root'
};
connections.treeObjects.push(treeObj);
return true;
});
}
return onJSTreeDataComplete();
function getJsTreeData(node, cb) {
if (node.id === '#') return cb(connections.treeObjects);
var dataUrl;
if (node.parent === '#') {
dataUrl = 'apiv2/keystree/' + encodeURIComponent(node.id) + '/';
}
else {
var root = getRootConnection(node);
var path = getFullKeyPath(node);
dataUrl = 'apiv2/keystree/' + encodeURIComponent(root) + '/' + encodeURIComponent(path) + '?absolute=false';
}
$.get({
url: dataUrl,
dataType: 'json'
}).done(function(nodeData) {
if (Array.isArray(nodeData.data)) {
nodeData.data.forEach(function(elem) {
if (elem.rel) elem.icon = getIconForType(elem.rel);
});
}
cb(nodeData.data)
}).fail(function(error) {
console.log('Error fetching data for node ' + node.id + ': ' + JSON.stringify(error));
if (error.responseJSON && error.responseJSON.connectionClosed) {
setRootConnectionNetworkError(true, node)
}
cb('Error fetching data');
});
}
function getIconForType(type) {
switch (type) {
case 'root': return 'images/treeRoot.png';
case 'string': return 'images/treeString.png';
case 'hash': return 'images/treeHash.png';
case 'set': return 'images/treeSet.png';
case 'list': return 'images/treeList.png';
case 'zset': return 'images/treeZSet.png';
case 'stream': return 'images/treeStream.png';
case 'binary': return 'images/treeBinary.png';
case 'ReJSON-RL': return 'images/treeJson.png';
default: return null;
}
}
function onJSTreeDataComplete () {
$('#keyTree').jstree({
core: {
data: getJsTreeData,
multiple : false,
check_callback : true,
//themes: {
// responsive: true
//}
},
contextmenu: {
items: function (node) {
var menu;
var rel = node.original.rel;
if (typeof rel === 'undefined' ) { // folder
menu = {
'addKey': fullMenu['addKey'],
'refresh': fullMenu['refresh'],
'export': fullMenu['export'],
'remKey': fullMenu['remKey']
}
}
else if (rel === 'root') { // root connection object (first level in tree-view)
menu = {
'addKey': fullMenu['addKey'],
'refresh': fullMenu['refresh'],
'export': fullMenu['export'],
'remConnection': fullMenu['remConnection']
}
if (node.id.startsWith('C:')) {
menu['custerNodes'] = fullMenu['custerNodes'];
}
}
else { // some redis key
menu = {
'renameKey': fullMenu['renameKey'],
'refresh': fullMenu['refresh'],
'export': fullMenu['export'],
'remKey': fullMenu['remKey']
}
}
if (redisReadOnly) {
delete menu['renameKey'];
delete menu['addKey'];
delete menu['remKey'];
}
return menu;
}
},
plugins: [ 'themes', 'contextmenu' ]
})
.on('select_node.jstree', treeNodeSelected)
.delegate('a', 'click', function (event, data2) {
event.preventDefault();
})
.on('keyup', function (e) {
var key = e.which;
// delete
if (key === 46) {
var node = getKeyTree().get_selected(true)[0];
// do not allow deletion of entire server, only keys within
if (node.parent !== '#') {
var connId = node.parents[node.parents.length-2];
deleteKey(connId, getFullKeyPath(node));
}
}
});
}
});
}
});
}
function treeNodeSelected (event, data) {
$('#body').html('Loading...');
var connectionId;
if (data.node.parent === '#') {
connectionId = data.node.id;
$.get('apiv2/server/' + connectionId + '/info')
.done(function (infoData, status) {
if (status !== 'success') {
return alert('Could not load server info');
}
if (typeof infoData === 'string') infoData = JSON.parse(infoData);
infoData.data.some(function (instance) {
if (instance.connectionId === connectionId) {
if (!instance.disabled) {
setRootConnectionNetworkError(false, data.node);
renderEjs('templates/serverInfo.ejs', instance, $('#body'), setupAddKeyButton);
}
else {
setRootConnectionNetworkError(true, data.node);
var html = '<h5>ERROR: ' + (instance.error ? instance.error : 'Server not available - cannot query status information.') + '</h5>';
$('#body').html(html);
setupAddKeyButton();
}
return true;
}
return false;
});
})
.fail(function (error) {
if (error.responseJSON) {
if (error.responseJSON.message) {
$('#body').html('<h5>Got ERROR: ' + error.responseJSON + '</h5>');
}
else {
$('#body').html('<h5>Network ERROR calling server...</h5>');
}
if (error.responseJSON.connectionClosed) setRootConnectionNetworkError(true, data.node);
}
});
} else {
connectionId = getRootConnection(data.node);
var path = getFullKeyPath(data.node);
return loadKey(connectionId, path);
}
}
/** finds root entry with connection object of the node given and changes icon to show disconnect state
*
* @param hasError flag to indicate a connection problem on a tree node
* @param node JSTree node the error occurred to get first sibling from tree root
*/
function setRootConnectionNetworkError (hasError, node) {
var tree = getKeyTree();
var root = getRootConnection(node);
var rootNode = tree.get_node(root);
if (hasError) tree.set_icon(rootNode, 'images/treeRootDisconnect.png');
else if (tree.get_icon(rootNode) === 'images/treeRootDisconnect.png') {
// only set icon if not already set to minimize redraws here...
tree.set_icon(rootNode, 'images/treeRoot.png');
}
}
function getFullKeyPath (node) {
if (node.parent === '#') {
return '';
}
return node.id.substr(getRootConnection(node).length + 1);
}
function getRootConnection (node) {
if (node.parent === '#') {
return node.id;
}
return node.parents[node.parents.length-2];
}
function loadKey (connectionId, key, index) {
if (index) {
$.get('apiv2/key/' + encodeURIComponent(connectionId) + '/' + encodeURIComponent(key) + '?index=' + index)
.done(processData)
.fail(errorHandler);
} else {
$.get('apiv2/key/' + encodeURIComponent(connectionId) + '/' + encodeURIComponent(key))
.done(processData)
.fail(errorHandler)
}
function processData (keyData, status) {
if (status !== 'success') {
return alert('Could not load key data');
}
setRootConnectionNetworkError(false, getKeyTree().get_selected(true)[0]);
if (typeof keyData === 'string') keyData = JSON.parse(keyData);
keyData.connectionId = connectionId;
if (uiConfig.clipboard) uiConfig.clipboard.destroy();
console.log('rendering type ' + keyData.type);
switch (keyData.type) {
case 'string':
selectTreeNodeString(keyData);
break;
case 'hash':
selectTreeNodeHash(keyData);
break;
case 'set':
selectTreeNodeSet(keyData);
break;
case 'list':
selectTreeNodeList(keyData);
break;
case 'zset':
selectTreeNodeZSet(keyData);
break;
case 'stream':
selectTreeNodeStream(keyData);
break;
case 'binary':
selectTreeNodeBinary(keyData);
break;
case 'ReJSON-RL':
selectTreeNodeReJSON(keyData);
break;
case 'none':
selectTreeNodeBranch(keyData);
break;
default:
var html = JSON.stringify(keyData);
$('#body').html(html);
resizeApp();
break;
}
}
function errorHandler(error) {
if (error.responseJSON) {
if (error.responseJSON.message) {
$('#body').html('<h5>Got ERROR: ' + error.responseJSON.message + '</h5>');
}
else {
$('#body').html('<h5>Network ERROR calling server...</h5>');
}
if (error.responseJSON.connectionClosed) setRootConnectionNetworkError(true, getKeyTree().get_selected(true)[0]);
}
}
}
function selectTreeNodeBranch (data) {
renderEjs('templates/editBranch.ejs', data, $('#body'));
}
function setupEditDataModals(idForm, idSaveBtn) {
$('#' + idForm).off('submit').on('submit', function(event) {
console.log('saving');
event.preventDefault();
var editForm = $(event.target);
var editModal = editForm.closest('.modal');
editModal.find('#' + idSaveBtn).button('loading');
$.post(editForm.attr('action'), editForm.serialize()
).done(function (data, status) {
console.log('saved', arguments);
})
.fail(function (err) {
console.log('save error', arguments);
alert('Could not save "' + err.statusText + '"');
})
.always(function () {
setTimeout(function () {
refreshTree();
getKeyTree().select_node(0);
editModal.find('#' + idSaveBtn).button('reset');
editModal.modal('hide');
}, 500);
});
});
}
function setupJsonInputValidator(idJsonCheckbox, idInput) {
var chkBox = $('#' + idJsonCheckbox);
chkBox.on('change', function(element) {
if (element.target.checked) addInputValidator(idInput, 'json');
else removeInputValidator(idInput);
});
chkBox.closest('.modal').on('hidden', function() {
removeInputValidator(idInput);
chkBox.prop('checked', false);
})
}
function registerModalFocus(idModal, idInput) {
var modal = $('#' + idModal);
modal.on('shown', function () {
modal.find('#' + idInput).trigger('focus')
});
}
function setupAddServerForm() {
var serverModal = $('#addServerModal');
// register add server form as ajax form to send bearer token too
$('#addServerForm').off('submit').on('submit', function (event) {
console.log('try connection to new redis server');
event.preventDefault();
$('#addServerBtn').prop('disabled', true).html('<i class="icon-refresh"></i> Saving');
var form = $(event.target);
$.post(form.attr('action'), form.serialize())
.done(function () {
if (arguments[0] && arguments[0].ok) {
console.log('Connect successful');
setTimeout(function() {
$(window).off('beforeunload', 'clearStorage');
location.reload();
}, 500);
}
else {
addServerError(arguments[0] ? arguments[0].message : 'Server error processing request');
}
})
.fail(function (err) {
console.log('connect error: ', arguments);
addServerError(err.statusText);
})
.always(function() {
$('#addServerBtn').prop('disabled', false).text('Connect...');
})
});
function addServerError(errMsg) {
alert('Could not connect to redis server "' + errMsg + '"');
serverModal.modal('hide');
}
// prepare all input elements
serverModal.find('#addServerGroupSentinel').hide();
serverModal.find('#addServerGroupCluster').hide();
serverModal.find('#serverType').on('change', function () {
switch ($(this).val()) {
case 'sentinel':
serverModal.find('#addServerGroupRedis').hide();
serverModal.find('#addServerGroupSentinel').show();
serverModal.find('#addServerGroupCluster').hide();
break;
case 'cluster':
serverModal.find('#addServerGroupRedis').hide();
serverModal.find('#addServerGroupSentinel').hide();
serverModal.find('#addServerGroupCluster').show();
break;
default: // 'redis'
serverModal.find('#addServerGroupRedis').show();
serverModal.find('#addServerGroupSentinel').hide();
serverModal.find('#addServerGroupCluster').hide();
}
});
serverModal.find('input:radio[name=sentinelPWType]').on('change', function() {
if ($(this).val() === 'sentinel') {
serverModal.find('#sentinelPassword').prop('disabled', false)
.prev('label').removeClass('muted');
}
else {
serverModal.find('#sentinelPassword').prop('disabled', true)
.prev('label').addClass('muted');
}
});
serverModal.find('input:radio[name=sentinelTLS]').on('change', function() {
if ($(this).val() === 'custom') {
serverModal.find('#sentinelTLSCA').prop('disabled', false)
.prev('label').removeClass('muted');
serverModal.find('#sentinelTLSPublicKey').prop('disabled', false)
.prev('label').removeClass('muted');
serverModal.find('#sentinelTLSPrivateKey').prop('disabled', false)
.prev('label').removeClass('muted');
serverModal.find('#sentinelTLSServerName').prop('disabled', false)
.prev('label').removeClass('muted');
}
else {
serverModal.find('#sentinelTLSCA').val('').prop('disabled', true)
.prev('label').addClass('muted');
serverModal.find('#sentinelTLSPublicKey').val('').prop('disabled', true)
.prev('label').addClass('muted');
serverModal.find('#sentinelTLSPrivateKey').val('').prop('disabled', true)
.prev('label').addClass('muted');
serverModal.find('#sentinelTLSServerName').val('').prop('disabled', true)
.prev('label').addClass('muted');
}
});
serverModal.find('input:radio[name=redisTLS]').on('change', function() {
if ($(this).val() === 'custom') {
serverModal.find('#redisTLSCA').prop('disabled', false)
.prev('label').removeClass('muted');
serverModal.find('#redisTLSPublicKey').prop('disabled', false)
.prev('label').removeClass('muted');
serverModal.find('#redisTLSPrivateKey').prop('disabled', false)
.prev('label').removeClass('muted');
serverModal.find('#redisTLSServerName').prop('disabled', false)
.prev('label').removeClass('muted');
}
else {
serverModal.find('#redisTLSCA').val('').prop('disabled', true)
.prev('label').addClass('muted');
serverModal.find('#redisTLSPublicKey').val('').prop('disabled', true)
.prev('label').addClass('muted');
serverModal.find('#redisTLSPrivateKey').val('').prop('disabled', true)
.prev('label').addClass('muted');
serverModal.find('#redisTLSServerName').val('').prop('disabled', true)
.prev('label').addClass('muted');
}
// disable following only for no tls, other setting allow this one
if ($(this).val() === 'no') {
serverModal.find('#clusterNoTlsValidation').prop('disabled', true)
.parent('label').addClass('muted');
}
else {
serverModal.find('#clusterNoTlsValidation').prop('disabled', false)
.parent('label').removeClass('muted');
}
});
serverModal.find('#label').trigger('focus');
}
function setupAddKeyButton (connectionId) {
var newKeyModal = $('#addKeyModal');
newKeyModal.find('#newStringValue').val('');
newKeyModal.find('#newFieldName').val('');
newKeyModal.find('#keyScore').val('');
newKeyModal.find('#addKeyConnectionId').val(connectionId);
newKeyModal.find('#addKeyValueIsJson').prop('checked', false);
newKeyModal.find('#addKeyFieldIsJson').prop('checked', false);
newKeyModal.find('#keyType').on('change', function () {
var score = newKeyModal.find('#scoreWrap');
if ($(this).val() === 'zset') {
score.show();
} else {
score.hide();
}
var field = newKeyModal.find('#fieldWrap');
if ($(this).val() === 'hash') {
field.show();
} else {
field.hide();
}
var fieldValue = newKeyModal.find('#fieldValueWrap');
var timestamp = newKeyModal.find('#timestampWrap');
if ($(this).val() === 'stream') {
fieldValue.show();
timestamp.show();
} else {
fieldValue.hide();
timestamp.hide();
}
});
}
function addNewKey() {
var newKeyModal = $('#addKeyModal');
var newKey = newKeyModal.find('#keyValue').val();
var connectionId = newKeyModal.find('#addKeyConnectionId').val();
var action = 'apiv2/key/' + encodeURIComponent(connectionId) + '/' + encodeURIComponent(newKey);
console.log('saving new key ' + newKey);
newKeyModal.find('#saveKeyButton').attr('disabled', 'disabled').html('<i class="icon-refresh"></i> Saving');
$.ajax({
url: action,
method: 'POST',
data: newKeyModal.find('#addKeyForm').serialize()
}).done(function() {
console.log('saved new key ' + newKey + ' at ' + connectionId);
}).fail(function(jqXHR, textStatus, errorThrown) {
console.log('save error for key ' + newKey + ': ' + textStatus);
alert('Could not save "' + errorThrown.statusText + '"');
}).always(function() {
setTimeout(function () {
newKeyModal.find('#saveKeyButton').prop('disabled', false).html('Save');
refreshTree();
newKeyModal.modal('hide');
}, 500);
});
}
function renameExistingKey() {
var modal = $('#renameKeyModal');
var oldKey = modal.find('#currentKeyName').val();
var newKey = modal.find('#renamedKeyName').val();
var connectionId = modal.find('#renameKeyConnectionId').val();
var action = 'apiv2/key/' + encodeURIComponent(connectionId) + '/' + encodeURIComponent(oldKey);
console.log('renaming ' + oldKey + ' to new key ' + newKey);
modal.find('#renameKeyButton').attr('disabled', 'disabled').html('<i class="icon-refresh"></i> Saving');
$.ajax({
url: action,
method: 'POST',
data: {key: newKey, force: modal.find('#forceRenameKey').is(':checked'), action: 'patch'}
}).done(function() {
console.log('renamed old key ' + newKey + ' at ' + connectionId);
}).fail(function(jqXHR, textStatus, errorThrown) {
console.log('rename error for key ' + oldKey + ': ' + textStatus);
alert('Could not rename "' + errorThrown + '" (HTTP ' + jqXHR.status + ')');
}).always(function(data, textStatus) {
// close modal for most return values incl. success
// but stay open if error message returned (key exists without overwrite)
if (textStatus === 'success' && data.error && data.error.code === 'ERR_KEY_EXISTS') {
modal.find('#renamedKeyName').after('<span class="text-error">' + data.error.title + '</span>')
.closest('.control-group').addClass('error');
}
else {
setTimeout(function() {
refreshTree();
modal.modal('hide');
}, 500);
}
modal.find('#renameKeyButton').prop('disabled', false).html('Save');
});
}
function selectTreeNodeString (data) {
renderEjs('templates/editString.ejs', data, $('#body'), function() {
var isJsonParsed = false;
try {
var jsonObject = data.value;
if (jsonObject.match(simpleObjRE)) {
jsonObject = losslessJSON.parse(data.value);
isJsonParsed = true;
}
$('#jqtree_string_div').jsonViewer(jsonObject, {withQuotes: true, withLinks: false, bigNumbers: true});
if ((uiConfig.jsonViewAsDefault & uiConfig.const.jsonViewString) > 0) dataUIFuncs.onModeJsonButtonClick('#editStringForm')
} catch (ex) {
$('#isJson').prop('checked', false);
$('#jqtree_string_div').text('Text is no valid JSON: ' + ex.message);
}
$('#stringValue').val(data.value);
// a this is json now assume it shall be json if it is object or array, but not for numbers
if (isJsonParsed && data.value.match(simpleObjRE)) {
$('#isJson').trigger('click');
}
if (!redisReadOnly) {
$('#editStringForm').off('submit').on('submit', function(event) {
console.log('saving');
event.preventDefault();
var editForm = $(event.target);
$('#saveKeyButton').attr('disabled', 'disabled').html('<i class="icon-refresh"></i> Saving');
$.post(editForm.attr('action'), editForm.serialize()
).done(function(data2, status) {
console.log('saved', arguments);
refreshTree();
getKeyTree().select_node(0);
})
.fail(function(err) {
console.log('save error', arguments);
alert('Could not save "' + err.statusText + '"');
})
.always(function() {
setTimeout(function() {
$('#saveKeyButton').prop('disabled', false).html('Save');
}, 500);
});
});
}
});
}
function selectTreeNodeBinary (data) {
// switch image from 'string' to 'binary', do not know this before really querying the value...
var tree = getKeyTree();
tree.set_icon(tree.get_selected(true)[0], 'images/treeBinary.png');
// only working for smaller data sets, no big binaries by now (everything load into browser)...
// calc number of 8bit-columns based on current "#body".width, static widths are taken from css classes
// TODO handle window resize
var idBody = $('#body');
data.offset = 0;
data.columns = Math.floor( (idBody.width() - 70 - 2*20) / 34 / 8 ) * 8;
data.value = BinaryView.base64DecToArr(data.value);
data.positions = [];
for (var i = 0; i < Math.ceil(data.value.length / data.columns); i += 1) {
data.positions.push( BinaryView.toHex(data.offset + i * data.columns, 8) );
}
renderEjs('templates/editBinary.ejs', data, idBody, function() {
console.log('edit binary template rendered');
idBody.find('.binaryView-hex').width(22 * data.columns);
idBody.find('.binaryView-char').width(12 * data.columns);
});
}
function selectTreeNodeHash (data) {
renderEjs('templates/editHash.ejs', data, $('#body'), function() {
console.log('edit hash template rendered');
if ((uiConfig.jsonViewAsDefault & uiConfig.const.jsonViewHash) > 0) dataUIFuncs.onModeJsonButtonClick()
});
}
function selectTreeNodeSet (data) {
renderEjs('templates/editSet.ejs', data, $('#body'), function() {
console.debug('edit set template rendered');
if ((uiConfig.jsonViewAsDefault & uiConfig.const.jsonViewSet) > 0) dataUIFuncs.onModeJsonButtonClick()
});
}
function selectTreeNodeList (data) {
if (data.items.length > 0) {
renderEjs('templates/editList.ejs', data, $('#body'), function() {
console.log('edit list template rendered');
if ((uiConfig.jsonViewAsDefault & uiConfig.const.jsonViewList) > 0) dataUIFuncs.onModeJsonButtonClick()
});
} else {
alert('Index out of bounds');
}
}
function selectTreeNodeZSet (data) {
if (data.items.length > 0) {
renderEjs('templates/editZSet.ejs', data, $('#body'), function() {
console.log('rendered zset template');
if ((uiConfig.jsonViewAsDefault & uiConfig.const.jsonViewZSet) > 0) dataUIFuncs.onModeJsonButtonClick()
});
} else {
alert('Index out of bounds');
}
}
function selectTreeNodeStream (data) {
renderEjs('templates/editStream.ejs', data, $('#body'), function() {
console.log('rendered stream template');
});
}
function selectTreeNodeReJSON(data) {
renderEjs('templates/editReJSON.ejs', data, $('#body'), function() {
console.log('rendered ReJSON template')
// do not check if it is valid json - assume it is as its stored server-side as ReJSON
// simplifies handling here compared to pure string data
const jsonObject = losslessJSON.parse(data.value);
$('#jqtree_json_div').jsonViewer(jsonObject, {withQuotes: true, withLinks: false, bigNumbers: true});
if ((uiConfig.jsonViewAsDefault & uiConfig.const.jsonViewString) > 0) dataUIFuncs.onModeJsonButtonClick('#editJsonForm')
$('#stringValue').val(data.value);
addInputValidator('stringValue', 'json');
if (!redisReadOnly) {
$('#editJsonForm').off('submit').on('submit', function(event) {
console.log('saving');
event.preventDefault();
var editForm = $(event.target);
$('#saveKeyButton').attr('disabled', 'disabled').html('<i class="icon-refresh"></i> Saving');
$.post(editForm.attr('action'), editForm.serialize()
).done(function(data2, status) {
console.log('saved', arguments);
refreshTree();
getKeyTree().select_node(0);
})
.fail(function(err) {
console.log('save error', arguments);
alert('Could not save "' + err.statusText + '"');
})
.always(function() {
setTimeout(function() {
$('#saveKeyButton').prop('disabled', false).html('Save');
}, 500);
});
});
}
});
}
function getKeyTree () {
return $.jstree.reference('#keyTree');
}
function refreshTree () {
getKeyTree().refresh();
}
function addKey (connectionId, key) {
if (typeof(connectionId) === 'object') {
// context menu click
const node = getKeyTree().get_node(connectionId.reference[0]);
connectionId = getRootConnection(node);
const foldingChar = connections.findById(connectionId).foldingChar
key = getFullKeyPath(node);
if (key.length > 0 && !key.endsWith(foldingChar)) {
key = key + foldingChar;
}
}
$('#keyValue').val(key);
$('#addKeyModal').modal('show');
setupAddKeyButton(connectionId);
}
function renameKey (connectionId, key) {
if (typeof(connectionId) === 'object') {
// context menu click
var node = getKeyTree().get_node(connectionId.reference[0]);
key = getFullKeyPath(node);
connectionId = getRootConnection(node);
}
var modal = $('#renameKeyModal');
modal.find('#currentKeyName').val(key);
modal.find('#currentKeyNameDisplay').text(key);
modal.find('#renamedKeyName').val(key);
modal.find('#renameKeyConnectionId').val(connectionId);
modal.find('#forceRenameKey').prop('checked', false);
modal.find('.text-error').remove();
modal.find('#renamedKeyName').closest('.control-group').removeClass('error');
modal.modal('show');
}
function exportKey (connectionId, key) {
var node = null;
if (typeof (connectionId) === 'object') {
// context menu click
node = getKeyTree().get_node(connectionId.reference[0]);
key = getFullKeyPath(node);
connectionId = getRootConnection(node);
}
$.ajax({
method: 'GET',
url: 'tools/forms/export',
success: function (res) {
var body = $('#body')
body.html(res);
body.find('#connectionExportField option[value="' + connectionId + '"]').attr('selected', true);
body.find('#exportKeyPrefix').val(key);
}
});
}
function deleteKey (connectionId, key) {
var node = null;
if (typeof(connectionId) === 'object') {
// context menu click
node = getKeyTree().get_node(connectionId.reference[0]);
key = getFullKeyPath(node);
connectionId = getRootConnection(node);
}
node = getKeyTree().get_node(connectionId);
const foldingChar = connections.findById(connectionId).foldingChar
// context menu or DEL key pressed on folder item
if (key.endsWith(foldingChar)) {
deleteBranch(connectionId, key);
return;
}
// delete this specific key only, no wildcard here
var result = confirm('Are you sure you want to delete "' + key + '" from "' + node.text + '"?');
if (result) {
$.post('apiv2/key/' + encodeURIComponent(connectionId) + '/' + encodeURIComponent(key) + '?action=delete', function (data, status) {
if (status !== 'success') {
return alert('Could not delete key');
}
refreshTree();
getKeyTree().select_node(-1);
$('#body').html('');
});
}
}
function decodeKey (connectionId, key) {
if (typeof(connectionId) === 'object') {
// context menu click
var node = getKeyTree().get_node(connectionId.reference[0]);
key = getFullKeyPath(node);
connectionId = getRootConnection(node);
}
$.post('apiv2/key/' + encodeURIComponent(connectionId) + '/' + encodeURIComponent(key) + '?action=decode', function (data, status) {
if (status !== 'success') {
return alert('Could not decode key');
}
$('#base64Button').html('Encode <small>base64</small>')
.off('click')
.on('click', function() {
encodeString(connectionId, key)
});
$('#stringValue').val(data);
});
}
function clusterNodes (connectionId) {
if (typeof(connectionId) === 'object') {
// context menu click
const node = getKeyTree().get_node(connectionId.reference[0]);
connectionId = getRootConnection(node);
const foldingChar = connections.findById(connectionId).foldingChar
}
$.get('apiv2/server/' + encodeURIComponent(connectionId) + '/cluster/nodes', function (data) {
if (data.error) {
alert("Error fetching cluster nodes:\n" + data.error);
}
else {
const modal = $('#clusterNodesModal');
const tab = modal.find('#clusterNodesTab');
tab.empty();
tab.append('<tr><th>ID</th><th>Node</th><th>Flags</th><th>Current Master Node</th>' +
'<th>Link-State</th></th><th>Ping</th><th>Last Pong Received</th><th>Config-Epoch</th><th>Slots</th></tr>');
data.data.forEach(function (n) {
tab.append(`<tr><td>${n.id}</td> <td>${n.node}</td> <td class="text-center">${n.flags}</td>` +
`<td>${n.primaryMaster}</td> <td class="text-center">${n.linkState}</td>` +
`<td class="text-center">${n.pingSent}</td><td class="text-center">${n.pongReceived}</td>` +
`<td class="text-center">${n.configEpoch}</td><td>${n.slots}</td></tr>`);
});
modal.modal('show');
}
});
}
function encodeString (connectionId, key) {
$.post('apiv2/encodeString/' + encodeURIComponent($('#stringValue').val()), function (data, status) {
if (status !== 'success') {
return alert('Could not encode key');
}
// needed to debounce
setTimeout(function() {
$('#base64Button').html('Decode <small>base64</small>')
.off('click')
.on('click', function() {
decodeKey(connectionId,key)
});
$('#stringValue').val(data);
}, 100);
});
}
function deleteBranch (connectionId, branchPrefix) {
const node = getKeyTree().get_node(connectionId);
const foldingChar = connections.findById(connectionId).foldingChar
const query = (branchPrefix.endsWith(foldingChar) ? branchPrefix : branchPrefix + foldingChar) + '*';
const result = confirm('Are you sure you want to delete "' + query + '" from "' + node.text + '"? This will delete all children as well!');
if (result) {
$.post('apiv2/keys/' + encodeURIComponent(connectionId) + '/' + encodeURIComponent(query) + '?action=delete', function (data, status) {
if (status !== 'success') {
return alert('Could not delete branch');
}
refreshTree();
getKeyTree().select_node(-1);
$('#body').html('');
});
}
}
function addListValue (connectionId, key) {
$('#key').val(key);
$('#addListValue').val('');
$('#addListConnectionId').val(connectionId);
$('#addListValueModal').modal('show');
}
function editListValue (connectionId, key, index, value) {
$('#editListConnectionId').val(connectionId);
$('#listKey').val(key);
$('#listIndex').val(index);
$('#listValue').val(value);
$('#listValueIsJson').prop('checked', false);
$('#editListValueModal').modal('show');
enableJsonValidationCheck(value, '#listValueIsJson');
}
function addSetMember (connectionId, key) {
$('#addSetKey').val(key);
$('#addSetMemberName').val('');
$('#addSetConnectionId').val(connectionId);
$('#addSetMemberModal').modal('show');
}
function editSetMember (connectionId, key, member) {
$('#setConnectionId').val(connectionId);
$('#setKey').val(key);
$('#setMember').val(member);
$('#setOldMember').val(member);
$('#setMemberIsJson').prop('checked', false);
$('#editSetMemberModal').modal('show');
enableJsonValidationCheck(member, '#setMemberIsJson');
}
function addZSetMember (connectionId, key) {
$('#addZSetKey').val(key);
$('#addZSetScore').val('');
$('#addZSetMemberName').val('');
$('#addZSetConnectionId').val(connectionId);
$('#addZSetMemberModal').modal('show');
}
function editZSetMember (connectionId, key, score, value) {
$('#zSetConnectionId').val(connectionId);
$('#zSetKey').val(key);
$('#zSetScore').val(score);
$('#zSetValue').val(value);
$('#zSetOldValue').val(value);
$('#zSetValueIsJson').prop('checked', false);
$('#editZSetMemberModal').modal('show');
enableJsonValidationCheck(value, '#zSetValueIsJson');
}
function addXSetMember (connectionId, key) {
$('#addXSetKey').val(key);
$('#addXSetTimestamp').val(Date.now()+'-0');
$('#addXSetField').val('');
$('#addXSetValue').val('');
$('#addXSetConnectionId').val(connectionId);
$('#addXSetMemberModal').modal('show');
}
function addHashField (connectionId, key) {
$('#addHashKey').val(key);
$('#addHashFieldName').val('');
$('#addHashFieldValue').val('');
$('#addHashConnectionId').val(connectionId);
$('#addHashFieldModal').modal('show');
}
function editHashField (connectionId, key, field, value) {
$('#hashConnectionId').val(connectionId);
$('#hashKey').val(key);
$('#hashField').val(field);
$('#hashFieldValue').val(value);
$('#hashFieldIsJson').prop('checked', false);
$('#editHashFieldModal').modal('show');
enableJsonValidationCheck(value, '#hashFieldIsJson');
}
function showHashField (connectionId, key, field) {
$.get('apiv2/hash/key/' + encodeURIComponent(connectionId) + '/' + encodeURIComponent(key) + '?field=' + encodeURIComponent(field))
.done(processData)
.fail(errorHandler)
function processData (keyData, status) {
if (status !== 'success') {
return alert('Could not load key data');
}
if (typeof keyData === 'string') keyData = JSON.parse(keyData);
var deferredRow = $('tr[data-deferred-field="' + field + '"');
if (deferredRow) {
// inject the data into the view
deferredRow.find('td.text-renderer').text(keyData.data);
// regenerate the json view
dataUIFuncs.createJSONViews(deferredRow.find('td.json-renderer'));
// remove the deferred attribute so the value is editable
deferredRow.removeAttr('data-deferred-field');
}
}
function errorHandler(error) {
if (error.responseJSON) {
if (error.responseJSON.message) {
$('#body').html('<h5>Got ERROR: ' + error.responseJSON.message + '</h5>');
}
else {
$('#body').html('<h5>Network ERROR calling server...</h5>');
}
if (error.responseJSON.connectionClosed) setRootConnectionNetworkError(true, getKeyTree().get_selected(true)[0]);
}
}
}
/** check if given string value is valid json and, if so enable validation
* for given field if this is an json object or array. Do not automatically
* enable validation on numbers or quoted strings. May be coincidence that this is json...
*
* @param {string} value string to check if valid json
* @param {string} isJsonCheckBox id string of checkbox element to activate validation
*/
function enableJsonValidationCheck(value, isJsonCheckBox) {
try {
// can use normal json.parse here as some bigint values changing are not relevant
JSON.parse(value);
// if this is valid json and is array or object assume we want validation active
if (value.match(simpleObjRE)) {
$(isJsonCheckBox).trigger('click');
}
}
catch (ex) {
// do nothing
}
}
function removeListElement () {
$('#listValue').val('REDISCOMMANDERTOMBSTONE');
$('#editListValueForm').trigger('submit');
}
function removeSetElement () {
$('#setMember').val('REDISCOMMANDERTOMBSTONE');
$('#editSetMemberForm').trigger('submit');
}
function removeZSetElement () {
$('#zSetValue').val('REDISCOMMANDERTOMBSTONE');
$('#editZSetMemberForm').trigger('submit');
}
function removeHashField () {
$('#hashFieldValue').val('REDISCOMMANDERTOMBSTONE');
$('#editHashFieldForm').trigger('submit');
}
function removeXSetElement (connectionId, key, timestamp) {
$.ajax({
url: 'apiv2/xset/member',
method: 'DELETE',
data: {
connectionId: connectionId,
key: key,
timestamp: timestamp
}
}).done(function(data, status) {
console.log('entry at timestamp ' + timestamp + ' deleted');
refreshTree();
getKeyTree().select_node(0);
})
.fail(function(err) {
console.log('delete stream entry error', arguments);
alert('Could not delete stream member at timestamp ' + timestamp + ': ' + err.statusText);
});
}
var redisCli = {
commandLineScrollTop: 0,
cliOpen: false,
hideCommandLineOutput: function hideCommandLineOutput() {
var output = $('#commandLineOutput');
if (output.is(':visible') && $('#lockCommandButton').hasClass('disabled')) {
output.slideUp(function() {
resizeApp();
});
redisCli.cliOpen = false;
redisCli.commandLineScrollTop = output.scrollTop() + 20;
$('#commandLineBorder').removeClass('show-vertical-scroll');
}
},
showCommandLineOutput: function showCommandLineOutput() {
var output = $('#commandLineOutput');
if (!output.is(':visible') && $('#lockCommandButton').hasClass('disabled')) {
output.slideDown(function() {
output.scrollTop(redisCli.commandLineScrollTop);
resizeApp();
});
redisCli.cliOpen = true;
$('#commandLineBorder').addClass('show-vertical-scroll');
}
},
loadCommandLine: function loadCommandLine() {
$('#commandLine').on('click', function() {
redisCli.showCommandLineOutput();
});
$('#app-container').on('click', function() {
redisCli.hideCommandLineOutput();
});
var readline = require('readline-browserify');
var output = document.getElementById('commandLineOutput');
var rl = readline.createInterface({
elementId: 'commandLine',
write: function(data) {
if (output.innerHTML.length > 0) {
output.innerHTML += '<br>';
}
output.innerHTML += escapeHtml(data);
output.scrollTop = output.scrollHeight;
},
completer: function(linePartial, callback) {
cmdparser.completer(linePartial, callback);
}
});
rl.setPrompt('redis> ');
rl.prompt();
rl.on('line', function(line) {
if (output.innerHTML.length > 0) {
output.innerHTML += '<br>';
}
output.innerHTML += '<span class="commandLineCommand">' + escapeHtml(line) + '</span>';
line = line.trim();
if (line.toLowerCase() === 'refresh') {
rl.prompt();
refreshTree();
rl.write('OK');
}
else {
$.post('apiv2/exec/' + encodeURIComponent($('#selectedConnection').val()), {cmd: line}, function(execData, status) {
rl.prompt();
if (status !== 'success') {
return alert('Could not delete branch');
}
try {
if (typeof execData === 'string') execData = JSON.parse(execData);
}
catch(ex) {
rl.write(execData);
return;
}
if (execData.hasOwnProperty('data')) execData = execData.data;
if (Array.isArray(execData)) {
for (var i = 0; i < execData.length; i++) {
rl.write((i + 1) + ') ' + JSON.stringify(execData[i]));
}
}
else {
rl.write(JSON.stringify(execData, null, ' '));
}
});
refreshTree();
}
});
},
setupCLIKeyEvents: function setupCLIKeyEvents() {
var ctrl_down = false;
var isMac = navigator.appVersion.indexOf('Mac') !== -1;
var cli = $('#_readline_cliForm input');
cli.on('keydown', function (e) {
var key = e.which;
//ctrl
if (key === 17 && isMac) {
ctrl_down = true;
}
//c
if (key === 67 && ctrl_down) {
redisCli.clearCLI();
e.preventDefault();
}
//esc
if (key === 27) {
redisCli.clearCLI();
e.preventDefault();
}
});
cli.on('keyup', function (e) {
var key = e.which;
//ctrl
if (key === 17 && isMac) {
ctrl_down = false;
}
});
},
clearCLI: function clearCLI () {
var cli = $('#_readline_cliForm input');
if (cli.val() == '') {
redisCli.hideCommandLineOutput();
} else {
cli.val('');
}
},
setupCommandLock: function setupCommandLock() {
$('#lockCommandButton').on('click', function () {
$(this).toggleClass('disabled');
});
}
};
/** Remove all input validators attached to an form element (keyup handler)
* as well as visual decorations applied
*
* @param {string|object} inputId id of input element or jquery object to remove handler and decoration from
*/
function removeInputValidator(inputId) {
if (typeof inputId === 'string') {
inputId = $(document.getElementById(inputId));
}
inputId.off('keyup').removeClass('validate-negative').removeClass('validate-positive');
}
/** Add data format validation function to an input element.
* The field gets decorated to visualize if input is valid for given data format.
*
* @param {string|object} inputId id of html input element to watch or jquery object
* @param {string} format data format to validate against, possible values: "json"
* @param {boolean} [currentState] optional start state to set now
*/
function addInputValidator(inputId, format, currentState) {
var input;
if (typeof inputId === 'string') {
input = $('#' + inputId)
}
else if (typeof inputId === 'object') {
input = inputId;
}
if (!input){
console.log('Invalid html id given to validate format: ', inputId);
return;
}
switch (format) {
case 'json':
input.on('keyup', validateInputAsJson);
break;
default:
console.log('Invalid format given to validate input: ', format);
return;
}
// set initial state if requested
if (typeof currentState === 'boolean') {
setValidationClasses(input.get(0), currentState);
}
else {
input.trigger('keyup');
}
}
/** method to check if a input field contains valid json and set visual accordingly.
*
*/
function validateInputAsJson() {
if (this.value) {
try {
JSON.parse(this.value);
setValidationClasses(this, true);
}
catch(e) {
setValidationClasses(this, false);
}
}
else {
setValidationClasses(this, false)
}
}
/** classes are only changed if not set right now
*
* @param {Element} element HTML DOM element to change validation classes
* @param {boolean} success true if positive validation class shall be assigned, false for error class
*/
function setValidationClasses(element, success) {
var add = (success ? 'validate-positive' : 'validate-negative');
var remove = (success ? 'validate-negative' : 'validate-positive');
if (element.className.indexOf(add) < 0) {
$(element).removeClass(remove).addClass(add);
}
}
var dataUIFuncs = {
/** function to toggle between display of raw strings and json object view.
*
* This function shows all raw text elements (class 'text-renderer') and
* hides json elements (class json-renderer), updating toggle buttons accordingly
*
* @param {string} parentSelector jquery selector with some parent element of the elements with raw text and json
* objects to show/hide
*/
onModeStringButtonClick: function onModeStringButtonClick(parentSelector) {
var parent = $(parentSelector || '#itemData');
parent.find('.text-renderer').css('display', 'inline-block');
parent.find('.json-renderer').css('display', 'none');
$('#viewModeJsonButton').css('display', 'inline');
$('#viewModeStringButton').css('display', 'none');
$('#saveKeyButton').css('display', 'inline');
},
/** function to toggle between display of raw strings and json object view.
*
* This function shows all json elements (class 'json-renderer') and
* hides text elements (class text-renderer), updating toggle buttons accordingly
*
* @param {string} [parentSelector] jquery selector with some parent element of the elements with raw text and json
* objects to show/hide, its using '#itemdata' as default if not set
*/
onModeJsonButtonClick: function onModeJsonButtonClick(parentSelector) {
var parent = $(parentSelector || '#itemData');
parent.find('.text-renderer').css('display', 'none');
parent.find('.json-renderer').css('display', 'inline-block');
$('#viewModeJsonButton').css('display', 'none');
$('#viewModeStringButton').css('display', 'inline');
$('#saveKeyButton').css('display', 'none');
},
/** this function generates the json object tree view for all elements containing
* the given selector. The raw text to convert to json is taken from the previous element in
* the dom tree - e.g. on table column where row X-1 is the text to convert to json and row X
* is the selected element to add json object too
*
* @param {string} jsonSelector jquery selector to find all elements where json views should be added
*/
createJSONViews: function createJSONViews(jsonSelector){
$(jsonSelector).each(function() {
var current = $(this);
var plain = current.prev().html();
try {
// display either as string if no valid json or as json object otherwise, ignore exception
current.jsonViewer(losslessJSON.parse(plain), {withQuotes: true, withLinks: false, bigNumbers: true});
}
catch(ex) {
// add json-viewer class manually to get same color/fonts
// calling jsonViewer() method instead gives quoted string like "blah\" blub" if it contains special chars
current.empty().append($('<span class="json-string">').text('"' + plain + '"'));
}
});
}
};
function escapeHtml (str) {
return str
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/\n/g, '<br>')
.replace(/\s/g, ' ');
}
/** Fetch the url give at filename from the server and render the content of this
* template with the data object. Afterwards the rendered html is added at the
* html element given.
*
* @param {string} filename url to retrieve as template
* @param {object} data object to use for rendering
* @param {object} element jquery html element to attach rendered data to
* @param {function} [callback] optional function to call when rendering html is attached to dom
*/
function rende