flipper-ui 1.2.1 → 1.4.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/examples/ui/basic.ru +4 -0
  3. data/flipper-ui.gemspec +3 -2
  4. data/lib/flipper/ui/action.rb +30 -46
  5. data/lib/flipper/ui/actions/actors_gate.rb +2 -8
  6. data/lib/flipper/ui/actions/add_feature.rb +0 -9
  7. data/lib/flipper/ui/actions/boolean_gate.rb +1 -1
  8. data/lib/flipper/ui/actions/cloud_migrate.rb +26 -0
  9. data/lib/flipper/ui/actions/feature.rb +0 -7
  10. data/lib/flipper/ui/actions/features.rb +2 -9
  11. data/lib/flipper/ui/actions/groups_gate.rb +2 -7
  12. data/lib/flipper/ui/actions/percentage_of_actors_gate.rb +2 -2
  13. data/lib/flipper/ui/actions/percentage_of_time_gate.rb +2 -2
  14. data/lib/flipper/ui/actions/settings.rb +0 -3
  15. data/lib/flipper/ui/configuration.rb +18 -4
  16. data/lib/flipper/ui/middleware.rb +1 -0
  17. data/lib/flipper/ui/public/css/bootstrap.min.css +6 -0
  18. data/lib/flipper/ui/public/js/application.js +17 -33
  19. data/lib/flipper/ui/public/js/bootstrap.min.js +7 -0
  20. data/lib/flipper/ui/public/js/popper.min.js +6 -0
  21. data/lib/flipper/ui/sources.json +5 -0
  22. data/lib/flipper/ui/util.rb +10 -0
  23. data/lib/flipper/ui/views/add_actor.erb +8 -4
  24. data/lib/flipper/ui/views/add_feature.erb +3 -3
  25. data/lib/flipper/ui/views/add_group.erb +14 -9
  26. data/lib/flipper/ui/views/feature.erb +93 -80
  27. data/lib/flipper/ui/views/features.erb +29 -29
  28. data/lib/flipper/ui/views/layout.erb +28 -20
  29. data/lib/flipper/ui/views/settings.erb +31 -4
  30. data/lib/flipper/ui.rb +16 -2
  31. data/lib/flipper/version.rb +1 -1
  32. data/spec/flipper/ui/actions/actors_gate_spec.rb +6 -6
  33. data/spec/flipper/ui/actions/add_feature_spec.rb +1 -1
  34. data/spec/flipper/ui/actions/boolean_gate_spec.rb +1 -1
  35. data/spec/flipper/ui/actions/cloud_migrate_spec.rb +54 -0
  36. data/spec/flipper/ui/actions/feature_spec.rb +28 -0
  37. data/spec/flipper/ui/actions/features_spec.rb +60 -3
  38. data/spec/flipper/ui/actions/groups_gate_spec.rb +6 -6
  39. data/spec/flipper/ui/actions/percentage_of_actors_gate_spec.rb +3 -3
  40. data/spec/flipper/ui/actions/percentage_of_time_gate_spec.rb +3 -3
  41. data/spec/flipper/ui/actions/settings_spec.rb +8 -0
  42. data/spec/flipper/ui/configuration_spec.rb +9 -4
  43. data/spec/flipper/ui_spec.rb +19 -31
  44. metadata +40 -35
  45. data/lib/flipper/ui/public/css/bootstrap-4.6.0.min.css +0 -7
  46. data/lib/flipper/ui/public/js/bootstrap-4.6.0.min.js +0 -7
  47. data/lib/flipper/ui/public/js/popper-1.12.9.min.js +0 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9e7548b068b159bfd58094d749671e3d93ad0e8e7e5159a60e334d5f87a94a52
4
- data.tar.gz: 47506fd1ede7edae268409131cdf5fc67398fdde135dde6c61902f7eb401033d
3
+ metadata.gz: 00b09ef9134ecfe7b076edae579e9ff91827964ada4053fa8d5b5167b2965992
4
+ data.tar.gz: 22b200ae581fd91ced63c0936bfa03ce9ee306779bf09c7ed40809ec97e9d4c6
5
5
  SHA512:
6
- metadata.gz: 86e2ffab3abe94252f89748155872551d94755c9c8c202a1d39e8d948b5ed0b062b9f5bfcdfcc4d371fee294ea051cfb7ff6808064c75eb8bc6b076106514c6b
7
- data.tar.gz: e507fe429a898098c26a063d3946ac4866e80b00eba146789bedebed50351869f0eeea8ee9019b2ebc49567c4fadd2cec3372dac607f69684176369402c35378
6
+ metadata.gz: d85919950864694f4156846ffd1cf20c758451821409c6ae7fb566b11759740842368af8393669617c5d8c5764ac5a9d11c7f91207641c51892dec0169cd09ad
7
+ data.tar.gz: 62b4831c0a38c4321ab627480e79ab5043140d4b57869c80a3792288d9b2d21421133f701ba1a8df72b6d5d707854df65714cce7009cb383aee039c04177718a
data/examples/ui/basic.ru CHANGED
@@ -25,6 +25,7 @@ Flipper::UI.configure do |config|
25
25
  config.feature_removal_enabled = true
26
26
  config.cloud_recommendation = true
27
27
  config.confirm_fully_enable = false
28
+ config.confirm_disable = false
28
29
  config.read_only = false
29
30
  # config.show_feature_description_in_list = true
30
31
  config.descriptions_source = lambda do |_keys|
@@ -46,6 +47,8 @@ Flipper::UI.configure do |config|
46
47
  '6' => '<a href="https://opensoul.org">Brandon</a>',
47
48
  }
48
49
  end
50
+
51
+ config.application_href = "https://example.com"
49
52
  end
50
53
 
51
54
  # You can uncomment these to get some default data:
@@ -63,5 +66,6 @@ end
63
66
  use Rack::Reloader
64
67
 
65
68
  run Flipper::UI.app { |builder|
69
+ builder.use Rack::Reloader, 1
66
70
  builder.use Rack::Session::Cookie, secret: "_super_secret"
67
71
  }
data/flipper-ui.gemspec CHANGED
@@ -21,8 +21,9 @@ Gem::Specification.new do |gem|
21
21
  gem.metadata = Flipper::METADATA
22
22
 
23
23
  gem.add_dependency 'rack', '>= 1.4', '< 4'
24
- gem.add_dependency 'rack-protection', '>= 1.5.3', '<= 4.0.0'
24
+ gem.add_dependency 'rack-protection', '>= 1.5.3', '<5.0.0'
25
+ gem.add_dependency 'rack-session', '>= 1.0.2', '< 3.0.0'
25
26
  gem.add_dependency 'flipper', "~> #{Flipper::VERSION}"
26
27
  gem.add_dependency 'erubi', '>= 1.0.0', '< 2.0.0'
27
- gem.add_dependency 'sanitize', '< 7'
28
+ gem.add_dependency 'sanitize', '< 8'
28
29
  end
@@ -7,12 +7,20 @@ require 'sanitize'
7
7
 
8
8
  module Flipper
9
9
  module UI
10
+ # Sanitize config for descriptions in list view. Removes anchor tags to
11
+ # avoid nested links (the feature row is wrapped in an <a> tag).
12
+ # See: https://github.com/flippercloud/flipper/issues/939
13
+ SANITIZE_LIST = Sanitize::Config.merge(
14
+ Sanitize::Config::BASIC,
15
+ elements: Sanitize::Config::BASIC[:elements] - ['a']
16
+ )
17
+
10
18
  class Action
11
19
  module FeatureNameFromRoute
12
20
  def feature_name
13
21
  @feature_name ||= begin
14
22
  match = request.path_info.match(self.class.route_regex)
15
- match ? Rack::Utils.unescape(match[:feature_name]) : nil
23
+ match ? Flipper::UI::Util.unescape(match[:feature_name]) : nil
16
24
  end
17
25
  end
18
26
  private :feature_name
@@ -27,24 +35,7 @@ module Flipper
27
35
  'delete'.freeze,
28
36
  ]).freeze
29
37
 
30
- SOURCES = {
31
- bootstrap_css: {
32
- src: '/css/bootstrap-4.6.0.min.css'.freeze,
33
- hash: 'sha384-B0vP5xmATw1+K9KRQjQERJvTumQW0nPEzvF6L/Z6nronJ3oUOFUFpCjEUQouq2+l'.freeze
34
- }.freeze,
35
- jquery_js: {
36
- src: '/js/jquery-3.6.0.slim.js'.freeze,
37
- hash: 'sha256-HwWONEZrpuoh951cQD1ov2HUK5zA5DwJ1DNUXaM6FsY='.freeze
38
- }.freeze,
39
- popper_js: {
40
- src: '/js/popper-1.12.9.min.js'.freeze,
41
- hash: 'sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q'.freeze
42
- }.freeze,
43
- bootstrap_js: {
44
- src: '/js/bootstrap-4.6.0.min.js'.freeze,
45
- hash: 'sha384-+YQ4JLhjyBLPDQt//I+STsc9iw4uQqACwlvpslubQzn4u2UU2UFM80nGisd026JF'.freeze
46
- }.freeze
47
- }.freeze
38
+ SOURCES = JSON.parse(File.read(File.expand_path('sources.json', __dir__))).freeze
48
39
  CONTENT_SECURITY_POLICY = <<-CSP.delete("\n")
49
40
  default-src 'none';
50
41
  img-src 'self';
@@ -110,12 +101,6 @@ module Flipper
110
101
  @request = request
111
102
  @code = 200
112
103
  @headers = {Rack::CONTENT_TYPE => 'text/plain'}
113
- @breadcrumbs =
114
- if Flipper::UI.configuration.application_breadcrumb_href
115
- [Breadcrumb.new('App', Flipper::UI.configuration.application_breadcrumb_href)]
116
- else
117
- []
118
- end
119
104
  end
120
105
 
121
106
  # Public: Runs the request method for the provided request.
@@ -187,7 +172,7 @@ module Flipper
187
172
  # location - The String location to set the Location header to.
188
173
  def redirect_to(location)
189
174
  status 302
190
- header 'location', "#{script_name}#{Rack::Utils.escape_path(location)}"
175
+ header 'location', "#{script_name}#{location}"
191
176
  halt [@code, @headers, ['']]
192
177
  end
193
178
 
@@ -219,16 +204,6 @@ module Flipper
219
204
  end
220
205
  end
221
206
 
222
- # Public: Add a breadcrumb to the trail.
223
- #
224
- # text - The String text for the breadcrumb.
225
- # href - The String href for the anchor tag (optional). If nil, breadcrumb
226
- # is assumed to be the end of the trail.
227
- def breadcrumb(text, href = nil)
228
- breadcrumb_href = href.nil? ? href : "#{script_name}#{href}"
229
- @breadcrumbs << Breadcrumb.new(text, breadcrumb_href)
230
- end
231
-
232
207
  # Private
233
208
  def view_with_layout(&block)
234
209
  view :layout, &block
@@ -252,6 +227,16 @@ module Flipper
252
227
  request.env['SCRIPT_NAME']
253
228
  end
254
229
 
230
+ # Internal: Generate urls relative to the app's script name.
231
+ #
232
+ # url_for("feature") # => "http://localhost:9292/flipper/feature"
233
+ # url_for("/thing") # => "http://localhost:9292/thing"
234
+ # url_for("https://example.com") # => "https://example.com"
235
+ #
236
+ def url_for(*parts)
237
+ URI.join(request.base_url, script_name + '/', *parts).to_s
238
+ end
239
+
255
240
  # Private
256
241
  def views_path
257
242
  self.class.views_path
@@ -279,11 +264,6 @@ module Flipper
279
264
  # to inform people of that fact.
280
265
  def render_read_only
281
266
  status 403
282
-
283
- breadcrumb 'Home', '/'
284
- breadcrumb 'Features', '/features'
285
- breadcrumb 'Noooooope'
286
-
287
267
  halt view_response(:read_only)
288
268
  end
289
269
 
@@ -296,19 +276,23 @@ module Flipper
296
276
  end
297
277
 
298
278
  def bootstrap_css
299
- SOURCES[:bootstrap_css]
279
+ asset_hash "/css/bootstrap.min.css"
300
280
  end
301
281
 
302
282
  def bootstrap_js
303
- SOURCES[:bootstrap_js]
283
+ asset_hash "/js/bootstrap.min.js"
304
284
  end
305
285
 
306
286
  def popper_js
307
- SOURCES[:popper_js]
287
+ asset_hash "/js/popper.min.js"
308
288
  end
309
289
 
310
- def jquery_js
311
- SOURCES[:jquery_js]
290
+ def asset_hash(src)
291
+ v = ENV["RACK_ENV"] == "development" ? Time.now.to_i : Flipper::VERSION
292
+ {
293
+ src: "#{src}?v=#{v}",
294
+ hash: SOURCES[src]
295
+ }
312
296
  end
313
297
  end
314
298
  end
@@ -13,12 +13,6 @@ module Flipper
13
13
  def get
14
14
  feature = flipper[feature_name]
15
15
  @feature = Decorators::Feature.new(feature)
16
-
17
- breadcrumb 'Home', '/'
18
- breadcrumb 'Features', '/features'
19
- breadcrumb @feature.key, "/features/#{@feature.key}"
20
- breadcrumb 'Add Actor'
21
-
22
16
  view_response :add_actor
23
17
  end
24
18
 
@@ -31,7 +25,7 @@ module Flipper
31
25
 
32
26
  if values.empty?
33
27
  error = "#{value.inspect} is not a valid actor value."
34
- redirect_to("/features/#{feature.key}/actors?error=#{error}")
28
+ redirect_to("/features/#{Flipper::UI::Util.escape feature.key}/actors?error=#{Flipper::UI::Util.escape error}")
35
29
  end
36
30
 
37
31
  values.each do |value|
@@ -45,7 +39,7 @@ module Flipper
45
39
  end
46
40
  end
47
41
 
48
- redirect_to("/features/#{feature.key}")
42
+ redirect_to("/features/#{Flipper::UI::Util.escape feature.key}")
49
43
  end
50
44
  end
51
45
  end
@@ -12,18 +12,9 @@ module Flipper
12
12
 
13
13
  unless Flipper::UI.configuration.feature_creation_enabled
14
14
  status 403
15
-
16
- breadcrumb 'Home', '/'
17
- breadcrumb 'Features', '/features'
18
- breadcrumb 'Noooooope'
19
-
20
15
  halt view_response(:feature_creation_disabled)
21
16
  end
22
17
 
23
- breadcrumb 'Home', '/'
24
- breadcrumb 'Features', '/features'
25
- breadcrumb 'Add'
26
-
27
18
  view_response :add_feature
28
19
  end
29
20
  end
@@ -21,7 +21,7 @@ module Flipper
21
21
  feature.disable
22
22
  end
23
23
 
24
- redirect_to "/features/#{@feature.key}"
24
+ redirect_to "/features/#{Flipper::UI::Util.escape @feature.key}"
25
25
  end
26
26
  end
27
27
  end
@@ -0,0 +1,26 @@
1
+ require 'flipper/ui/action'
2
+ require 'flipper/ui/util'
3
+
4
+ module Flipper
5
+ module UI
6
+ module Actions
7
+ class CloudMigrate < UI::Action
8
+ route %r{\A/settings\/cloud/?\Z}
9
+
10
+ def post
11
+ result = Flipper::Cloud.migrate(flipper)
12
+
13
+ if result.url
14
+ status 302
15
+ header 'location', result.url
16
+ halt [@code, @headers, ['']]
17
+ else
18
+ message = "Migration failed (HTTP #{result.code})"
19
+ message << ": #{result.message}" if result.message
20
+ redirect_to "/settings?error=#{Flipper::UI::Util.escape(message)}"
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -18,10 +18,6 @@ module Flipper
18
18
  @page_title = "#{@feature.key} // Features"
19
19
  @percentages = [0, 1, 5, 10, 25, 50, 100]
20
20
 
21
- breadcrumb 'Home', '/'
22
- breadcrumb 'Features', '/features'
23
- breadcrumb @feature.key
24
-
25
21
  view_response :feature
26
22
  end
27
23
 
@@ -31,9 +27,6 @@ module Flipper
31
27
  unless Flipper::UI.configuration.feature_removal_enabled
32
28
  status 403
33
29
 
34
- breadcrumb 'Home', '/'
35
- breadcrumb 'Features', '/features'
36
-
37
30
  halt view_response(:feature_removal_disabled)
38
31
  end
39
32
 
@@ -29,9 +29,6 @@ module Flipper
29
29
 
30
30
  @show_blank_slate = @features.empty?
31
31
 
32
- breadcrumb 'Home', '/'
33
- breadcrumb 'Features'
34
-
35
32
  view_response :features
36
33
  end
37
34
 
@@ -41,10 +38,6 @@ module Flipper
41
38
  unless Flipper::UI.configuration.feature_creation_enabled
42
39
  status 403
43
40
 
44
- breadcrumb 'Home', '/'
45
- breadcrumb 'Features', '/features'
46
- breadcrumb 'Noooooope'
47
-
48
41
  halt view_response(:feature_creation_disabled)
49
42
  end
50
43
 
@@ -52,13 +45,13 @@ module Flipper
52
45
 
53
46
  if Util.blank?(value)
54
47
  error = "#{value.inspect} is not a valid feature name."
55
- redirect_to("/features/new?error=#{error}")
48
+ redirect_to("/features/new?error=#{Flipper::UI::Util.escape error}")
56
49
  end
57
50
 
58
51
  feature = flipper[value]
59
52
  feature.add
60
53
 
61
- redirect_to "/features/#{value}"
54
+ redirect_to "/features/#{Flipper::UI::Util.escape value}"
62
55
  end
63
56
  end
64
57
  end
@@ -13,11 +13,6 @@ module Flipper
13
13
  feature = flipper[feature_name]
14
14
  @feature = Decorators::Feature.new(feature)
15
15
 
16
- breadcrumb 'Home', '/'
17
- breadcrumb 'Features', '/features'
18
- breadcrumb @feature.key, "/features/#{@feature.key}"
19
- breadcrumb 'Add Group'
20
-
21
16
  view_response :add_group
22
17
  end
23
18
 
@@ -35,10 +30,10 @@ module Flipper
35
30
  feature.disable_group value
36
31
  end
37
32
 
38
- redirect_to("/features/#{feature.key}")
33
+ redirect_to("/features/#{Flipper::UI::Util.escape feature.key}")
39
34
  else
40
35
  error = "The group named #{value.inspect} has not been registered."
41
- redirect_to("/features/#{feature.key}/groups?error=#{error}")
36
+ redirect_to("/features/#{Flipper::UI::Util.escape feature.key}/groups?error=#{Flipper::UI::Util.escape error}")
42
37
  end
43
38
  end
44
39
  end
@@ -19,10 +19,10 @@ module Flipper
19
19
  feature.enable_percentage_of_actors params['value']
20
20
  rescue ArgumentError => exception
21
21
  error = "Invalid percentage of actors value: #{exception.message}"
22
- redirect_to("/features/#{@feature.key}?error=#{error}")
22
+ redirect_to("/features/#{Flipper::UI::Util.escape @feature.key}?error=#{Flipper::UI::Util.escape error}")
23
23
  end
24
24
 
25
- redirect_to "/features/#{@feature.key}"
25
+ redirect_to "/features/#{Flipper::UI::Util.escape @feature.key}"
26
26
  end
27
27
  end
28
28
  end
@@ -19,10 +19,10 @@ module Flipper
19
19
  feature.enable_percentage_of_time params['value']
20
20
  rescue ArgumentError => exception
21
21
  error = "Invalid percentage of time value: #{exception.message}"
22
- redirect_to("/features/#{@feature.key}?error=#{error}")
22
+ redirect_to("/features/#{Flipper::UI::Util.escape @feature.key}?error=#{Flipper::UI::Util.escape error}")
23
23
  end
24
24
 
25
- redirect_to "/features/#{@feature.key}"
25
+ redirect_to "/features/#{Flipper::UI::Util.escape @feature.key}"
26
26
  end
27
27
  end
28
28
  end
@@ -10,9 +10,6 @@ module Flipper
10
10
  def get
11
11
  @page_title = 'Settings'
12
12
 
13
- breadcrumb 'Home', '/'
14
- breadcrumb 'Settings'
15
-
16
13
  view_response :settings
17
14
  end
18
15
  end
@@ -5,18 +5,28 @@ module Flipper
5
5
  class Configuration
6
6
  attr_reader :delete
7
7
 
8
- attr_accessor :banner_text,
9
- :banner_class
8
+ attr_accessor :banner_text
9
+ attr_reader :banner_class
10
10
 
11
11
  # Public: Is the UI in read only mode or not. Default is false. This
12
12
  # supersedes all other write-related options such as
13
13
  # (feature_creation_enabled and feature_removal_enabled).
14
14
  attr_accessor :read_only
15
15
 
16
- # Public: If you set this, the UI will always have a first breadcrumb that
16
+ # Public: If you set this, the UI will always have a first nav item that
17
17
  # says "App" which points to this href. The href can be a path (ie: "/")
18
18
  # or full url ("https://app.example.com/").
19
- attr_accessor :application_breadcrumb_href
19
+ attr_accessor :application_href
20
+ alias_method :application_breadcrumb_href, :application_href
21
+ alias_method :application_breadcrumb_href=, :application_href=
22
+
23
+ # Public: An array of nav items to show. By default "Features" and
24
+ # "Settings" are shown, but you can add your own. Each item must have
25
+ # a `:title` and `:href` key:
26
+ #
27
+ # config.nav_items << { title: "Custom", href: "/custom/page" }
28
+ #
29
+ attr_accessor :nav_items
20
30
 
21
31
  # Public: Is feature creation allowed from the UI? Defaults to true. If
22
32
  # set to false, users of the UI cannot create features. All feature
@@ -97,6 +107,10 @@ module Flipper
97
107
  @confirm_fully_enable = false
98
108
  @confirm_disable = true
99
109
  @read_only = false
110
+ @nav_items = [
111
+ { title: "Features", href: "features" },
112
+ { title: "Settings", href: "settings" },
113
+ ]
100
114
  end
101
115
 
102
116
  def using_descriptions?
@@ -27,6 +27,7 @@ module Flipper
27
27
  @action_collection.add UI::Actions::Features
28
28
  @action_collection.add UI::Actions::Export
29
29
  @action_collection.add UI::Actions::Import
30
+ @action_collection.add UI::Actions::CloudMigrate
30
31
  @action_collection.add UI::Actions::Settings
31
32
 
32
33
  # Static Assets/Files