memcached-manager 0.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 (89) hide show
  1. data/.rspec +2 -0
  2. data/.travis.yml +19 -0
  3. data/Gemfile +24 -0
  4. data/Gemfile.lock +133 -0
  5. data/LICENSE.txt +20 -0
  6. data/README.rdoc +72 -0
  7. data/Rakefile +57 -0
  8. data/VERSION +1 -0
  9. data/config.ru +8 -0
  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 +27 -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/public/images/glyphicons-halflings-white.png +0 -0
  46. data/lib/public/images/glyphicons-halflings.png +0 -0
  47. data/lib/public/javascripts/angular/controllers.js +86 -0
  48. data/lib/public/javascripts/angular/routes.js +66 -0
  49. data/lib/public/javascripts/angular/services/notification.js +16 -0
  50. data/lib/public/javascripts/angular/services/resources.js +20 -0
  51. data/lib/public/javascripts/angular/services/response.js +10 -0
  52. data/lib/public/javascripts/angular-resource.min.js +10 -0
  53. data/lib/public/javascripts/angular-ui-states.min.js +7 -0
  54. data/lib/public/javascripts/angular.min.js +161 -0
  55. data/lib/public/javascripts/application.js +18 -0
  56. data/lib/public/javascripts/jquery-1.9.1.min.js +5 -0
  57. data/lib/public/javascripts/noty/jquery.noty.js +547 -0
  58. data/lib/public/javascripts/noty/layouts/top.js +34 -0
  59. data/lib/public/javascripts/noty/themes/default.js +156 -0
  60. data/lib/public/javascripts/noty_config.js +25 -0
  61. data/lib/public/stylesheets/app.css +50 -0
  62. data/lib/public/stylesheets/base.css +269 -0
  63. data/lib/public/stylesheets/icons.css +492 -0
  64. data/lib/public/stylesheets/layout.css +58 -0
  65. data/lib/public/stylesheets/skeleton.css +242 -0
  66. data/lib/public/templates/edit.html.erb +6 -0
  67. data/lib/public/templates/keys.html.erb +36 -0
  68. data/lib/public/templates/new.html.erb +6 -0
  69. data/lib/public/templates/show.html.erb +2 -0
  70. data/lib/public/templates/stats.html.erb +8 -0
  71. data/lib/views/index.erb +13 -0
  72. data/lib/views/layout.erb +52 -0
  73. data/lib/webapp.rb +14 -0
  74. data/memcached-manager.gemspec +145 -0
  75. data/spec/javascripts/angular/controllers/list_keys_controller_spec.js +26 -0
  76. data/spec/javascripts/angular/services/notification_spec.js +45 -0
  77. data/spec/javascripts/angular/services/resource_spec.js +41 -0
  78. data/spec/javascripts/angular/services/response_spec.js +26 -0
  79. data/spec/javascripts/helpers/SpecHelper.js +2 -0
  80. data/spec/javascripts/helpers/angular-mocks.js +1962 -0
  81. data/spec/javascripts/support/jasmine.yml +96 -0
  82. data/spec/javascripts/support/jasmine_helper.rb +11 -0
  83. data/spec/lib/extensions/api_response_spec.rb +28 -0
  84. data/spec/lib/extensions/error_spec.rb +23 -0
  85. data/spec/lib/extensions/memcached_connection_spec.rb +25 -0
  86. data/spec/lib/extensions/memcached_inspector_spec.rb +41 -0
  87. data/spec/lib/extensions/memcached_settings_spec.rb +53 -0
  88. data/spec/spec_helper.rb +41 -0
  89. metadata +252 -0
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
+ 'localhost'
6
+ end
7
+
8
+ def memcached_port session
9
+ return session['port'] if configured? session, 'port'
10
+ '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,86 @@
1
+ controllers.controller('ListKeysController', ['$scope', 'Keys', '$state', '$location', function($scope, Keys, $state, $location) {
2
+ $scope.keys = Keys.get();
3
+ }]);
4
+
5
+ controllers.controller('CreateKeyController', ['$scope', 'Keys', '$state', '$location', 'Notification', 'Response', function($scope, Keys, $state, $location, Notification, Response) {
6
+ $scope.createKey = function() {
7
+ pair = { key: $scope.pair.key, value: $scope.pair.value };
8
+
9
+ $state.transitionTo('processingRequest');
10
+
11
+ Keys.save(pair, function(response) {
12
+ if (Response.valid(response)) { // Checks if response is valid
13
+ Notification.success('Yay! Key created with success.');
14
+
15
+ // Refreshing list
16
+ $state.transitionTo('showKey', pair);
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
+ controllers.controller('DeleteKeyController', ['$state', '$scope', 'Key', '$location', 'Notification', 'Response', function($state, $scope, Key, $location, Notification, Response){
28
+ $scope.removeKey = function(pair) {
29
+ $state.transitionTo('processingRequest');
30
+
31
+ Key.delete({key: pair.key}, function(response) {
32
+ if (Response.valid(response)){ // Checks if response is valid
33
+ Notification.info('Yay! Key deleted with success.');
34
+
35
+ // Refreshing list
36
+ $state.transitionTo('showKeys');
37
+
38
+ // Redirecting to / so the list gets refreshed.
39
+ $location.path('/');
40
+ } else {
41
+ Notification.error(Response.errors(response));
42
+ }
43
+ });
44
+ }
45
+ }]);
46
+
47
+ controllers.controller('UpdateKeyController', ['$state', '$scope', 'Key', 'Keys', '$location', 'Notification', 'Response', function($state, $scope, Key, Keys, $location, Notification, Response) {
48
+ $scope.updateKey = function() {
49
+ pair = { key: $scope.pair.key, value: $scope.pair.value };
50
+
51
+ $state.transitionTo('processingRequest');
52
+
53
+ Keys.update(pair, function(response) {
54
+ if (Response.valid(response)){ // Checks if response is valid
55
+ Notification.success('Yay! Key updated with success.');
56
+
57
+ // Refreshing list
58
+ $state.transitionTo('showKey', pair);
59
+
60
+ // Redirecting to / so the list gets refreshed.
61
+ $location.path('/');
62
+ } else {
63
+ Notification.error(Response.errors(response));
64
+ }
65
+ });
66
+ }
67
+ }]);
68
+
69
+ controllers.controller('EditKeyController', ['$state', '$scope', 'Key', '$location', '$stateParams', function($state, $scope, Key, $location, $stateParams) {
70
+ // Set up the current form text 'key' as key
71
+ $scope.pair = Key.find({key: $stateParams.key});
72
+ }]);
73
+
74
+ controllers.controller('StatsController', ['$state', '$scope', 'Stats', '$location', '$stateParams', function($state, $scope, Stats, $location, $stateParams) {
75
+ // Set up the current form text 'key' as key
76
+ $scope.stats = Stats.all();
77
+ }]);
78
+
79
+ controllers.controller('ShowKeyController', ['$state', '$scope', 'Key', '$location', '$stateParams', function($state, $scope, Key, $location, $stateParams) {
80
+ // Set up the current form text 'key' as key
81
+ $scope.pair = Key.find({key: $stateParams.key});
82
+ }]);
83
+
84
+ controllers.controller('BootstrapController', ['$state', function($state) {
85
+ $state.transitionTo('showKeys');
86
+ }]);
@@ -0,0 +1,66 @@
1
+ app.config(function($stateProvider, $routeProvider){
2
+ $stateProvider
3
+ .state('showKeys', {
4
+ url: "",
5
+ views: {
6
+ "keys": {
7
+ templateUrl: "/templates/keys.html.erb",
8
+ controller: 'ListKeysController'
9
+ },
10
+ "key": {
11
+ templateUrl: '/templates/stats.html.erb',
12
+ controller: 'StatsController'
13
+ }
14
+ }
15
+ })
16
+ .state('processingRequest', {
17
+ url: "/processing",
18
+ views: {
19
+ "keys": {
20
+ templateUrl: "/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: "/templates/keys.html.erb",
33
+ controller: 'ListKeysController'
34
+ },
35
+ "key": {
36
+ templateUrl: "/templates/edit.html.erb",
37
+ controller: 'EditKeyController'
38
+ }
39
+ }
40
+ })
41
+ .state('newKey', {
42
+ url: "/new",
43
+ views: {
44
+ "keys": {
45
+ templateUrl: "/templates/keys.html.erb",
46
+ controller: 'ListKeysController'
47
+ },
48
+ "key": {
49
+ templateUrl: "/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: "/templates/keys.html.erb",
58
+ controller: 'ListKeysController'
59
+ },
60
+ "key": {
61
+ templateUrl: "/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('/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('/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('/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);