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.
Files changed (67) hide show
  1. checksums.yaml +13 -5
  2. data/.travis.yml +1 -1
  3. data/CONTRIBUTING.md +4 -4
  4. data/Dockerfile +7 -0
  5. data/Gemfile +0 -1
  6. data/Gemfile.lock +23 -19
  7. data/README.rdoc +8 -5
  8. data/Rakefile +6 -6
  9. data/VERSION +1 -1
  10. data/features/api/run_command.feature +6 -0
  11. data/features/api/search_memcached_keys.feature +6 -0
  12. data/features/step_definitions/api/create_memcached_key.rb +1 -1
  13. data/features/step_definitions/api/list_memcached_keys.rb +1 -0
  14. data/features/step_definitions/api/run_command.rb +12 -0
  15. data/features/step_definitions/api/search_memcached_keys.rb +6 -0
  16. data/features/step_definitions/webapp/create_memcached_key.rb +13 -7
  17. data/features/step_definitions/webapp/delete_memcached_key.rb +7 -13
  18. data/features/step_definitions/webapp/edit_configs.rb +7 -0
  19. data/features/support/env.rb +3 -2
  20. data/features/webapp/edit_configs.feature +9 -0
  21. data/features/webapp/edit_memcached_key.feature +1 -1
  22. data/fig.yml +13 -0
  23. data/githubpage_idea +23 -0
  24. data/lib/api.rb +25 -7
  25. data/lib/extensions.rb +6 -0
  26. data/lib/extensions/memcached_command.rb +14 -0
  27. data/lib/extensions/memcached_connection.rb +9 -0
  28. data/lib/extensions/memcached_inspector.rb +12 -5
  29. data/lib/extensions/memcached_settings.rb +2 -2
  30. data/lib/public/images/favicon.png +0 -0
  31. data/lib/public/images/glyphicons-halflings.png +0 -0
  32. data/lib/public/images/logo.png +0 -0
  33. data/lib/public/images/org-logo.png +0 -0
  34. data/lib/public/images/search.png +0 -0
  35. data/lib/public/javascripts/angular/controllers.js +48 -3
  36. data/lib/public/javascripts/angular/filters.js +85 -0
  37. data/lib/public/javascripts/angular/routes.js +26 -0
  38. data/lib/public/javascripts/angular/services/notification.js +41 -6
  39. data/lib/public/javascripts/angular/services/query_params_singleton.js +10 -0
  40. data/lib/public/javascripts/angular/services/resources.js +13 -0
  41. data/lib/public/javascripts/application.js +38 -1
  42. data/lib/public/javascripts/humanize.js +473 -0
  43. data/lib/public/javascripts/humanize_duration.js +329 -0
  44. data/lib/public/javascripts/jquery-terminal.js +4335 -0
  45. data/lib/public/javascripts/underscore.js +5 -0
  46. data/lib/public/stylesheets/app.css +196 -10
  47. data/lib/public/stylesheets/buttons.css +107 -0
  48. data/lib/public/stylesheets/inputs.css +119 -0
  49. data/lib/public/stylesheets/jquery-terminal.css +184 -0
  50. data/lib/public/stylesheets/media_queries.css +162 -0
  51. data/lib/public/templates/config.html.erb +8 -0
  52. data/lib/public/templates/edit.html.erb +5 -3
  53. data/lib/public/templates/keys.html.erb +42 -31
  54. data/lib/public/templates/new.html.erb +6 -4
  55. data/lib/public/templates/show.html.erb +1 -1
  56. data/lib/public/templates/stats.html.erb +1 -2
  57. data/lib/routes.rb +3 -0
  58. data/lib/views/index.erb +24 -3
  59. data/lib/views/layout.erb +14 -1
  60. data/memcached-manager.gemspec +31 -5
  61. data/spec/lib/extensions/api_response_spec.rb +9 -7
  62. data/spec/lib/extensions/memcached_command_spec.rb +13 -0
  63. data/spec/lib/extensions/memcached_connection_spec.rb +9 -4
  64. data/spec/lib/extensions/memcached_inspector_spec.rb +38 -17
  65. data/spec/lib/extensions/memcached_settings_spec.rb +16 -16
  66. data/spec/spec_helper.rb +2 -4
  67. metadata +48 -21
@@ -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
- humanized_expires_time = Time.at(expires_time.to_i).to_s
38
- expired = false
39
- expired = true if Time.at(expires_time.to_i) < Time.now
40
- keys << { key: cache_key, bytes: bytes, expires_at: humanized_expires_time, expired: expired }
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['host'] if configured? session, 'host'
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['port'] if configured? session, 'port'
9
+ return session[:port] if configured? session, :port
10
10
  ENV['memcached_port'] || '11211'
11
11
  end
12
12
 
@@ -1,8 +1,43 @@
1
- controllers.controller('ListKeysController', ['$scope', 'Keys', '$state', '$location', function($scope, Keys, $state, $location) {
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
- noty({text: message, type: 'error'});
39
+ window.notification(message, 'error');
40
+ },
41
+ fatal: function(message) {
42
+ window.notification(message, 'fatal');
8
43
  },
9
44
  info: function(message) {
10
- noty({text: message, type: 'information'})
45
+ window.notification(message, 'info');
11
46
  },
12
47
  success: function(message) {
13
- noty({text: message, type: 'success'})
48
+ window.notification(message, 'success');
14
49
  }
15
50
  };
16
51
  });
@@ -0,0 +1,10 @@
1
+ services.factory('QueryParamsSingleton', function() {
2
+ return {
3
+ getQuery: function() {
4
+ return window.stateVariables.searchQuery;
5
+ },
6
+ setQuery: function(query) {
7
+ return window.stateVariables.searchQuery = query;
8
+ }
9
+ };
10
+ });
@@ -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 app = angular.module('memcached-keys', ['ngResource', 'ui.state', 'keysServices', 'AppControllers']);
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
+ });