memcached-manager 0.4.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +13 -5
- data/.travis.yml +1 -1
- data/CONTRIBUTING.md +4 -4
- data/Dockerfile +7 -0
- data/Gemfile +0 -1
- data/Gemfile.lock +23 -19
- data/README.rdoc +8 -5
- data/Rakefile +6 -6
- data/VERSION +1 -1
- data/features/api/run_command.feature +6 -0
- data/features/api/search_memcached_keys.feature +6 -0
- data/features/step_definitions/api/create_memcached_key.rb +1 -1
- data/features/step_definitions/api/list_memcached_keys.rb +1 -0
- data/features/step_definitions/api/run_command.rb +12 -0
- data/features/step_definitions/api/search_memcached_keys.rb +6 -0
- data/features/step_definitions/webapp/create_memcached_key.rb +13 -7
- data/features/step_definitions/webapp/delete_memcached_key.rb +7 -13
- data/features/step_definitions/webapp/edit_configs.rb +7 -0
- data/features/support/env.rb +3 -2
- data/features/webapp/edit_configs.feature +9 -0
- data/features/webapp/edit_memcached_key.feature +1 -1
- data/fig.yml +13 -0
- data/githubpage_idea +23 -0
- data/lib/api.rb +25 -7
- data/lib/extensions.rb +6 -0
- data/lib/extensions/memcached_command.rb +14 -0
- data/lib/extensions/memcached_connection.rb +9 -0
- data/lib/extensions/memcached_inspector.rb +12 -5
- data/lib/extensions/memcached_settings.rb +2 -2
- data/lib/public/images/favicon.png +0 -0
- data/lib/public/images/glyphicons-halflings.png +0 -0
- data/lib/public/images/logo.png +0 -0
- data/lib/public/images/org-logo.png +0 -0
- data/lib/public/images/search.png +0 -0
- data/lib/public/javascripts/angular/controllers.js +48 -3
- data/lib/public/javascripts/angular/filters.js +85 -0
- data/lib/public/javascripts/angular/routes.js +26 -0
- data/lib/public/javascripts/angular/services/notification.js +41 -6
- data/lib/public/javascripts/angular/services/query_params_singleton.js +10 -0
- data/lib/public/javascripts/angular/services/resources.js +13 -0
- data/lib/public/javascripts/application.js +38 -1
- data/lib/public/javascripts/humanize.js +473 -0
- data/lib/public/javascripts/humanize_duration.js +329 -0
- data/lib/public/javascripts/jquery-terminal.js +4335 -0
- data/lib/public/javascripts/underscore.js +5 -0
- data/lib/public/stylesheets/app.css +196 -10
- data/lib/public/stylesheets/buttons.css +107 -0
- data/lib/public/stylesheets/inputs.css +119 -0
- data/lib/public/stylesheets/jquery-terminal.css +184 -0
- data/lib/public/stylesheets/media_queries.css +162 -0
- data/lib/public/templates/config.html.erb +8 -0
- data/lib/public/templates/edit.html.erb +5 -3
- data/lib/public/templates/keys.html.erb +42 -31
- data/lib/public/templates/new.html.erb +6 -4
- data/lib/public/templates/show.html.erb +1 -1
- data/lib/public/templates/stats.html.erb +1 -2
- data/lib/routes.rb +3 -0
- data/lib/views/index.erb +24 -3
- data/lib/views/layout.erb +14 -1
- data/memcached-manager.gemspec +31 -5
- data/spec/lib/extensions/api_response_spec.rb +9 -7
- data/spec/lib/extensions/memcached_command_spec.rb +13 -0
- data/spec/lib/extensions/memcached_connection_spec.rb +9 -4
- data/spec/lib/extensions/memcached_inspector_spec.rb +38 -17
- data/spec/lib/extensions/memcached_settings_spec.rb +16 -16
- data/spec/spec_helper.rb +2 -4
- metadata +48 -21
data/lib/extensions.rb
CHANGED
@@ -1,7 +1,12 @@
|
|
1
|
+
require 'sinatra'
|
2
|
+
require 'sinatra/base'
|
3
|
+
require 'sinatra/contrib'
|
4
|
+
|
1
5
|
#TODO: autoload extensions
|
2
6
|
require_relative 'extensions/errors'
|
3
7
|
require_relative 'extensions/memcached_settings'
|
4
8
|
require_relative 'extensions/memcached_connection'
|
9
|
+
require_relative 'extensions/memcached_command'
|
5
10
|
require_relative 'extensions/memcached_inspector'
|
6
11
|
require_relative 'extensions/api_response'
|
7
12
|
|
@@ -10,5 +15,6 @@ module Sinatra
|
|
10
15
|
helpers MemcachedSettings
|
11
16
|
helpers MemcachedConnection
|
12
17
|
helpers MemcachedInspector
|
18
|
+
helpers MemcachedCommand
|
13
19
|
helpers APIResponse
|
14
20
|
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Sinatra
|
2
|
+
module MemcachedCommand
|
3
|
+
def memcached_command options
|
4
|
+
host = options[:host]
|
5
|
+
port = options[:port]
|
6
|
+
command = options[:command]
|
7
|
+
|
8
|
+
memcached_connection = Net::Telnet::new("Host" => host, "Port" => port, "Timeout" => 3)
|
9
|
+
memcached_connection.cmd("String" => command, "Match" => /^END/) do |response|
|
10
|
+
return {response: response.inspect}
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -8,6 +8,15 @@ module Sinatra
|
|
8
8
|
@memcached.close
|
9
9
|
end
|
10
10
|
|
11
|
+
def memcached_connected? host, port
|
12
|
+
begin
|
13
|
+
Net::Telnet::new("Host" => host, "Port" => port)
|
14
|
+
rescue Errno::ECONNREFUSED
|
15
|
+
return false
|
16
|
+
end
|
17
|
+
true
|
18
|
+
end
|
19
|
+
|
11
20
|
def memcached_connection
|
12
21
|
@memcached
|
13
22
|
end
|
@@ -4,6 +4,7 @@ module Sinatra
|
|
4
4
|
host = options[:host]
|
5
5
|
port = options[:port]
|
6
6
|
key = options[:key]
|
7
|
+
query = options[:query]
|
7
8
|
|
8
9
|
inspect = inspector host, port
|
9
10
|
|
@@ -12,6 +13,11 @@ module Sinatra
|
|
12
13
|
inspect = inspect.select{|pair| pair[:key] == key }.first
|
13
14
|
end
|
14
15
|
|
16
|
+
# Filter keys by query/regexp
|
17
|
+
if !query.nil?
|
18
|
+
inspect = inspect.select{|pair| pair[:key] =~ Regexp.new(query, 'i') }
|
19
|
+
end
|
20
|
+
|
15
21
|
inspect
|
16
22
|
end
|
17
23
|
|
@@ -34,12 +40,13 @@ module Sinatra
|
|
34
40
|
localhost.cmd("String" => "stats cachedump #{slab_id} #{cache_dump_limit}", "Match" => /^END/) do |c|
|
35
41
|
matches = c.scan(/^ITEM (.+?) \[(\d+) b; (\d+) s\]$/).each do |key_data|
|
36
42
|
(cache_key, bytes, expires_time) = key_data
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
43
|
+
expires_in = expires_time.to_i
|
44
|
+
infinite = false
|
45
|
+
infinite = true if Time.at(expires_time.to_i) < Time.now
|
46
|
+
|
47
|
+
keys << { key: cache_key, bytes: bytes, expires_in: expires_in, infinite: infinite }
|
41
48
|
end
|
42
|
-
end
|
49
|
+
end
|
43
50
|
end
|
44
51
|
|
45
52
|
keys
|
@@ -1,12 +1,12 @@
|
|
1
1
|
module Sinatra
|
2
2
|
module MemcachedSettings
|
3
3
|
def memcached_host session
|
4
|
-
return session[
|
4
|
+
return session[:host] if configured? session, :host
|
5
5
|
ENV['memcached_host'] || 'localhost'
|
6
6
|
end
|
7
7
|
|
8
8
|
def memcached_port session
|
9
|
-
return session[
|
9
|
+
return session[:port] if configured? session, :port
|
10
10
|
ENV['memcached_port'] || '11211'
|
11
11
|
end
|
12
12
|
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
@@ -1,8 +1,43 @@
|
|
1
|
-
controllers.controller('
|
1
|
+
controllers.controller('EditConfigController', ['$scope', 'Configs', '$state', '$location', function($scope, Configs, $state, $location) {
|
2
|
+
$scope.config = Configs.get();
|
3
|
+
}]);
|
4
|
+
|
5
|
+
controllers.controller('UpdateConfigController', ['$state', '$scope', 'Configs', '$location', 'Notification', 'Response', function($state, $scope, Configs, $location, Notification, Response) {
|
6
|
+
$scope.updateConfig = function() {
|
7
|
+
config = { host: $scope.config.host, port: $scope.config.port };
|
8
|
+
|
9
|
+
$state.transitionTo('processingRequest');
|
10
|
+
|
11
|
+
Configs.save(config, function(response) {
|
12
|
+
if (Response.valid(response)){ // Checks if response is valid
|
13
|
+
Notification.success('Yay! Memcached config updated with success.');
|
14
|
+
|
15
|
+
// Refreshing list
|
16
|
+
$state.transitionTo('showKeys');
|
17
|
+
|
18
|
+
// Redirecting to / so the list gets refreshed.
|
19
|
+
$location.path('/');
|
20
|
+
} else {
|
21
|
+
Notification.error(Response.errors(response));
|
22
|
+
}
|
23
|
+
});
|
24
|
+
}
|
25
|
+
}]);
|
26
|
+
|
27
|
+
|
28
|
+
controllers.controller('ListKeysController', ['$scope', 'Keys', '$state', '$location', 'QueryParamsSingleton', function($scope, Keys, $state, $location, QueryParamsSingleton) {
|
2
29
|
$scope.keys = Keys.get();
|
30
|
+
$scope.query = QueryParamsSingleton.getQuery();
|
31
|
+
|
3
32
|
$scope.showKeys = function() {
|
4
33
|
$state.transitionTo('showKeys');
|
5
34
|
};
|
35
|
+
|
36
|
+
$scope.filterByQuery = function() {
|
37
|
+
QueryParamsSingleton.setQuery($scope.query);
|
38
|
+
|
39
|
+
$scope.keys = Keys.get({query: $scope.query});
|
40
|
+
}
|
6
41
|
}]);
|
7
42
|
|
8
43
|
controllers.controller('CreateKeyController', ['$scope', 'Keys', '$state', '$location', 'Notification', 'Response', function($scope, Keys, $state, $location, Notification, Response) {
|
@@ -74,9 +109,10 @@ controllers.controller('EditKeyController', ['$state', '$scope', 'Key', '$locati
|
|
74
109
|
$scope.pair = Key.find({key: $stateParams.key});
|
75
110
|
}]);
|
76
111
|
|
77
|
-
controllers.controller('StatsController', ['$state', '$scope', 'Stats', '$location', '$stateParams', function($state, $scope, Stats, $location, $stateParams) {
|
112
|
+
controllers.controller('StatsController', ['$state', '$scope', 'Stats', '$location', '$stateParams', 'Connection', function($state, $scope, Stats, $location, $stateParams, Connection) {
|
78
113
|
// Set up the current form text 'key' as key
|
79
114
|
$scope.stats = Stats.all();
|
115
|
+
$scope.connection = Connection.get();
|
80
116
|
}]);
|
81
117
|
|
82
118
|
controllers.controller('ShowKeyController', ['$state', '$scope', 'Key', '$location', '$stateParams', function($state, $scope, Key, $location, $stateParams) {
|
@@ -84,6 +120,15 @@ controllers.controller('ShowKeyController', ['$state', '$scope', 'Key', '$locati
|
|
84
120
|
$scope.pair = Key.find({key: $stateParams.key});
|
85
121
|
}]);
|
86
122
|
|
87
|
-
controllers.controller('BootstrapController', ['$state', function($state) {
|
123
|
+
controllers.controller('BootstrapController', ['$state', 'Connection', 'Notification', function($state, Connection, Notification) {
|
124
|
+
setInterval(function() {
|
125
|
+
Connection.get(function(response){
|
126
|
+
if (!response.connected) {
|
127
|
+
Notification.fatal('Memcached is out of reach. Please update settings with a valid memcached host and port.');
|
128
|
+
$state.transitionTo('editConfig');
|
129
|
+
};
|
130
|
+
});
|
131
|
+
}, 1000);
|
132
|
+
|
88
133
|
$state.transitionTo('showKeys');
|
89
134
|
}]);
|
@@ -0,0 +1,85 @@
|
|
1
|
+
filters.filter('configs', function() {
|
2
|
+
return function(args, keyToRead){
|
3
|
+
var key, value;
|
4
|
+
|
5
|
+
key = Object.keys(args)[0];
|
6
|
+
|
7
|
+
if (key != undefined) {
|
8
|
+
switch(keyToRead) {
|
9
|
+
case "numberOfKeys":
|
10
|
+
value = args[key]['curr_items'];
|
11
|
+
break;
|
12
|
+
case "bytes":
|
13
|
+
value = args[key]['bytes'];
|
14
|
+
break;
|
15
|
+
case "maxBytes":
|
16
|
+
value = args[key]['limit_maxbytes'];
|
17
|
+
break;
|
18
|
+
case "percentUsage":
|
19
|
+
value = (args[key]['bytes'] / args[key]['limit_maxbytes']).toFixed(2);
|
20
|
+
break;
|
21
|
+
}
|
22
|
+
}
|
23
|
+
|
24
|
+
return value;
|
25
|
+
}
|
26
|
+
}).filter('humanize_size', function() {
|
27
|
+
return function(args) {
|
28
|
+
return humanize.filesize(args);
|
29
|
+
}
|
30
|
+
}).filter('shortify_key', function() {
|
31
|
+
return function(args) {
|
32
|
+
if (args.length > 13) {
|
33
|
+
args = args.substring(0,13) + '...';
|
34
|
+
} else {
|
35
|
+
args;
|
36
|
+
}
|
37
|
+
return args;
|
38
|
+
}
|
39
|
+
}).filter('humanize_timestamp', function() {
|
40
|
+
return function(args) {
|
41
|
+
function humanizeTimestamp(ttl) {
|
42
|
+
var minutes,
|
43
|
+
hours,
|
44
|
+
days,
|
45
|
+
years,
|
46
|
+
humanizedTimestamp;
|
47
|
+
|
48
|
+
minutes = parseInt(ttl / 60);
|
49
|
+
hours = parseInt(minutes / 60);
|
50
|
+
days = parseInt(hours / 24);
|
51
|
+
years = parseInt(days / 365);
|
52
|
+
|
53
|
+
if (days < 30) {
|
54
|
+
humanizedTimestamp = parseInt(days) + 'd';
|
55
|
+
}
|
56
|
+
|
57
|
+
if (hours < 24) {
|
58
|
+
humanizedTimestamp = parseInt(hours) + 'h';
|
59
|
+
}
|
60
|
+
|
61
|
+
if (minutes < 60) {
|
62
|
+
humanizedTimestamp = parseInt(minutes) + 'm';
|
63
|
+
}
|
64
|
+
|
65
|
+
if (minutes == 0) {
|
66
|
+
humanizedTimestamp = ttl + 's';
|
67
|
+
}
|
68
|
+
|
69
|
+
return humanizedTimestamp;
|
70
|
+
}
|
71
|
+
expires_in = args.expires_in
|
72
|
+
infinite = args.infinite
|
73
|
+
|
74
|
+
var ttl = expires_in - Math.floor(Date.now() / 1000);
|
75
|
+
|
76
|
+
if (infinite) {
|
77
|
+
return '∞';
|
78
|
+
} else if (ttl <= 0) {
|
79
|
+
return 'expired'
|
80
|
+
} else {
|
81
|
+
return humanizeTimestamp(ttl);
|
82
|
+
}
|
83
|
+
}
|
84
|
+
});
|
85
|
+
|
@@ -13,6 +13,32 @@ app.config(function($stateProvider, $routeProvider){
|
|
13
13
|
}
|
14
14
|
}
|
15
15
|
})
|
16
|
+
.state('showStats', {
|
17
|
+
url: "/stats",
|
18
|
+
views: {
|
19
|
+
"keys": {
|
20
|
+
templateUrl: window.basePath + "/templates/keys.html.erb",
|
21
|
+
controller: 'ListKeysController'
|
22
|
+
},
|
23
|
+
"key": {
|
24
|
+
templateUrl: window.basePath + '/templates/stats.html.erb',
|
25
|
+
controller: 'StatsController'
|
26
|
+
}
|
27
|
+
}
|
28
|
+
})
|
29
|
+
.state('editConfig', {
|
30
|
+
url: '/config',
|
31
|
+
views: {
|
32
|
+
"keys": {
|
33
|
+
templateUrl: window.basePath + "/templates/keys.html.erb",
|
34
|
+
controller: 'ListKeysController'
|
35
|
+
},
|
36
|
+
'key': {
|
37
|
+
templateUrl: window.basePath + '/templates/config.html.erb',
|
38
|
+
controller: 'EditConfigController'
|
39
|
+
}
|
40
|
+
}
|
41
|
+
})
|
16
42
|
.state('processingRequest', {
|
17
43
|
url: "/processing",
|
18
44
|
views: {
|
@@ -1,16 +1,51 @@
|
|
1
|
+
window.notification = function(message, type) {
|
2
|
+
var color;
|
3
|
+
|
4
|
+
// type of notification to color mapping
|
5
|
+
switch(type) {
|
6
|
+
case 'success':
|
7
|
+
color = 'green';
|
8
|
+
break;
|
9
|
+
case 'error':
|
10
|
+
color = 'red';
|
11
|
+
break;
|
12
|
+
case 'fatal':
|
13
|
+
color = 'red-fatal';
|
14
|
+
break;
|
15
|
+
case 'info':
|
16
|
+
color = 'blue';
|
17
|
+
break;
|
18
|
+
}
|
19
|
+
|
20
|
+
// if past notification isn't the same as the currrent one
|
21
|
+
if ($('.notification-red-fatal').text() != message) {
|
22
|
+
// remove current notification div
|
23
|
+
if ($('#notification').length) { $('#notification').remove(); }
|
24
|
+
|
25
|
+
// add a new notification with of the desired type and message
|
26
|
+
$('.container').prepend('<div id="notification" class="notification-'+ color +'">'+ message +'</div>');
|
27
|
+
}
|
28
|
+
|
29
|
+
if (type != 'fatal'){
|
30
|
+
setTimeout(function(){
|
31
|
+
$('#notification').animate({height: 'toggle', easing: 'swing'}, 1000);
|
32
|
+
}, 1000);
|
33
|
+
}
|
34
|
+
}
|
35
|
+
|
1
36
|
services.factory('Notification', function() {
|
2
37
|
return {
|
3
|
-
alert: function(message) {
|
4
|
-
noty({text: message, type: 'alert'});
|
5
|
-
},
|
6
38
|
error: function(message) {
|
7
|
-
|
39
|
+
window.notification(message, 'error');
|
40
|
+
},
|
41
|
+
fatal: function(message) {
|
42
|
+
window.notification(message, 'fatal');
|
8
43
|
},
|
9
44
|
info: function(message) {
|
10
|
-
|
45
|
+
window.notification(message, 'info');
|
11
46
|
},
|
12
47
|
success: function(message) {
|
13
|
-
|
48
|
+
window.notification(message, 'success');
|
14
49
|
}
|
15
50
|
};
|
16
51
|
});
|
@@ -18,3 +18,16 @@ services.factory('Stats', ['$resource', function($resource) {
|
|
18
18
|
'all': { method: 'GET' }
|
19
19
|
});
|
20
20
|
}]);
|
21
|
+
|
22
|
+
services.factory('Configs', ['$resource', function($resource) {
|
23
|
+
return $resource(window.basePath + '/api/config.json', {}, {
|
24
|
+
'get': { method: 'GET' },
|
25
|
+
'update':{ method: 'POST' }
|
26
|
+
});
|
27
|
+
}]);
|
28
|
+
|
29
|
+
services.factory('Connection', ['$resource', function($resource) {
|
30
|
+
return $resource(window.basePath + '/api/status.json', {}, {
|
31
|
+
'get': { method: 'GET' },
|
32
|
+
});
|
33
|
+
}]);
|
@@ -3,10 +3,12 @@
|
|
3
3
|
// Setting up
|
4
4
|
var services = angular.module("keysServices", []);
|
5
5
|
var controllers = angular.module("AppControllers", []);
|
6
|
-
var
|
6
|
+
var filters = angular.module("AppFilters", []);
|
7
|
+
var app = angular.module('memcached-keys', ['ngResource', 'ui.state', 'keysServices', 'AppControllers', 'AppFilters']);
|
7
8
|
|
8
9
|
// This is a workaround to send data(post/put) as form instead of clean request payload
|
9
10
|
services.config(function ($httpProvider) {
|
11
|
+
$httpProvider.defaults.withCredentials = true;
|
10
12
|
$httpProvider.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
|
11
13
|
$httpProvider.defaults.headers.put['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
|
12
14
|
$httpProvider.defaults.transformRequest = function(data){
|
@@ -16,3 +18,38 @@ services.config(function ($httpProvider) {
|
|
16
18
|
return $.param(data);
|
17
19
|
}
|
18
20
|
});
|
21
|
+
|
22
|
+
|
23
|
+
$('#openTerminal').click(function() {
|
24
|
+
$('.bottom-container').css('width', '100%');
|
25
|
+
$('.bottom-container').css('height', '100%');
|
26
|
+
$('#terminal').terminal(function(command, term) {
|
27
|
+
var customCommands = ['clear', 'CLEAR', 'quit', 'QUIT'] ;
|
28
|
+
|
29
|
+
if(customCommands.indexOf(command) == -1) {
|
30
|
+
$.ajax({
|
31
|
+
url: "/api/run.json",
|
32
|
+
dataType: 'json',
|
33
|
+
method: 'POST',
|
34
|
+
data: {command: command},
|
35
|
+
}).done(function(response) {
|
36
|
+
term.echo(response.response);
|
37
|
+
});
|
38
|
+
} else {
|
39
|
+
switch(command){
|
40
|
+
case 'CLEAR':
|
41
|
+
term.clear();
|
42
|
+
break;
|
43
|
+
case 'quit':
|
44
|
+
case 'QUIT':
|
45
|
+
$('.bottom-container').css('width', 0);
|
46
|
+
$('.bottom-container').css('height', 0);
|
47
|
+
break;
|
48
|
+
}
|
49
|
+
}
|
50
|
+
}, {
|
51
|
+
greetings: 'This is a memcached session, all commands are sent to your configured server.\nTo clear the terminal screen, type the command CLEAR. Or to quit, type QUIT.',
|
52
|
+
name: 'memcached client',
|
53
|
+
enabled: false,
|
54
|
+
prompt: 'Memcached Command> '});
|
55
|
+
});
|