flipper-ui 0.21.0.rc1 → 0.22.1

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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/docs/ui/README.md +1 -1
  3. data/examples/ui/authorization.ru +12 -50
  4. data/examples/ui/basic.ru +12 -23
  5. data/lib/flipper/ui/action.rb +48 -1
  6. data/lib/flipper/ui/actions/actors_gate.rb +1 -1
  7. data/lib/flipper/ui/actions/features.rb +2 -2
  8. data/lib/flipper/ui/actions/file.rb +1 -1
  9. data/lib/flipper/ui/actions/groups_gate.rb +1 -1
  10. data/lib/flipper/ui/actions/percentage_of_actors_gate.rb +1 -1
  11. data/lib/flipper/ui/actions/percentage_of_time_gate.rb +1 -1
  12. data/lib/flipper/ui/decorators/feature.rb +3 -3
  13. data/lib/flipper/ui/middleware.rb +2 -1
  14. data/lib/flipper/ui/public/css/application.css +7 -0
  15. data/lib/flipper/ui/views/feature.erb +3 -3
  16. data/lib/flipper/ui/views/features.erb +1 -1
  17. data/lib/flipper/ui/views/layout.erb +4 -5
  18. data/lib/flipper/ui.rb +2 -2
  19. data/lib/flipper/version.rb +1 -1
  20. data/spec/flipper/ui/actions/actors_gate_spec.rb +19 -2
  21. data/spec/flipper/ui/actions/boolean_gate_spec.rb +18 -0
  22. data/spec/flipper/ui/actions/feature_spec.rb +18 -0
  23. data/spec/flipper/ui/actions/features_spec.rb +16 -3
  24. data/spec/flipper/ui/actions/file_spec.rb +0 -10
  25. data/spec/flipper/ui/actions/groups_gate_spec.rb +20 -3
  26. data/spec/flipper/ui/actions/percentage_of_actors_gate_spec.rb +18 -1
  27. data/spec/flipper/ui/actions/percentage_of_time_gate_spec.rb +18 -1
  28. data/spec/flipper/ui_spec.rb +0 -13
  29. metadata +6 -14
  30. data/lib/flipper/ui/public/octicons/LICENSE.txt +0 -9
  31. data/lib/flipper/ui/public/octicons/README.md +0 -1
  32. data/lib/flipper/ui/public/octicons/octicons-local.ttf +0 -0
  33. data/lib/flipper/ui/public/octicons/octicons.css +0 -236
  34. data/lib/flipper/ui/public/octicons/octicons.eot +0 -0
  35. data/lib/flipper/ui/public/octicons/octicons.svg +0 -200
  36. data/lib/flipper/ui/public/octicons/octicons.ttf +0 -0
  37. data/lib/flipper/ui/public/octicons/octicons.woff +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1892ab407f3f56ccddb2268e6ce53d7d02ef68a5004c914bc68a61c791c1a97f
4
- data.tar.gz: cff24cf7ed9f5521118d16af76cd2b8bc9b7ef90b345610450f2c2d836cc915d
3
+ metadata.gz: a619370e297b8fc379c57c8394cdfc798120ebfd9d4d091bc1902f3d9a2183be
4
+ data.tar.gz: a36c5d0a79836aec62188c04f5954a506d737e110a135028b3fc7d9d889a2579
5
5
  SHA512:
6
- metadata.gz: 1c3afd4b54efb3d6df85b1be0df6671083d208a451526a71483519eb748ff87aaf4b5ca1d4d4594981b6172fbb7e4b82e6e7cab3268d26c237f98da499c3bf18
7
- data.tar.gz: fc85ea7b8bae97535e34321b1c89ae1dcda2b834dc31876d91ddc7ec5387d52a2f912c53c1cf8a965c3442e655676923ee1040385fe3bd73df4b8c2a82af4c04
6
+ metadata.gz: b02d0683b248297b59e6013f816a6f09bbde28889e38201e0f41c89d29118f06f16061749d55041a786daf81d2d67da8e88885033c4819233794dbd55df3ea8d
7
+ data.tar.gz: 425e485bc419c1ae45554298df69a2d6cfdb4d2169fa2b0dc1b3e5e32009e5bafd27372796ab8a1bf3ac68dbc2d1be6a2474cbd7855b1d245fd61c8940e1cb81
data/docs/ui/README.md CHANGED
@@ -109,7 +109,7 @@ Minimal example for Rack:
109
109
  ```ruby
110
110
  # config.ru
111
111
 
112
- require 'flipper-ui'
112
+ require 'flipper/ui'
113
113
 
114
114
  adapter = Flipper::Adapters::Memory.new
115
115
  flipper = Flipper.new(adapter)
@@ -5,50 +5,13 @@
5
5
  # http://localhost:9999/
6
6
  #
7
7
  require 'bundler/setup'
8
- require "logger"
9
-
10
- require "flipper-ui"
8
+ require "flipper/ui"
11
9
  require "flipper/adapters/pstore"
12
- require "active_support/notifications"
13
10
 
14
11
  Flipper.register(:admins) { |actor|
15
12
  actor.respond_to?(:admin?) && actor.admin?
16
13
  }
17
14
 
18
- Flipper.register(:early_access) { |actor|
19
- actor.respond_to?(:early?) && actor.early?
20
- }
21
-
22
- # Setup logging of flipper calls.
23
- if ENV["LOG"] == "1"
24
- $logger = Logger.new(STDOUT)
25
- require "flipper/instrumentation/log_subscriber"
26
- Flipper::Instrumentation::LogSubscriber.logger = $logger
27
- end
28
-
29
- adapter = Flipper::Adapters::PStore.new
30
- flipper = Flipper.new(adapter, instrumenter: ActiveSupport::Notifications)
31
-
32
- Flipper::UI.configure do |config|
33
- # config.banner_text = 'Production Environment'
34
- # config.banner_class = 'danger'
35
- config.feature_creation_enabled = true
36
- config.feature_removal_enabled = true
37
- # config.show_feature_description_in_list = true
38
- config.descriptions_source = lambda do |_keys|
39
- {
40
- "search_performance_another_long_thing" => "Just to test feature name length.",
41
- "gauges_tracking" => "Should we track page views with gaug.es.",
42
- "unused" => "Not used.",
43
- "suits" => "Are suits necessary in business?",
44
- "secrets" => "Secrets are lies.",
45
- "logging" => "Log all the things.",
46
- "new_cache" => "Like the old cache but newer.",
47
- "a/b" => "Why would someone use a slash? I don't know but someone did. Let's make this really long so they regret using slashes. Please don't use slashes.",
48
- }
49
- end
50
- end
51
-
52
15
  # Example middleware to allow reading the Flipper UI but nothing else.
53
16
  class FlipperReadOnlyMiddleware
54
17
  def initialize(app)
@@ -67,18 +30,17 @@ class FlipperReadOnlyMiddleware
67
30
  end
68
31
 
69
32
  # You can uncomment these to get some default data:
70
- # flipper[:search_performance_another_long_thing].enable
71
- # flipper[:gauges_tracking].enable
72
- # flipper[:unused].disable
73
- # flipper[:suits].enable_actor Flipper::Actor.new('1')
74
- # flipper[:suits].enable_actor Flipper::Actor.new('6')
75
- # flipper[:secrets].enable_group :admins
76
- # flipper[:secrets].enable_group :early_access
77
- # flipper[:logging].enable_percentage_of_time 5
78
- # flipper[:new_cache].enable_percentage_of_actors 15
79
- # flipper["something/slashed"].add
80
-
81
- run Flipper::UI.app(flipper) { |builder|
33
+ # Flipper.enable(:search_performance_another_long_thing)
34
+ # Flipper.disable(:gauges_tracking)
35
+ # Flipper.disable(:unused)
36
+ # Flipper.enable_actor(:suits, Flipper::Actor.new('1'))
37
+ # Flipper.enable_actor(:suits, Flipper::Actor.new('6'))
38
+ # Flipper.enable_group(:secrets, :admins)
39
+ # Flipper.enable_percentage_of_time(:logging, 5)
40
+ # Flipper.enable_percentage_of_actors(:new_cache, 15)
41
+ # Flipper.add("a/b")
42
+
43
+ run Flipper::UI.app { |builder|
82
44
  builder.use Rack::Session::Cookie, secret: "_super_secret"
83
45
  builder.use FlipperReadOnlyMiddleware
84
46
  }
data/examples/ui/basic.ru CHANGED
@@ -9,9 +9,8 @@
9
9
  # http://localhost:9999/
10
10
  #
11
11
  require 'bundler/setup'
12
- require "flipper-ui"
12
+ require "flipper/ui"
13
13
  require "flipper/adapters/pstore"
14
- require "active_support/notifications"
15
14
 
16
15
  Flipper.register(:admins) { |actor|
17
16
  actor.respond_to?(:admin?) && actor.admin?
@@ -21,16 +20,6 @@ Flipper.register(:early_access) { |actor|
21
20
  actor.respond_to?(:early?) && actor.early?
22
21
  }
23
22
 
24
- # Setup logging of flipper calls.
25
- if ENV["LOG"] == "1"
26
- $logger = Logger.new(STDOUT)
27
- require "flipper/instrumentation/log_subscriber"
28
- Flipper::Instrumentation::LogSubscriber.logger = $logger
29
- end
30
-
31
- adapter = Flipper::Adapters::PStore.new
32
- flipper = Flipper.new(adapter, instrumenter: ActiveSupport::Notifications)
33
-
34
23
  Flipper::UI.configure do |config|
35
24
  # config.banner_text = 'Production Environment'
36
25
  # config.banner_class = 'danger'
@@ -53,17 +42,17 @@ Flipper::UI.configure do |config|
53
42
  end
54
43
 
55
44
  # You can uncomment these to get some default data:
56
- # flipper[:search_performance_another_long_thing].enable
57
- # flipper[:gauges_tracking].enable
58
- # flipper[:unused].disable
59
- # flipper[:suits].enable_actor Flipper::Actor.new('1')
60
- # flipper[:suits].enable_actor Flipper::Actor.new('6')
61
- # flipper[:secrets].enable_group :admins
62
- # flipper[:secrets].enable_group :early_access
63
- # flipper[:logging].enable_percentage_of_time 5
64
- # flipper[:new_cache].enable_percentage_of_actors 15
65
- # flipper["a/b"].add
45
+ # Flipper.enable(:search_performance_another_long_thing)
46
+ # Flipper.disable(:gauges_tracking)
47
+ # Flipper.disable(:unused)
48
+ # Flipper.enable_actor(:suits, Flipper::Actor.new('1'))
49
+ # Flipper.enable_actor(:suits, Flipper::Actor.new('6'))
50
+ # Flipper.enable_group(:secrets, :admins)
51
+ # Flipper.enable_group(:secrets, :early_access)
52
+ # Flipper.enable_percentage_of_time(:logging, 5)
53
+ # Flipper.enable_percentage_of_actors(:new_cache, 15)
54
+ # Flipper.add("a/b")
66
55
 
67
- run Flipper::UI.app(flipper) { |builder|
56
+ run Flipper::UI.app { |builder|
68
57
  builder.use Rack::Session::Cookie, secret: "_super_secret"
69
58
  }
@@ -26,6 +26,36 @@ module Flipper
26
26
  'delete'.freeze,
27
27
  ]).freeze
28
28
 
29
+ SOURCES = {
30
+ bootstrap_css: {
31
+ src: "https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css".freeze,
32
+ hash: "sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm".freeze
33
+ }.freeze,
34
+ jquery_js: {
35
+ src: "https://code.jquery.com/jquery-3.2.1.slim.min.js".freeze,
36
+ hash: "sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN".freeze
37
+ }.freeze,
38
+ popper_js: {
39
+ src: "https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js".freeze,
40
+ hash: "sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q".freeze
41
+ }.freeze,
42
+ bootstrap_js: {
43
+ src: "https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js".freeze,
44
+ hash: "sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl".freeze
45
+ }.freeze
46
+ }.freeze
47
+ SCRIPT_SRCS = SOURCES.values_at(:jquery_js, :popper_js, :bootstrap_js).map { |s| s[:src] }
48
+ STYLE_SRCS = SOURCES.values_at(:bootstrap_css).map { |s| s[:src] }
49
+ CONTENT_SECURITY_POLICY = <<-CSP.delete("\n")
50
+ default-src 'none';
51
+ img-src 'self';
52
+ font-src 'self';
53
+ script-src 'report-sample' 'self' #{SCRIPT_SRCS.join(' ')};
54
+ style-src 'self' 'unsafe-inline' #{STYLE_SRCS.join(' ')};
55
+ style-src-attr 'unsafe-inline' ;
56
+ style-src-elem 'self' #{STYLE_SRCS.join(' ')};
57
+ CSP
58
+
29
59
  # Public: Call this in subclasses so the action knows its route.
30
60
  #
31
61
  # regex - The Regexp that this action should run for.
@@ -130,6 +160,7 @@ module Flipper
130
160
  # Returns a response.
131
161
  def view_response(name)
132
162
  header 'Content-Type', 'text/html'
163
+ header 'Content-Security-Policy', CONTENT_SECURITY_POLICY
133
164
  body = view_with_layout { view_without_layout name }
134
165
  halt [@code, @headers, [body]]
135
166
  end
@@ -151,7 +182,7 @@ module Flipper
151
182
  # location - The String location to set the Location header to.
152
183
  def redirect_to(location)
153
184
  status 302
154
- header 'Location', "#{script_name}#{location}"
185
+ header 'Location', "#{script_name}#{Rack::Utils.escape_path(location)}"
155
186
  halt [@code, @headers, ['']]
156
187
  end
157
188
 
@@ -237,6 +268,22 @@ module Flipper
237
268
  def valid_request_method?
238
269
  VALID_REQUEST_METHOD_NAMES.include?(request_method_name)
239
270
  end
271
+
272
+ def bootstrap_css
273
+ SOURCES[:bootstrap_css]
274
+ end
275
+
276
+ def bootstrap_js
277
+ SOURCES[:bootstrap_js]
278
+ end
279
+
280
+ def popper_js
281
+ SOURCES[:popper_js]
282
+ end
283
+
284
+ def jquery_js
285
+ SOURCES[:jquery_js]
286
+ end
240
287
  end
241
288
  end
242
289
  end
@@ -27,7 +27,7 @@ module Flipper
27
27
  value = params['value'].to_s.strip
28
28
 
29
29
  if Util.blank?(value)
30
- error = Rack::Utils.escape("#{value.inspect} is not a valid actor value.")
30
+ error = "#{value.inspect} is not a valid actor value."
31
31
  redirect_to("/features/#{feature.key}/actors?error=#{error}")
32
32
  end
33
33
 
@@ -49,14 +49,14 @@ module Flipper
49
49
  value = params['value'].to_s.strip
50
50
 
51
51
  if Util.blank?(value)
52
- error = Rack::Utils.escape("#{value.inspect} is not a valid feature name.")
52
+ error = "#{value.inspect} is not a valid feature name."
53
53
  redirect_to("/features/new?error=#{error}")
54
54
  end
55
55
 
56
56
  feature = flipper[value]
57
57
  feature.add
58
58
 
59
- redirect_to "/features/#{Rack::Utils.escape_path(value)}"
59
+ redirect_to "/features/#{value}"
60
60
  end
61
61
  end
62
62
  end
@@ -5,7 +5,7 @@ module Flipper
5
5
  module UI
6
6
  module Actions
7
7
  class File < UI::Action
8
- route %r{(images|css|js|octicons)/.*\Z}
8
+ route %r{(images|css|js)/.*\Z}
9
9
 
10
10
  def get
11
11
  Rack::File.new(public_path).call(request.env)
@@ -35,7 +35,7 @@ module Flipper
35
35
 
36
36
  redirect_to("/features/#{feature.key}")
37
37
  else
38
- error = Rack::Utils.escape("The group named #{value.inspect} has not been registered.")
38
+ error = "The group named #{value.inspect} has not been registered."
39
39
  redirect_to("/features/#{feature.key}/groups?error=#{error}")
40
40
  end
41
41
  end
@@ -16,7 +16,7 @@ module Flipper
16
16
  begin
17
17
  feature.enable_percentage_of_actors params['value']
18
18
  rescue ArgumentError => exception
19
- error = Rack::Utils.escape("Invalid percentage of actors value: #{exception.message}")
19
+ error = "Invalid percentage of actors value: #{exception.message}"
20
20
  redirect_to("/features/#{@feature.key}?error=#{error}")
21
21
  end
22
22
 
@@ -16,7 +16,7 @@ module Flipper
16
16
  begin
17
17
  feature.enable_percentage_of_time params['value']
18
18
  rescue ArgumentError => exception
19
- error = Rack::Utils.escape("Invalid percentage of time value: #{exception.message}")
19
+ error = "Invalid percentage of time value: #{exception.message}"
20
20
  redirect_to("/features/#{@feature.key}?error=#{error}")
21
21
  end
22
22
 
@@ -23,11 +23,11 @@ module Flipper
23
23
  def color_class
24
24
  case feature.state
25
25
  when :on
26
- 'text-success'
26
+ 'bg-success'
27
27
  when :off
28
- 'text-danger'
28
+ 'bg-danger'
29
29
  when :conditional
30
- 'text-warning'
30
+ 'bg-warning'
31
31
  end
32
32
  end
33
33
 
@@ -12,6 +12,7 @@ module Flipper
12
12
  def initialize(app, options = {})
13
13
  @app = app
14
14
  @env_key = options.fetch(:env_key, 'flipper')
15
+ @flipper = options.fetch(:flipper) { Flipper }
15
16
 
16
17
  @action_collection = ActionCollection.new
17
18
 
@@ -43,7 +44,7 @@ module Flipper
43
44
  if action_class.nil?
44
45
  @app.call(env)
45
46
  else
46
- flipper = env.fetch(@env_key)
47
+ flipper = env.fetch(@env_key) { Flipper }
47
48
  action_class.run(flipper, request)
48
49
  end
49
50
  end
@@ -29,3 +29,10 @@ html {
29
29
  .toggle-on .toggle-block-when-off {
30
30
  display: none;
31
31
  }
32
+
33
+ .status {
34
+ height: 10px;
35
+ width: 10px;
36
+ border-radius: 50%;
37
+ display: inline-block;
38
+ }
@@ -21,7 +21,7 @@
21
21
  <div class="card">
22
22
  <%# Gate State Header %>
23
23
  <div class="card-header">
24
- <span class="octicon octicon-squirrel <%= @feature.color_class %> mr-2"></span>
24
+ <span class="status <%= @feature.color_class %> mr-2"></span>
25
25
  <%= @feature.gate_state_title %>
26
26
  </div>
27
27
 
@@ -69,8 +69,8 @@
69
69
  <%== csrf_input_tag %>
70
70
  <input type="hidden" name="operation" value="disable">
71
71
  <input type="hidden" name="value" value="<%= item %>">
72
- <button type="submit" value="Disable" class="btn btn-link btn-sm text-danger" data-toggle="tooltip" title="Disable <%= item %>" data-placement="left">
73
- <span class="octicon octicon-trashcan"></span>
72
+ <button type="submit" value="Disable" class="btn btn-outline-danger" data-toggle="tooltip" title="Disable <%= item %>" data-placement="left">
73
+ Remove
74
74
  </button>
75
75
  </form>
76
76
  </div>
@@ -40,7 +40,7 @@
40
40
  <% @features.each do |feature| %>
41
41
  <div class="feature row align-items-center mt-0 px-3 border-bottom">
42
42
  <div class="col-1 col-md-auto">
43
- <span class="octicon octicon-squirrel <%= feature.color_class %>" data-toggle="tooltip" title=<%= feature.state.to_s.capitalize %>></span>
43
+ <span class="status <%= feature.color_class %>" data-toggle="tooltip" title=<%= feature.state.to_s.capitalize %>></span>
44
44
  </div>
45
45
  <div class="col-10">
46
46
  <a href="<%= "#{script_name}/features/#{feature.key}" %>" class="d-block px-0 py-3 btn text-left text-dark">
@@ -6,8 +6,7 @@
6
6
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
7
7
  <meta name="viewport" content="width=device-width, initial-scale=1">
8
8
 
9
- <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
10
- <link rel="stylesheet" href="<%= script_name %>/octicons/octicons.css">
9
+ <link rel="stylesheet" href="<%= bootstrap_css[:src] %>" integrity="<%= bootstrap_css[:hash] %>" crossorigin="anonymous">
11
10
  <link rel="stylesheet" href="<%= script_name %>/css/application.css">
12
11
  </head>
13
12
  <body class="py-4">
@@ -52,9 +51,9 @@
52
51
  <% end %>
53
52
  </div>
54
53
 
55
- <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
56
- <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
57
- <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
54
+ <script src="<%= jquery_js[:src] %>" integrity="<%= jquery_js[:hash] %>" crossorigin="anonymous"></script>
55
+ <script src="<%= popper_js[:src] %>" integrity="<%= popper_js[:hash] %>" crossorigin="anonymous"></script>
56
+ <script src="<%= bootstrap_js[:src] %>" integrity="<%= bootstrap_js[:hash] %>" crossorigin="anonymous"></script>
58
57
  <script src="<%= script_name %>/js/application.js"></script>
59
58
  </body>
60
59
  </html>
data/lib/flipper/ui.rb CHANGED
@@ -39,14 +39,14 @@ module Flipper
39
39
  def self.app(flipper = nil, options = {})
40
40
  env_key = options.fetch(:env_key, 'flipper')
41
41
  rack_protection_options = options.fetch(:rack_protection, use: :authenticity_token)
42
+
42
43
  app = ->(_) { [200, { 'Content-Type' => 'text/html' }, ['']] }
43
44
  builder = Rack::Builder.new
44
45
  yield builder if block_given?
45
46
  builder.use Rack::Protection, rack_protection_options
46
47
  builder.use Rack::MethodOverride
47
48
  builder.use Flipper::Middleware::SetupEnv, flipper, env_key: env_key
48
- builder.use Flipper::Middleware::Memoizer, env_key: env_key
49
- builder.use Flipper::UI::Middleware, env_key: env_key
49
+ builder.use Flipper::UI::Middleware, flipper: flipper, env_key: env_key
50
50
  builder.run app
51
51
  klass = self
52
52
  builder.define_singleton_method(:inspect) { klass.inspect } # pretty rake routes output
@@ -1,3 +1,3 @@
1
1
  module Flipper
2
- VERSION = '0.21.0.rc1'.freeze
2
+ VERSION = '0.22.1'.freeze
3
3
  end
@@ -61,6 +61,23 @@ RSpec.describe Flipper::UI::Actions::ActorsGate do
61
61
  expect(last_response.headers['Location']).to eq('/features/search')
62
62
  end
63
63
 
64
+ context "when feature name contains space" do
65
+ before do
66
+ post 'features/sp%20ace/actors',
67
+ { 'value' => value, 'operation' => 'enable', 'authenticity_token' => token },
68
+ 'rack.session' => session
69
+ end
70
+
71
+ it 'adds item to members' do
72
+ expect(flipper["sp ace"].actors_value).to include('User;6')
73
+ end
74
+
75
+ it "redirects back to feature" do
76
+ expect(last_response.status).to be(302)
77
+ expect(last_response.headers['Location']).to eq('/features/sp%20ace')
78
+ end
79
+ end
80
+
64
81
  context 'value contains whitespace' do
65
82
  let(:value) { ' User;6 ' }
66
83
 
@@ -75,7 +92,7 @@ RSpec.describe Flipper::UI::Actions::ActorsGate do
75
92
 
76
93
  it 'redirects back to feature' do
77
94
  expect(last_response.status).to be(302)
78
- expect(last_response.headers['Location']).to eq('/features/search/actors?error=%22%22+is+not+a+valid+actor+value.')
95
+ expect(last_response.headers['Location']).to eq('/features/search/actors?error=%22%22%20is%20not%20a%20valid%20actor%20value.')
79
96
  end
80
97
  end
81
98
 
@@ -84,7 +101,7 @@ RSpec.describe Flipper::UI::Actions::ActorsGate do
84
101
 
85
102
  it 'redirects back to feature' do
86
103
  expect(last_response.status).to be(302)
87
- expect(last_response.headers['Location']).to eq('/features/search/actors?error=%22%22+is+not+a+valid+actor+value.')
104
+ expect(last_response.headers['Location']).to eq('/features/search/actors?error=%22%22%20is%20not%20a%20valid%20actor%20value.')
88
105
  end
89
106
  end
90
107
  end