memcached-manager 0.4.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
+ });