memcached-manager 0.2.3 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. data/.rspec +2 -0
  2. data/.travis.yml +19 -0
  3. data/CONTRIBUTING.md +28 -0
  4. data/Gemfile +26 -0
  5. data/Gemfile.lock +143 -0
  6. data/README.rdoc +15 -1
  7. data/Rakefile +60 -0
  8. data/VERSION +1 -0
  9. data/config.ru +2 -6
  10. data/features/api/create_memcached_key.feature +6 -0
  11. data/features/api/delete_memcached_key.feature +6 -0
  12. data/features/api/list_memcached_keys.feature +6 -0
  13. data/features/api/set_memcached_info.feature +5 -0
  14. data/features/api/show_memcached_key.feature +10 -0
  15. data/features/api/show_memcached_stats.feature +5 -0
  16. data/features/api/update_memcached_key.feature +7 -0
  17. data/features/step_definitions/api/create_memcached_key.rb +21 -0
  18. data/features/step_definitions/api/delete_memcached_key.rb +7 -0
  19. data/features/step_definitions/api/list_memcached_keys.rb +16 -0
  20. data/features/step_definitions/api/show_memcached_key.rb +18 -0
  21. data/features/step_definitions/api/show_memcached_stats.rb +3 -0
  22. data/features/step_definitions/api/update_memcached_key.rb +18 -0
  23. data/features/step_definitions/set_memcached_info.rb +25 -0
  24. data/features/step_definitions/webapp/create_memcached_key.rb +10 -0
  25. data/features/step_definitions/webapp/delete_memcached_key.rb +16 -0
  26. data/features/step_definitions/webapp/edit_memcached_key.rb +3 -0
  27. data/features/step_definitions/webapp/list_memcached_keys.rb +4 -0
  28. data/features/step_definitions/webapp/show_memcached_key.rb +4 -0
  29. data/features/step_definitions/webapp/show_memcached_stats.rb +5 -0
  30. data/features/support/env.rb +28 -0
  31. data/features/support/hooks.rb +3 -0
  32. data/features/webapp/create_memcached_key.feature +9 -0
  33. data/features/webapp/delete_memcached_key.feature +8 -0
  34. data/features/webapp/edit_memcached_key.feature +9 -0
  35. data/features/webapp/list_memcached_keys.feature +8 -0
  36. data/features/webapp/show_memcached_key.feature +7 -0
  37. data/features/webapp/show_memcached_stats.feature +6 -0
  38. data/lib/api.rb +85 -0
  39. data/lib/extensions/api_response.rb +15 -0
  40. data/lib/extensions/errors.rb +19 -0
  41. data/lib/extensions/memcached_connection.rb +15 -0
  42. data/lib/extensions/memcached_inspector.rb +49 -0
  43. data/lib/extensions/memcached_settings.rb +18 -0
  44. data/lib/extensions.rb +14 -0
  45. data/lib/memcached-manager.rb +4 -0
  46. data/lib/public/images/glyphicons-halflings-white.png +0 -0
  47. data/lib/public/images/glyphicons-halflings.png +0 -0
  48. data/lib/public/javascripts/angular/controllers.js +89 -0
  49. data/lib/public/javascripts/angular/routes.js +66 -0
  50. data/lib/public/javascripts/angular/services/notification.js +16 -0
  51. data/lib/public/javascripts/angular/services/resources.js +20 -0
  52. data/lib/public/javascripts/angular/services/response.js +10 -0
  53. data/lib/public/javascripts/angular-resource.min.js +10 -0
  54. data/lib/public/javascripts/angular-ui-states.min.js +7 -0
  55. data/lib/public/javascripts/angular.min.js +161 -0
  56. data/lib/public/javascripts/application.js +18 -0
  57. data/lib/public/javascripts/jquery-1.9.1.min.js +4 -0
  58. data/lib/public/javascripts/noty/jquery.noty.js +547 -0
  59. data/lib/public/javascripts/noty/layouts/top.js +34 -0
  60. data/lib/public/javascripts/noty/themes/default.js +156 -0
  61. data/lib/public/javascripts/noty_config.js +25 -0
  62. data/lib/public/stylesheets/app.css +50 -0
  63. data/lib/public/stylesheets/base.css +269 -0
  64. data/lib/public/stylesheets/icons.css +492 -0
  65. data/lib/public/stylesheets/layout.css +58 -0
  66. data/lib/public/stylesheets/skeleton.css +242 -0
  67. data/lib/public/templates/edit.html.erb +6 -0
  68. data/lib/public/templates/keys.html.erb +36 -0
  69. data/lib/public/templates/new.html.erb +6 -0
  70. data/lib/public/templates/show.html.erb +2 -0
  71. data/lib/public/templates/stats.html.erb +8 -0
  72. data/lib/routes.rb +4 -0
  73. data/lib/views/index.erb +13 -0
  74. data/lib/views/layout.erb +55 -0
  75. data/lib/webapp.rb +15 -0
  76. data/memcached-manager.gemspec +154 -0
  77. data/spec/javascripts/angular/controllers/list_keys_controller_spec.js +1 -0
  78. data/spec/javascripts/angular/services/notification_spec.js +2 -0
  79. data/spec/javascripts/angular/services/resource_spec.js +2 -0
  80. data/spec/javascripts/angular/services/response_spec.js +2 -0
  81. metadata +80 -6
data/lib/api.rb ADDED
@@ -0,0 +1,85 @@
1
+ # Sinatra
2
+ require 'sinatra/base'
3
+ require 'sinatra/contrib'
4
+ require 'json'
5
+
6
+ # Libs
7
+ require 'dalli'
8
+ require 'net/telnet'
9
+
10
+ # Project files
11
+ require_relative './extensions'
12
+
13
+ module MemcachedManager
14
+ class API < Sinatra::Base
15
+ enable :inline_templates
16
+ enable :sessions
17
+
18
+ set :public_folder, 'public'
19
+
20
+ helpers Sinatra::MemcachedSettings
21
+ helpers Sinatra::MemcachedConnection
22
+ helpers Sinatra::MemcachedInspector
23
+ helpers Sinatra::Errors
24
+ helpers Sinatra::APIResponse
25
+
26
+ before do
27
+ content_type :json
28
+
29
+ setup_errors
30
+
31
+ try { setup_memcached(memcached_host(session), memcached_port(session)) }
32
+ end
33
+
34
+ after do
35
+ close_memcached
36
+ end
37
+
38
+ get '/config.json' do
39
+ api_response { { host: memcached_host(session), port: memcached_port(session) } }
40
+ end
41
+
42
+ post '/config.json' do
43
+ session['host'] = params['host']
44
+ session['port'] = params['port']
45
+
46
+ api_response { { host: memcached_host(session), port: memcached_port(session) } }
47
+ end
48
+
49
+ post '/keys.json' do
50
+ try { memcached_connection.set(params[:key], params[:value]) }
51
+
52
+ api_response { memcached_inspect(host: memcached_host(session), port: memcached_port(session), key: params[:key]) }
53
+ end
54
+
55
+ put '/keys.json' do
56
+ try { memcached_connection.replace(params[:key], params[:value]) }
57
+
58
+ api_response { memcached_inspect(host: memcached_host(session), port: memcached_port(session), key: params[:key]) }
59
+ end
60
+
61
+ get '/keys.json' do
62
+ api_response do
63
+ memcached_inspect(host: memcached_host(session), port: memcached_port(session))
64
+ end
65
+ end
66
+
67
+ get '/keys/:key.json' do
68
+ value = memcached_connection.get(params[:key])
69
+
70
+ try { raise 'Key not found.' if value.nil? }
71
+
72
+ api_response { { key: params[:key], value: value } }
73
+ end
74
+
75
+ delete '/keys/:key.json' do
76
+ try { memcached_connection.delete(params[:key]) }
77
+
78
+ api_response { { key: params[:key] } }
79
+ end
80
+
81
+ get '/stats.json' do
82
+ api_response { memcached_connection.stats }
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,15 @@
1
+ module Sinatra
2
+ module APIResponse
3
+ # This makes the api very DRY by taking care of API errors and converting the object
4
+ # or value passed by the block to json.
5
+ def api_response(&block)
6
+ raise 'No block given' unless block_given?
7
+
8
+ if errors.any?
9
+ return { errors: errors }.to_json
10
+ else
11
+ block.call.tap {|response| return response.to_json }
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,19 @@
1
+ module Sinatra
2
+ module Errors
3
+ def setup_errors
4
+ @errors = []
5
+ end
6
+
7
+ def try(&block)
8
+ begin
9
+ yield
10
+ rescue Exception => e
11
+ @errors << e.message
12
+ end
13
+ end
14
+
15
+ def errors
16
+ @errors
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,15 @@
1
+ module Sinatra
2
+ module MemcachedConnection
3
+ def setup_memcached host, port
4
+ @memcached = Dalli::Client.new("#{host}:#{port}")
5
+ end
6
+
7
+ def close_memcached
8
+ @memcached.close
9
+ end
10
+
11
+ def memcached_connection
12
+ @memcached
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,49 @@
1
+ module Sinatra
2
+ module MemcachedInspector
3
+ def memcached_inspect options
4
+ host = options[:host]
5
+ port = options[:port]
6
+ key = options[:key]
7
+
8
+ inspect = inspector host, port
9
+
10
+ # Filter by key if defined
11
+ if !key.nil?
12
+ inspect = inspect.select{|pair| pair[:key] == key }.first
13
+ end
14
+
15
+ inspect
16
+ end
17
+
18
+ private
19
+ def inspector host, port
20
+ # Looks horrible, got it from a gist... yet, it works.
21
+
22
+ keys = []
23
+ cache_dump_limit = 9_000 # It's over...
24
+
25
+ localhost = Net::Telnet::new("Host" => host, "Port" => port, "Timeout" => 3)
26
+ slab_ids = []
27
+
28
+ localhost.cmd("String" => "stats items", "Match" => /^END/) do |c|
29
+ matches = c.scan(/STAT items:(\d+):/)
30
+ slab_ids = matches.flatten.uniq
31
+ end
32
+
33
+ slab_ids.each do |slab_id|
34
+ localhost.cmd("String" => "stats cachedump #{slab_id} #{cache_dump_limit}", "Match" => /^END/) do |c|
35
+ matches = c.scan(/^ITEM (.+?) \[(\d+) b; (\d+) s\]$/).each do |key_data|
36
+ (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 }
41
+ end
42
+ end
43
+ end
44
+
45
+ keys
46
+ end
47
+ end
48
+ end
49
+
@@ -0,0 +1,18 @@
1
+ module Sinatra
2
+ module MemcachedSettings
3
+ def memcached_host session
4
+ return session['host'] if configured? session, 'host'
5
+ ENV['memcached_host'] || 'localhost'
6
+ end
7
+
8
+ def memcached_port session
9
+ return session['port'] if configured? session, 'port'
10
+ ENV['memcached_port'] || '11211'
11
+ end
12
+
13
+ private
14
+ def configured? session, parameter
15
+ session.key?(parameter)
16
+ end
17
+ end
18
+ end
data/lib/extensions.rb ADDED
@@ -0,0 +1,14 @@
1
+ #TODO: autoload extensions
2
+ require_relative 'extensions/errors'
3
+ require_relative 'extensions/memcached_settings'
4
+ require_relative 'extensions/memcached_connection'
5
+ require_relative 'extensions/memcached_inspector'
6
+ require_relative 'extensions/api_response'
7
+
8
+ module Sinatra
9
+ helpers Errors
10
+ helpers MemcachedSettings
11
+ helpers MemcachedConnection
12
+ helpers MemcachedInspector
13
+ helpers APIResponse
14
+ end
@@ -0,0 +1,4 @@
1
+ require 'api'
2
+ require 'webapp'
3
+ require 'routes'
4
+
@@ -0,0 +1,89 @@
1
+ controllers.controller('ListKeysController', ['$scope', 'Keys', '$state', '$location', function($scope, Keys, $state, $location) {
2
+ $scope.keys = Keys.get();
3
+ $scope.showKeys = function() {
4
+ $state.transitionTo('showKeys');
5
+ };
6
+ }]);
7
+
8
+ controllers.controller('CreateKeyController', ['$scope', 'Keys', '$state', '$location', 'Notification', 'Response', function($scope, Keys, $state, $location, Notification, Response) {
9
+ $scope.createKey = function() {
10
+ pair = { key: $scope.pair.key, value: $scope.pair.value };
11
+
12
+ $state.transitionTo('processingRequest');
13
+
14
+ Keys.save(pair, function(response) {
15
+ if (Response.valid(response)) { // Checks if response is valid
16
+ Notification.success('Yay! Key created with success.');
17
+
18
+ // Refreshing list
19
+ $state.transitionTo('showKey', pair);
20
+
21
+ // Redirecting to / so the list gets refreshed.
22
+ $location.path('/');
23
+ } else {
24
+ Notification.error(Response.errors(response));
25
+ }
26
+ });
27
+ }
28
+ }]);
29
+
30
+ controllers.controller('DeleteKeyController', ['$state', '$scope', 'Key', '$location', 'Notification', 'Response', function($state, $scope, Key, $location, Notification, Response){
31
+ $scope.removeKey = function(pair) {
32
+ $state.transitionTo('processingRequest');
33
+
34
+ Key.delete({key: pair.key}, function(response) {
35
+ if (Response.valid(response)){ // Checks if response is valid
36
+ Notification.info('Yay! Key deleted with success.');
37
+
38
+ // Refreshing list
39
+ $state.transitionTo('showKeys');
40
+
41
+ // Redirecting to / so the list gets refreshed.
42
+ $location.path('/');
43
+ } else {
44
+ Notification.error(Response.errors(response));
45
+ }
46
+ });
47
+ }
48
+ }]);
49
+
50
+ controllers.controller('UpdateKeyController', ['$state', '$scope', 'Key', 'Keys', '$location', 'Notification', 'Response', function($state, $scope, Key, Keys, $location, Notification, Response) {
51
+ $scope.updateKey = function() {
52
+ pair = { key: $scope.pair.key, value: $scope.pair.value };
53
+
54
+ $state.transitionTo('processingRequest');
55
+
56
+ Keys.update(pair, function(response) {
57
+ if (Response.valid(response)){ // Checks if response is valid
58
+ Notification.success('Yay! Key updated with success.');
59
+
60
+ // Refreshing list
61
+ $state.transitionTo('showKey', pair);
62
+
63
+ // Redirecting to / so the list gets refreshed.
64
+ $location.path('/');
65
+ } else {
66
+ Notification.error(Response.errors(response));
67
+ }
68
+ });
69
+ }
70
+ }]);
71
+
72
+ controllers.controller('EditKeyController', ['$state', '$scope', 'Key', '$location', '$stateParams', function($state, $scope, Key, $location, $stateParams) {
73
+ // Set up the current form text 'key' as key
74
+ $scope.pair = Key.find({key: $stateParams.key});
75
+ }]);
76
+
77
+ controllers.controller('StatsController', ['$state', '$scope', 'Stats', '$location', '$stateParams', function($state, $scope, Stats, $location, $stateParams) {
78
+ // Set up the current form text 'key' as key
79
+ $scope.stats = Stats.all();
80
+ }]);
81
+
82
+ controllers.controller('ShowKeyController', ['$state', '$scope', 'Key', '$location', '$stateParams', function($state, $scope, Key, $location, $stateParams) {
83
+ // Set up the current form text 'key' as key
84
+ $scope.pair = Key.find({key: $stateParams.key});
85
+ }]);
86
+
87
+ controllers.controller('BootstrapController', ['$state', function($state) {
88
+ $state.transitionTo('showKeys');
89
+ }]);
@@ -0,0 +1,66 @@
1
+ app.config(function($stateProvider, $routeProvider){
2
+ $stateProvider
3
+ .state('showKeys', {
4
+ url: "",
5
+ views: {
6
+ "keys": {
7
+ templateUrl: window.basePath + "/templates/keys.html.erb",
8
+ controller: 'ListKeysController'
9
+ },
10
+ "key": {
11
+ templateUrl: window.basePath + '/templates/stats.html.erb',
12
+ controller: 'StatsController'
13
+ }
14
+ }
15
+ })
16
+ .state('processingRequest', {
17
+ url: "/processing",
18
+ views: {
19
+ "keys": {
20
+ templateUrl: window.basePath + "/templates/keys.html.erb",
21
+ controller: 'ListKeysController'
22
+ },
23
+ "key": {
24
+ template: 'Processing...' // Show a image or somethin', lol
25
+ }
26
+ }
27
+ })
28
+ .state('editKey', {
29
+ url: "/edit/:key", // :key is going to be inside $stateParams whenever it's injected in a controller here
30
+ views: {
31
+ "keys": {
32
+ templateUrl: window.basePath + "/templates/keys.html.erb",
33
+ controller: 'ListKeysController'
34
+ },
35
+ "key": {
36
+ templateUrl: window.basePath + "/templates/edit.html.erb",
37
+ controller: 'EditKeyController'
38
+ }
39
+ }
40
+ })
41
+ .state('newKey', {
42
+ url: "/new",
43
+ views: {
44
+ "keys": {
45
+ templateUrl: window.basePath + "/templates/keys.html.erb",
46
+ controller: 'ListKeysController'
47
+ },
48
+ "key": {
49
+ templateUrl: window.basePath + "/templates/new.html.erb",
50
+ }
51
+ }
52
+ })
53
+ .state('showKey', {
54
+ url: "/key/:key", // :key is going to be inside $stateParams whenever it's injected in a controller here
55
+ views: {
56
+ "keys": {
57
+ templateUrl: window.basePath + "/templates/keys.html.erb",
58
+ controller: 'ListKeysController'
59
+ },
60
+ "key": {
61
+ templateUrl: window.basePath + "/templates/show.html.erb",
62
+ controller: 'ShowKeyController'
63
+ }
64
+ }
65
+ });
66
+ })
@@ -0,0 +1,16 @@
1
+ services.factory('Notification', function() {
2
+ return {
3
+ alert: function(message) {
4
+ noty({text: message, type: 'alert'});
5
+ },
6
+ error: function(message) {
7
+ noty({text: message, type: 'error'});
8
+ },
9
+ info: function(message) {
10
+ noty({text: message, type: 'information'})
11
+ },
12
+ success: function(message) {
13
+ noty({text: message, type: 'success'})
14
+ }
15
+ };
16
+ });
@@ -0,0 +1,20 @@
1
+ services.factory('Keys', ['$resource', function($resource) {
2
+ return $resource(window.basePath + '/api/keys.json', {}, {
3
+ 'get': { method: 'GET', isArray:true },
4
+ 'save': { method: 'POST' },
5
+ 'update': { method: 'PUT' }
6
+ });
7
+ }]);
8
+
9
+ services.factory('Key', ['$resource', function($resource) {
10
+ return $resource(window.basePath + '/api/keys/:key.json', { key: '@key' }, {
11
+ 'find': { method: 'GET' },
12
+ 'delete': { method: 'DELETE' }
13
+ });
14
+ }]);
15
+
16
+ services.factory('Stats', ['$resource', function($resource) {
17
+ return $resource(window.basePath + '/api/stats.json', {}, {
18
+ 'all': { method: 'GET' }
19
+ });
20
+ }]);
@@ -0,0 +1,10 @@
1
+ services.factory('Response', function() {
2
+ return {
3
+ valid: function(response) {
4
+ return typeof(response.errors) == 'undefined';
5
+ },
6
+ errors: function(response) {
7
+ return response.errors[0];
8
+ }
9
+ };
10
+ });
@@ -0,0 +1,10 @@
1
+ /*
2
+ AngularJS v1.0.2
3
+ (c) 2010-2012 Google, Inc. http://angularjs.org
4
+ License: MIT
5
+ */
6
+ (function(A,f,u){'use strict';f.module("ngResource",["ng"]).factory("$resource",["$http","$parse",function(v,w){function g(b,c){return encodeURIComponent(b).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(c?null:/%20/g,"+")}function l(b,c){this.template=b+="#";this.defaults=c||{};var a=this.urlParams={};j(b.split(/\W/),function(c){c&&b.match(RegExp("[^\\\\]:"+c+"\\W"))&&(a[c]=!0)});this.template=b.replace(/\\:/g,":")}function s(b,c,a){function f(d){var b=
7
+ {};j(c||{},function(a,x){var m;a.charAt&&a.charAt(0)=="@"?(m=a.substr(1),m=w(m)(d)):m=a;b[x]=m});return b}function e(a){t(a||{},this)}var y=new l(b),a=r({},z,a);j(a,function(d,g){var l=d.method=="POST"||d.method=="PUT"||d.method=="PATCH";e[g]=function(a,b,c,g){var i={},h,k=o,p=null;switch(arguments.length){case 4:p=g,k=c;case 3:case 2:if(q(b)){if(q(a)){k=a;p=b;break}k=b;p=c}else{i=a;h=b;k=c;break}case 1:q(a)?k=a:l?h=a:i=a;break;case 0:break;default:throw"Expected between 0-4 arguments [params, data, success, error], got "+
8
+ arguments.length+" arguments.";}var n=this instanceof e?this:d.isArray?[]:new e(h);v({method:d.method,url:y.url(r({},f(h),d.params||{},i)),data:h}).then(function(a){var b=a.data;if(b)d.isArray?(n.length=0,j(b,function(a){n.push(new e(a))})):t(b,n);(k||o)(n,a.headers)},p);return n};e.bind=function(d){return s(b,r({},c,d),a)};e.prototype["$"+g]=function(a,b,d){var c=f(this),i=o,h;switch(arguments.length){case 3:c=a;i=b;h=d;break;case 2:case 1:q(a)?(i=a,h=b):(c=a,i=b||o);case 0:break;default:throw"Expected between 1-3 arguments [params, success, error], got "+
9
+ arguments.length+" arguments.";}e[g].call(this,c,l?this:u,i,h)}});return e}var z={get:{method:"GET"},save:{method:"POST"},query:{method:"GET",isArray:!0},remove:{method:"DELETE"},"delete":{method:"DELETE"}},o=f.noop,j=f.forEach,r=f.extend,t=f.copy,q=f.isFunction;l.prototype={url:function(b){var c=this,a=this.template,f,b=b||{};j(this.urlParams,function(e,d){f=g(b[d]||c.defaults[d]||"",!0).replace(/%26/gi,"&").replace(/%3D/gi,"=").replace(/%2B/gi,"+");a=a.replace(RegExp(":"+d+"(\\W)"),f+"$1")});var a=
10
+ a.replace(/\/?#$/,""),e=[];j(b,function(a,b){c.urlParams[b]||e.push(g(b)+"="+g(a))});e.sort();a=a.replace(/\/*$/,"");return a+(e.length?"?"+e.join("&"):"")}};return s}])})(window,window.angular);
@@ -0,0 +1,7 @@
1
+ /**
2
+ * State-based routing for AngularJS
3
+ * @version v0.0.1 - 2013-04-06
4
+ * @link http://angular-ui.github.com/
5
+ * @license MIT License, http://www.opensource.org/licenses/MIT
6
+ */
7
+ (function(t,r,e){"use strict";function n(t,r){return g(new(g(function(){},{prototype:t})),r)}function a(t){return d(arguments,function(r){r!==t&&d(r,function(r,e){t.hasOwnProperty(e)||(t[e]=r)})}),t}function i(t,r,e){this.fromConfig=function(t,r,e){return h(t.template)?this.fromString(t.template,r):h(t.templateUrl)?this.fromUrl(t.templateUrl,r):h(t.templateProvider)?this.fromProvider(t.templateProvider,r,e):null},this.fromString=function(t,r){return p(t)?t(r):t},this.fromUrl=function(e,n){return p(e)&&(e=e(n)),null==e?null:t.get(e,{cache:r}).then(function(t){return t.data})},this.fromProvider=function(t,r,n){return e.invoke(t,null,n||{params:r})}}function o(t){function r(r){if(!/^\w+$/.test(r))throw Error("Invalid parameter name '"+r+"' in pattern '"+t+"'");if(i[r])throw Error("Duplicate parameter name '"+r+"' in pattern '"+t+"'");i[r]=!0,l.push(r)}function e(t){return t.replace(/[\\\[\]\^$*+?.()|{}]/g,"\\$&")}var n,a=/([:*])(\w+)|\{(\w+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g,i={},o="^",s=0,u=this.segments=[],l=this.params=[];this.source=t;for(var c,f,h;(n=a.exec(t))&&(c=n[2]||n[3],f=n[4]||("*"==n[1]?".*":"[^/]*"),h=t.substring(s,n.index),!(h.indexOf("?")>=0));)o+=e(h)+"("+f+")",r(c),u.push(h),s=a.lastIndex;h=t.substring(s);var p=h.indexOf("?");if(p>=0){var m=this.sourceSearch=h.substring(p);h=h.substring(0,p),this.sourcePath=t.substring(0,s+p),d(m.substring(1).split(/[&?]/),r)}else this.sourcePath=t,this.sourceSearch="";o+=e(h)+"$",u.push(h),this.regexp=RegExp(o),this.prefix=u[0]}function s(){this.compile=function(t){return new o(t)},this.isMatcher=function(t){return t instanceof o},this.$get=function(){return this}}function u(t){function r(t){var r=/^\^((?:\\[^a-zA-Z0-9]|[^\\\[\]\^$*+?.()|{}]+)*)/.exec(t.source);return null!=r?r[1].replace(/\\(.)/g,"$1"):""}function e(t,r){return t.replace(/\$(\$|\d{1,2})/,function(t,e){return r["$"===e?0:Number(e)]})}function n(t,r,e){if(!e)return!1;var n=r(e,t);return h(n)?n:!0}var a=[],i=null;this.rule=function(t){if(!p(t))throw Error("'rule' must be a function");return a.push(t),this},this.otherwise=function(t){if(m(t)){var r=t;t=function(){return r}}else if(!p(t))throw Error("'rule' must be a function");return i=t,this},this.when=function(a,i){var o,s;if(m(a)&&(a=t.compile(a)),t.isMatcher(a)){if(m(i))s=t.compile(i),i=function(t){return s.format(t)};else if(!p(i))throw Error("invalid 'handler' in when()");o=function(t){return n(t,i,a.exec(t.path(),t.search()))},o.prefix=m(a.prefix)?a.prefix:""}else{if(!(a instanceof RegExp))throw Error("invalid 'what' in when()");if(m(i))s=i,i=function(t){return e(s,t)};else if(!p(i))throw Error("invalid 'handler' in when()");if(a.global||a.sticky)throw Error("when() RegExp must not be global or sticky");o=function(t){return n(t,i,a.exec(t.path()))},o.prefix=r(a)}return this.rule(o)},this.$get=["$location","$rootScope",function(t,r){function e(){var r,e,n=a.length;for(r=0;n>r;r++)if(e=a[r](t)){m(e)&&t.replace().url(e);break}}return i&&a.push(i),r.$on("$locationChangeSuccess",e),{}}]}function l(t,r){function e(t){var r;if(m(t)){if(r=c[t],!r)throw Error("No such state '"+t+"'")}else if(r=c[t.name],!r||r!==t&&r.self!==t)throw Error("Invalid or unregistered state");return r}function i(a){a=n(a,{self:a,toString:function(){return this.name}});var i=a.name;if(!m(i)||i.indexOf("@")>=0)throw Error("State must have a valid name");if(c[i])throw Error("State '"+i+"'' is already defined");var o=u;if(h(a.parent))null!=a.parent&&(o=e(a.parent));else{var s=/^(.+)\.[^.]+$/.exec(i);null!=s&&(o=e(s[1]))}a.parent=o;var f=a.url;if(m(f))f=a.url="^"==f.charAt(0)?r.compile(f.substring(1)):(o.navigable||u).url.concat(f);else if($(f)&&p(f.exec)&&p(f.format)&&p(f.concat));else if(null!=f)throw Error("Invalid url '"+f+"' in state '"+a+"'");a.navigable=f?a:o?o.navigable:null;var w=a.params;if(w){if(!v(w))throw Error("Invalid params in state '"+a+"'");if(f)throw Error("Both params and url specicified in state '"+a+"'")}else w=a.params=f?f.parameters():a.parent.params;var b={};if(d(w,function(t){b[t]=!0}),o){d(o.params,function(t){if(!b[t])throw Error("Missing required parameter '"+t+"' in state '"+i+"'");b[t]=!1});var E=a.ownParams=[];d(b,function(t,r){t&&E.push(r)})}else a.ownParams=w;var x={};d(h(a.views)?a.views:{"":a},function(t,r){0>r.indexOf("@")&&(r=r+"@"+a.parent.name),x[r]=t}),a.views=x,a.path=o?o.path.concat(a):[];var P=a.includes=o?g({},o.includes):{};return P[i]=!0,a.resolve||(a.resolve={}),!a.abstract&&f&&t.when(f,function(t){l.transitionTo(a,t,!1)}),c[i]=a,a}function o(t,r){return $(t)?r=t:r.name=t,i(r),this}function s(t,r,i,o,s,c){function f(t,e,n,s,u){function l(e,n){d(e,function(e,a){f.push(r.when(m(e)?o.get(e):o.invoke(e,t.self,h)).then(function(t){n[a]=t}))})}var c,f=[s];n?c=e:(c={},d(t.params,function(t){c[t]=e[t]}));var h={$stateParams:c},p=u.globals={$stateParams:c};return l(t.resolve,p),p.$$state=t,d(t.views,function(e,n){var a=u[n]={$$controller:e.controller};f.push(r.when(i.fromConfig(e,c,h)||"").then(function(t){a.$template=t})),e.resolve!==t.resolve&&l(e.resolve,a)}),r.all(f).then(function(r){return a(u.globals,r[0].globals),d(t.views,function(t,r){a(u[r],u.globals)}),u})}function p(t,r,e){for(var n=0;e.length>n;n++){var a=e[n];if(t[a]!=r[a])return!1}return!0}var $=r.reject(Error("transition superseded")),v=r.reject(Error("transition prevented"));return l={params:{},current:u.self,$current:u,transition:null},l.transitionTo=function(a,i,m){if(h(m)||(m=!0),a=e(a),a.abstract)throw Error("Cannot transition to abstract state '"+a+"'");var g,b,E=a.path,x=l.$current,P=l.params,C=x.path,S=u.locals,y=[];for(g=0,b=E[g];b&&b===C[g]&&p(i,P,b.ownParams);g++,b=E[g])S=y[g]=b.locals;if(a===x&&S===x.locals)return l.transition=null,r.when(l.current);var j={};if(d(a.params,function(t){var r=i[t];j[t]=null!=r?r+"":null}),i=j,t.$broadcast("$stateChangeStart",a.self,i,x.self,P).defaultPrevented)return v;for(var R=r.when(S),I=g;E.length>I;I++,b=E[I])S=y[I]=n(S),R=f(b,i,b===a,R,S);var k=l.transition=R.then(function(){var r,e,n;if(l.transition!==k)return $;for(r=C.length-1;r>=g;r--)n=C[r],n.self.onExit&&o.invoke(n.self.onExit,n.self,n.locals.globals),n.locals=null;for(r=g;E.length>r;r++)e=E[r],e.locals=y[r],e.self.onEnter&&o.invoke(e.self.onEnter,e.self,e.locals.globals);l.$current=a,l.current=a.self,l.params=i,w(l.params,s),l.transition=null;var u=a.navigable;return m&&u&&c.url(u.url.format(u.locals.globals.$stateParams)),t.$broadcast("$stateChangeSuccess",a.self,i,x.self,P),l.current},function(e){return l.transition!==k?$:(l.transition=null,t.$broadcast("$stateChangeError",a.self,i,x.self,P,e),r.reject(e))});return k},l.is=function(t){return l.$current===e(t)},l.includes=function(t){return l.$current.includes[e(t).name]},l}var u,l,c={};u=i({name:"",url:"^",views:null,"abstract":!0}),u.locals={globals:{$stateParams:{}}},u.navigable=null,this.state=o,this.$get=s,s.$inject=["$rootScope","$q","$templateFactory","$injector","$stateParams","$location","$urlRouter"]}function c(t,r,e,n){var a={restrict:"ECA",terminal:!0,link:function(i,o,s){function u(){var a=t.$current&&t.$current.locals[f];if(a!==c)if(l&&(l.$destroy(),l=null),a){c=a,m.state=a.$$state,o.html(a.$template);var s=r(o.contents());if(l=i.$new(),a.$$controller){a.$scope=l;var u=e(a.$$controller,a);o.contents().data("$ngControllerController",u)}s(l),l.$emit("$viewContentLoaded"),l.$eval(h),n()}else c=null,m.state=null,o.html("")}var l,c,f=s[a.name]||s.name||"",h=s.onload||"",p=o.parent().inheritedData("$uiView");0>f.indexOf("@")&&(f=f+"@"+(p?p.state.name:""));var m={name:f,state:null};o.data("$uiView",m),i.$on("$stateChangeSuccess",u),u()}};return a}function f(t,r){function a(t){this.locals=t.locals.globals,this.params=this.locals.$stateParams}function i(){this.locals=null,this.params=null}function o(e,o){if(null!=o.redirectTo){var s,l=o.redirectTo;if(m(l))s=l;else{if(!p(l))throw Error("Invalid 'redirectTo' in when()");s=function(t,r){return l(t,r.path(),r.search())}}r.when(e,s)}else t.state(n(o,{parent:null,name:"route:"+encodeURIComponent(e),url:e,onEnter:a,onExit:i}));return u.push(o),this}function s(t,r,n){function a(t){return""!==t.name?t:e}var i={routes:u,params:n,current:e};return r.$on("$stateChangeStart",function(t,e,n,i){r.$broadcast("$routeChangeStart",a(e),a(i))}),r.$on("$stateChangeSuccess",function(t,e,n,o){i.current=a(e),r.$broadcast("$routeChangeSuccess",a(e),a(o)),w(n,i.params)}),r.$on("$stateChangeError",function(t,e,n,i,o,s){r.$broadcast("$routeChangeError",a(e),a(i),s)}),i}var u=[];a.$inject=["$$state"],this.when=o,this.$get=s,s.$inject=["$state","$rootScope","$routeParams"]}var h=r.isDefined,p=r.isFunction,m=r.isString,$=r.isObject,v=r.isArray,d=r.forEach,g=r.extend,w=r.copy;r.module("ui.util",["ng"]),r.module("ui.router",["ui.util"]),r.module("ui.state",["ui.router","ui.util"]),r.module("ui.compat",["ui.state"]),i.$inject=["$http","$templateCache","$injector"],r.module("ui.util").service("$templateFactory",i),o.prototype.concat=function(t){return new o(this.sourcePath+t+this.sourceSearch)},o.prototype.toString=function(){return this.source},o.prototype.exec=function(t,r){var e=this.regexp.exec(t);if(!e)return null;var n,a=this.params,i=a.length,o=this.segments.length-1,s={};for(n=0;o>n;n++)s[a[n]]=decodeURIComponent(e[n+1]);for(;i>n;n++)s[a[n]]=r[a[n]];return s},o.prototype.parameters=function(){return this.params},o.prototype.format=function(t){var r=this.segments,e=this.params;if(!t)return r.join("");var n,a,i,o=r.length-1,s=e.length,u=r[0];for(n=0;o>n;n++)i=t[e[n]],null!=i&&(u+=i),u+=r[n+1];for(;s>n;n++)i=t[e[n]],null!=i&&(u+=(a?"&":"?")+e[n]+"="+encodeURIComponent(i),a=!0);return u},r.module("ui.util").provider("$urlMatcherFactory",s),u.$inject=["$urlMatcherFactoryProvider"],r.module("ui.router").provider("$urlRouter",u),l.$inject=["$urlRouterProvider","$urlMatcherFactoryProvider"],r.module("ui.state").value("$stateParams",{}).provider("$state",l),c.$inject=["$state","$compile","$controller","$anchorScroll"],r.module("ui.state").directive("uiView",c),f.$inject=["$stateProvider","$urlRouterProvider"],r.module("ui.compat").provider("$route",f).directive("ngView",c)})(window,window.angular);