flipper-ui 0.20.0 → 0.25.2

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/examples/ui/authorization.ru +46 -0
  3. data/examples/ui/basic.ru +21 -34
  4. data/flipper-ui.gemspec +2 -1
  5. data/lib/flipper/ui/action.rb +50 -1
  6. data/lib/flipper/ui/actions/actors_gate.rb +11 -8
  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/configuration.rb +11 -0
  13. data/lib/flipper/ui/decorators/feature.rb +3 -3
  14. data/lib/flipper/ui/middleware.rb +2 -1
  15. data/lib/flipper/ui/public/css/application.css +7 -0
  16. data/lib/flipper/ui/public/images/logo.png +0 -0
  17. data/lib/flipper/ui/public/js/application.js +15 -4
  18. data/lib/flipper/ui/views/add_actor.erb +1 -1
  19. data/lib/flipper/ui/views/feature.erb +9 -9
  20. data/lib/flipper/ui/views/features.erb +3 -6
  21. data/lib/flipper/ui/views/layout.erb +20 -6
  22. data/lib/flipper/ui.rb +5 -22
  23. data/lib/flipper/version.rb +1 -1
  24. data/spec/flipper/ui/action_spec.rb +0 -2
  25. data/spec/flipper/ui/actions/actors_gate_spec.rb +70 -7
  26. data/spec/flipper/ui/actions/add_feature_spec.rb +0 -2
  27. data/spec/flipper/ui/actions/boolean_gate_spec.rb +18 -2
  28. data/spec/flipper/ui/actions/feature_spec.rb +18 -2
  29. data/spec/flipper/ui/actions/features_spec.rb +16 -5
  30. data/spec/flipper/ui/actions/file_spec.rb +0 -12
  31. data/spec/flipper/ui/actions/groups_gate_spec.rb +20 -5
  32. data/spec/flipper/ui/actions/home_spec.rb +0 -2
  33. data/spec/flipper/ui/actions/percentage_of_actors_gate_spec.rb +18 -3
  34. data/spec/flipper/ui/actions/percentage_of_time_gate_spec.rb +18 -3
  35. data/spec/flipper/ui/configuration_spec.rb +11 -2
  36. data/spec/flipper/ui/decorators/feature_spec.rb +0 -2
  37. data/spec/flipper/ui/decorators/gate_spec.rb +0 -1
  38. data/spec/flipper/ui/util_spec.rb +0 -1
  39. data/spec/flipper/ui_spec.rb +1 -37
  40. metadata +28 -26
  41. data/docs/ui/README.md +0 -190
  42. data/docs/ui/images/banner.png +0 -0
  43. data/docs/ui/images/description.png +0 -0
  44. data/docs/ui/images/feature.png +0 -0
  45. data/docs/ui/images/features.png +0 -0
  46. data/lib/flipper/ui/public/octicons/LICENSE.txt +0 -9
  47. data/lib/flipper/ui/public/octicons/README.md +0 -1
  48. data/lib/flipper/ui/public/octicons/octicons-local.ttf +0 -0
  49. data/lib/flipper/ui/public/octicons/octicons.css +0 -236
  50. data/lib/flipper/ui/public/octicons/octicons.eot +0 -0
  51. data/lib/flipper/ui/public/octicons/octicons.svg +0 -200
  52. data/lib/flipper/ui/public/octicons/octicons.ttf +0 -0
  53. 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: bfa566d9ce94dadf56643f0f8effcd75795dc478e8098a91374fa20b612d772c
4
- data.tar.gz: 0423fa89225c97f21cfe5bc851b43c8d13dbe930764989a8255cd61ab23bdada
3
+ metadata.gz: ca054fcd02084bba5c6217471a0c2113b8325b5e3e7763b5eab3723156b245d7
4
+ data.tar.gz: 156f7fec4900a85a7473e225d4e688be1621e59ac854f4d68a033fc85ae953bb
5
5
  SHA512:
6
- metadata.gz: b65bcd93a19db4a5807f6ef4900d99ec9e8bf3232e569d0e5c27d4ad54b294657055448d5b45ee68bc66b9bf03a5810c090b210adf1de7501ea3bd61cdbd4c39
7
- data.tar.gz: 52c3fbade80bc667eed46f94059d65f84c1e46330a456dda768c6cbc5e807b0681bb5d8952e88fde8b4dae992009ac559c5073073987bfdc581b55ca75482790
6
+ metadata.gz: 6f2fa40777f1d9dbe2aa9e73a28f212f37d7dfd511a0409c928960ee815df6731d5afb990b3645c33faf55245238b70ccc34a56d07b0625228a30f170fd87487
7
+ data.tar.gz: 6f36a16dbbb178f78a193d25cdae9dc523b2ae8d069cce1e12e73432ab3ac749b9089e16b075cb44f16b4ed13e78c003be8825eeb4ae4384d73066acf0876bbe
@@ -0,0 +1,46 @@
1
+ #
2
+ # Usage:
3
+ # bundle exec rackup examples/ui/authorization.ru -p 9999
4
+ # bundle exec shotgun examples/ui/authorization.ru -p 9999
5
+ # http://localhost:9999/
6
+ #
7
+ require 'bundler/setup'
8
+ require "flipper/ui"
9
+ require "flipper/adapters/pstore"
10
+
11
+ Flipper.register(:admins) { |actor|
12
+ actor.respond_to?(:admin?) && actor.admin?
13
+ }
14
+
15
+ # Example middleware to allow reading the Flipper UI but nothing else.
16
+ class FlipperReadOnlyMiddleware
17
+ def initialize(app)
18
+ @app = app
19
+ end
20
+
21
+ def call(env)
22
+ request = Rack::Request.new(env)
23
+
24
+ if request.get?
25
+ @app.call(env)
26
+ else
27
+ [401, {}, ["You can only look"]]
28
+ end
29
+ end
30
+ end
31
+
32
+ # You can uncomment these to get some default data:
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|
44
+ builder.use Rack::Session::Cookie, secret: "_super_secret"
45
+ builder.use FlipperReadOnlyMiddleware
46
+ }
data/examples/ui/basic.ru CHANGED
@@ -1,20 +1,16 @@
1
1
  #
2
2
  # Usage:
3
- # bundle exec rackup examples/ui/basic.ru -p 9999
4
- # bundle exec shotgun examples/ui/basic.ru -p 9999
3
+ # # if you want it to not reload and be really fast
4
+ # bin/rackup examples/ui/basic.ru -p 9999
5
+ #
6
+ # # if you want reloading
7
+ # bin/shotgun examples/ui/basic.ru -p 9999
8
+ #
5
9
  # http://localhost:9999/
6
10
  #
7
- require "pp"
8
- require "logger"
9
- require "pathname"
10
-
11
- root_path = Pathname(__FILE__).dirname.join("..").expand_path
12
- lib_path = root_path.join("lib")
13
- $:.unshift(lib_path)
14
-
15
- require "flipper-ui"
11
+ require 'bundler/setup'
12
+ require "flipper/ui"
16
13
  require "flipper/adapters/pstore"
17
- require "active_support/notifications"
18
14
 
19
15
  Flipper.register(:admins) { |actor|
20
16
  actor.respond_to?(:admin?) && actor.admin?
@@ -24,21 +20,12 @@ Flipper.register(:early_access) { |actor|
24
20
  actor.respond_to?(:early?) && actor.early?
25
21
  }
26
22
 
27
- # Setup logging of flipper calls.
28
- if ENV["LOG"] == "1"
29
- $logger = Logger.new(STDOUT)
30
- require "flipper/instrumentation/log_subscriber"
31
- Flipper::Instrumentation::LogSubscriber.logger = $logger
32
- end
33
-
34
- adapter = Flipper::Adapters::PStore.new
35
- flipper = Flipper.new(adapter, instrumenter: ActiveSupport::Notifications)
36
-
37
23
  Flipper::UI.configure do |config|
38
24
  # config.banner_text = 'Production Environment'
39
25
  # config.banner_class = 'danger'
40
26
  config.feature_creation_enabled = true
41
27
  config.feature_removal_enabled = true
28
+ config.cloud_recommendation = true
42
29
  # config.show_feature_description_in_list = true
43
30
  config.descriptions_source = lambda do |_keys|
44
31
  {
@@ -55,17 +42,17 @@ Flipper::UI.configure do |config|
55
42
  end
56
43
 
57
44
  # You can uncomment these to get some default data:
58
- # flipper[:search_performance_another_long_thing].enable
59
- # flipper[:gauges_tracking].enable
60
- # flipper[:unused].disable
61
- # flipper[:suits].enable_actor Flipper::Actor.new('1')
62
- # flipper[:suits].enable_actor Flipper::Actor.new('6')
63
- # flipper[:secrets].enable_group :admins
64
- # flipper[:secrets].enable_group :early_access
65
- # flipper[:logging].enable_percentage_of_time 5
66
- # flipper[:new_cache].enable_percentage_of_actors 15
67
- # flipper["a/b"].add
68
-
69
- run Flipper::UI.app(flipper) { |builder|
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")
55
+
56
+ run Flipper::UI.app { |builder|
70
57
  builder.use Rack::Session::Cookie, secret: "_super_secret"
71
58
  }
data/flipper-ui.gemspec CHANGED
@@ -21,7 +21,8 @@ Gem::Specification.new do |gem|
21
21
  gem.metadata = Flipper::METADATA
22
22
 
23
23
  gem.add_dependency 'rack', '>= 1.4', '< 3'
24
- gem.add_dependency 'rack-protection', '>= 1.5.3', '< 2.2.0'
24
+ gem.add_dependency 'rack-protection', '>= 1.5.3', '<= 2.3.0'
25
25
  gem.add_dependency 'flipper', "~> #{Flipper::VERSION}"
26
26
  gem.add_dependency 'erubi', '>= 1.0.0', '< 2.0.0'
27
+ gem.add_dependency 'sanitize', '< 7'
27
28
  end
@@ -3,6 +3,7 @@ require 'flipper/ui/configuration'
3
3
  require 'flipper/ui/error'
4
4
  require 'erubi'
5
5
  require 'json'
6
+ require 'sanitize'
6
7
 
7
8
  module Flipper
8
9
  module UI
@@ -26,6 +27,36 @@ module Flipper
26
27
  'delete'.freeze,
27
28
  ]).freeze
28
29
 
30
+ SOURCES = {
31
+ bootstrap_css: {
32
+ src: 'https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css'.freeze,
33
+ hash: 'sha384-B0vP5xmATw1+K9KRQjQERJvTumQW0nPEzvF6L/Z6nronJ3oUOFUFpCjEUQouq2+l'.freeze
34
+ }.freeze,
35
+ jquery_js: {
36
+ src: 'https://code.jquery.com/jquery-3.6.0.slim.js'.freeze,
37
+ hash: 'sha256-HwWONEZrpuoh951cQD1ov2HUK5zA5DwJ1DNUXaM6FsY='.freeze
38
+ }.freeze,
39
+ popper_js: {
40
+ src: 'https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js'.freeze,
41
+ hash: 'sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q'.freeze
42
+ }.freeze,
43
+ bootstrap_js: {
44
+ src: 'https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/js/bootstrap.min.js'.freeze,
45
+ hash: 'sha384-+YQ4JLhjyBLPDQt//I+STsc9iw4uQqACwlvpslubQzn4u2UU2UFM80nGisd026JF'.freeze
46
+ }.freeze
47
+ }.freeze
48
+ SCRIPT_SRCS = SOURCES.values_at(:jquery_js, :popper_js, :bootstrap_js).map { |s| s[:src] }
49
+ STYLE_SRCS = SOURCES.values_at(:bootstrap_css).map { |s| s[:src] }
50
+ CONTENT_SECURITY_POLICY = <<-CSP.delete("\n")
51
+ default-src 'none';
52
+ img-src 'self';
53
+ font-src 'self';
54
+ script-src 'report-sample' 'self' #{SCRIPT_SRCS.join(' ')};
55
+ style-src 'self' 'unsafe-inline' #{STYLE_SRCS.join(' ')};
56
+ style-src-attr 'unsafe-inline' ;
57
+ style-src-elem 'self' #{STYLE_SRCS.join(' ')};
58
+ CSP
59
+
29
60
  # Public: Call this in subclasses so the action knows its route.
30
61
  #
31
62
  # regex - The Regexp that this action should run for.
@@ -130,6 +161,7 @@ module Flipper
130
161
  # Returns a response.
131
162
  def view_response(name)
132
163
  header 'Content-Type', 'text/html'
164
+ header 'Content-Security-Policy', CONTENT_SECURITY_POLICY
133
165
  body = view_with_layout { view_without_layout name }
134
166
  halt [@code, @headers, [body]]
135
167
  end
@@ -151,7 +183,7 @@ module Flipper
151
183
  # location - The String location to set the Location header to.
152
184
  def redirect_to(location)
153
185
  status 302
154
- header 'Location', "#{script_name}#{location}"
186
+ header 'Location', "#{script_name}#{Rack::Utils.escape_path(location)}"
155
187
  halt [@code, @headers, ['']]
156
188
  end
157
189
 
@@ -207,6 +239,7 @@ module Flipper
207
239
  def view(name)
208
240
  path = views_path.join("#{name}.erb")
209
241
  raise "Template does not exist: #{path}" unless path.exist?
242
+
210
243
  eval(Erubi::Engine.new(path.read, escape: true).src)
211
244
  end
212
245
 
@@ -237,6 +270,22 @@ module Flipper
237
270
  def valid_request_method?
238
271
  VALID_REQUEST_METHOD_NAMES.include?(request_method_name)
239
272
  end
273
+
274
+ def bootstrap_css
275
+ SOURCES[:bootstrap_css]
276
+ end
277
+
278
+ def bootstrap_js
279
+ SOURCES[:bootstrap_js]
280
+ end
281
+
282
+ def popper_js
283
+ SOURCES[:popper_js]
284
+ end
285
+
286
+ def jquery_js
287
+ SOURCES[:jquery_js]
288
+ end
240
289
  end
241
290
  end
242
291
  end
@@ -25,19 +25,22 @@ module Flipper
25
25
  def post
26
26
  feature = flipper[feature_name]
27
27
  value = params['value'].to_s.strip
28
+ values = value.split(UI.configuration.actors_separator).map(&:strip).uniq
28
29
 
29
- if Util.blank?(value)
30
- error = Rack::Utils.escape("#{value.inspect} is not a valid actor value.")
30
+ if values.empty?
31
+ error = "#{value.inspect} is not a valid actor value."
31
32
  redirect_to("/features/#{feature.key}/actors?error=#{error}")
32
33
  end
33
34
 
34
- actor = Flipper::Actor.new(value)
35
+ values.each do |value|
36
+ actor = Flipper::Actor.new(value)
35
37
 
36
- case params['operation']
37
- when 'enable'
38
- feature.enable_actor actor
39
- when 'disable'
40
- feature.disable_actor actor
38
+ case params['operation']
39
+ when 'enable'
40
+ feature.enable_actor actor
41
+ when 'disable'
42
+ feature.disable_actor actor
43
+ end
41
44
  end
42
45
 
43
46
  redirect_to("/features/#{feature.key}")
@@ -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
 
@@ -26,6 +26,10 @@ module Flipper
26
26
  # won't see a videoclip of Taylor Swift when there aren't any features
27
27
  attr_accessor :fun
28
28
 
29
+ # Public: Tired of seeing the awesome message about Cloud? Set this to
30
+ # false and it will go away. Defaults to true.
31
+ attr_accessor :cloud_recommendation
32
+
29
33
  # Public: What should show up in the form to add actors. This can be
30
34
  # different per application since flipper_id's can be whatever an
31
35
  # application needs. Defaults to "a flipper id".
@@ -40,6 +44,11 @@ module Flipper
40
44
  # Default false. Only works when using descriptions.
41
45
  attr_accessor :show_feature_description_in_list
42
46
 
47
+ # Public: What should be used to denote you are trying to add multiple
48
+ # actors at once (instead of just a single actor).
49
+ # Default is comma ",".
50
+ attr_accessor :actors_separator
51
+
43
52
  VALID_BANNER_CLASS_VALUES = %w(
44
53
  danger
45
54
  dark
@@ -60,9 +69,11 @@ module Flipper
60
69
  @feature_creation_enabled = true
61
70
  @feature_removal_enabled = true
62
71
  @fun = true
72
+ @cloud_recommendation = true
63
73
  @add_actor_placeholder = "a flipper id"
64
74
  @descriptions_source = DEFAULT_DESCRIPTIONS_SOURCE
65
75
  @show_feature_description_in_list = false
76
+ @actors_separator = ','
66
77
  end
67
78
 
68
79
  def using_descriptions?
@@ -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
+ }
Binary file
@@ -1,6 +1,17 @@
1
- $(function() {
2
- $(document).on('click', '.js-toggle-trigger', function() {
3
- var $container = $(this).closest('.js-toggle-container');
4
- return $container.toggleClass('toggle-on');
1
+ $(function () {
2
+ $(document).on("click", ".js-toggle-trigger", function () {
3
+ var $container = $(this).closest(".js-toggle-container");
4
+ return $container.toggleClass("toggle-on");
5
+ });
6
+
7
+ $("#delete_feature__button").on("click", function (e) {
8
+ const featureName = $("#feature_name").val();
9
+ const promptMessage = prompt(
10
+ `Are you sure you want to remove this feature from the list of features and disable it for everyone? Please enter the name of the feature to confirm it: ${featureName}`
11
+ );
12
+
13
+ if (promptMessage !== featureName) {
14
+ e.preventDefault();
15
+ }
5
16
  });
6
17
  });
@@ -8,7 +8,7 @@
8
8
  <h4 class="card-header">Enable Actor for <%= @feature.key %></h4>
9
9
  <div class="card-body">
10
10
  <p>
11
- Turn on this feature for an individual actor.
11
+ Turn on this feature for actors.
12
12
  </p>
13
13
  <form action="<%= script_name %>/features/<%= @feature.key %>/actors" method="post" class="form-inline">
14
14
  <%== csrf_input_tag %>
@@ -9,7 +9,7 @@
9
9
  <div class="col">
10
10
  <div class="card">
11
11
  <div class="card-body">
12
- <%= @feature.description %>
12
+ <%== Sanitize.fragment(@feature.description, Sanitize::Config::BASIC) %>
13
13
  </div>
14
14
  </div>
15
15
  </div>
@@ -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>
@@ -130,8 +130,8 @@
130
130
  <%== csrf_input_tag %>
131
131
  <input type="hidden" name="operation" value="disable">
132
132
  <input type="hidden" name="value" value="<%= item %>">
133
- <button type="submit" value="Disable" class="btn btn-link btn-sm text-danger" data-toggle="tooltip" title="Disable <%= item %>" data-placement="left">
134
- <span class="octicon octicon-trashcan"></span>
133
+ <button type="submit" value="Disable" class="btn btn-outline-danger" data-toggle="tooltip" title="Disable <%= item %>" data-placement="left">
134
+ Remove
135
135
  </button>
136
136
  </form>
137
137
  </div>
@@ -258,11 +258,11 @@
258
258
  <p>
259
259
  <%= Flipper::UI.configuration.delete.description %>
260
260
  </p>
261
-
262
- <form action="<%= script_name %>/features/<%= @feature.key %>" method="post" onsubmit="return confirm('Are you sure you want to remove this feature from the list of features and disable it for everyone?')">
261
+ <form action="<%= script_name %>/features/<%= @feature.key %>" method="post">
263
262
  <%== csrf_input_tag %>
263
+ <input type="hidden" id="feature_name" name="_feature" value="<%= feature_name %>">
264
264
  <input type="hidden" name="_method" value="DELETE">
265
- <button type="submit" name="action" value="Delete" class="btn btn-outline-danger" data-toggle="tooltip" title="Remove feature from list of features and disable it." data-placement="right">Delete</button>
265
+ <button type="submit" name="action" value="Delete" id="delete_feature__button" class="btn btn-outline-danger" data-toggle="tooltip" title="Remove feature from list of features and disable it." data-placement="right">Delete</button>
266
266
  </form>
267
267
  </div>
268
268
  </div>
@@ -1,5 +1,5 @@
1
1
  <% if @show_blank_slate %>
2
- <div class="jumbotron text-center">
2
+ <div class="jumbotron text-center m-0">
3
3
  <% if Flipper::UI.configuration.fun %>
4
4
  <h4>But I've got a blank space baby...</h4>
5
5
  <p>And I'll flip your features.</p>
@@ -8,9 +8,6 @@
8
8
  <a class="btn btn-primary btn-sm" href="<%= script_name %>/features/new">Add Feature</a>
9
9
  </p>
10
10
  <%- end -%>
11
- <div class="embed-responsive embed-responsive-16by9">
12
- <iframe class="embed-responsive-item" width="560" height="315" src="https://www.youtube.com/embed/e-ORhEE9VVg" frameborder="0" allowfullscreen></iframe>
13
- </div>
14
11
  <% else %>
15
12
  <h4>Getting Started</h4>
16
13
  <p class="mb-1">You have not added any features to configure yet.</p>
@@ -40,14 +37,14 @@
40
37
  <% @features.each do |feature| %>
41
38
  <div class="feature row align-items-center mt-0 px-3 border-bottom">
42
39
  <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>
40
+ <span class="status <%= feature.color_class %>" data-toggle="tooltip" title=<%= feature.state.to_s.capitalize %>></span>
44
41
  </div>
45
42
  <div class="col-10">
46
43
  <a href="<%= "#{script_name}/features/#{feature.key}" %>" class="d-block px-0 py-3 btn text-left text-dark">
47
44
  <div class="text-truncate" style="font-weight: 500"><%= feature.key %></div>
48
45
  <% if Flipper::UI.configuration.show_feature_description_in_list? && Flipper::UI::Util.present?(feature.description) %>
49
46
  <div class="text-muted font-weight-light" style="line-height: 1.4; white-space: initial; padding: 8px 0">
50
- <%= feature.description %>
47
+ <%== Sanitize.fragment(feature.description, Sanitize::Config::BASIC) %>
51
48
  </div>
52
49
  <% end %>
53
50
  <div class="text-muted text-truncate">
@@ -6,15 +6,14 @@
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">
14
13
  <div class="container mw-600">
15
14
  <%- unless Flipper::UI.configuration.banner_text.nil? -%>
16
15
  <div class="alert alert-<%= Flipper::UI.configuration.banner_class %> text-center font-weight-bold">
17
- <%= Flipper::UI.configuration.banner_text %>
16
+ <%== Sanitize.fragment(Flipper::UI.configuration.banner_text, Sanitize::Config::BASIC) %>
18
17
  </div>
19
18
  <%- end -%>
20
19
 
@@ -35,11 +34,26 @@
35
34
  <div>
36
35
  <%== yield %>
37
36
  </div>
37
+
38
+ <% if Flipper::UI.configuration.cloud_recommendation %>
39
+ <div class="d-flex justify-content-center mt-5">
40
+ <div class="row" style="max-width: 350px;">
41
+ <div class="col-auto d-flex align-items-center">
42
+ <a href="https://www.flippercloud.io/?utm_source=oss&utm_medium=ui&utm_campaign=spread_the_love">
43
+ <img src="<%= script_name %>/images/logo.png" width="50" />
44
+ </a>
45
+ </div>
46
+ <div class="col text-muted p-0">
47
+ <small>For support, audit history, finer-grained permissions, multi-environment sync, and all your projects in one place check out <a href="https://www.flippercloud.io/?utm_source=oss&utm_medium=ui&utm_campaign=spread_the_love">Flipper&nbsp;Cloud</a>.</small>
48
+ </div>
49
+ </div>
50
+ </div>
51
+ <% end %>
38
52
  </div>
39
53
 
40
- <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
41
- <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>
42
- <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>
43
57
  <script src="<%= script_name %>/js/application.js"></script>
44
58
  </body>
45
59
  </html>
data/lib/flipper/ui.rb CHANGED
@@ -14,24 +14,6 @@ require 'flipper/ui/configuration'
14
14
 
15
15
  module Flipper
16
16
  module UI
17
- class << self
18
- # These three configuration options have been moved to Flipper::UI::Configuration
19
- deprecated_configuration_options = %w(application_breadcrumb_href
20
- feature_creation_enabled
21
- feature_removal_enabled)
22
- deprecated_configuration_options.each do |attribute_name|
23
- send(:define_method, "#{attribute_name}=".to_sym) do
24
- raise ConfigurationDeprecated, "The UI configuration for #{attribute_name} has " \
25
- "deprecated. This configuration option has moved to Flipper::UI::Configuration"
26
- end
27
-
28
- send(:define_method, attribute_name.to_sym) do
29
- raise ConfigurationDeprecated, "The UI configuration for #{attribute_name} has " \
30
- "deprecated. This configuration option has moved to Flipper::UI::Configuration"
31
- end
32
- end
33
- end
34
-
35
17
  def self.root
36
18
  @root ||= Pathname(__FILE__).dirname.expand_path.join('ui')
37
19
  end
@@ -39,18 +21,19 @@ module Flipper
39
21
  def self.app(flipper = nil, options = {})
40
22
  env_key = options.fetch(:env_key, 'flipper')
41
23
  rack_protection_options = options.fetch(:rack_protection, use: :authenticity_token)
24
+
42
25
  app = ->(_) { [200, { 'Content-Type' => 'text/html' }, ['']] }
43
26
  builder = Rack::Builder.new
44
27
  yield builder if block_given?
45
28
  builder.use Rack::Protection, rack_protection_options
46
29
  builder.use Rack::MethodOverride
47
30
  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
31
+ builder.use Flipper::UI::Middleware, flipper: flipper, env_key: env_key
50
32
  builder.run app
51
33
  klass = self
52
- builder.define_singleton_method(:inspect) { klass.inspect } # pretty rake routes output
53
- builder
34
+ app = builder.to_app
35
+ app.define_singleton_method(:inspect) { klass.inspect } # pretty rake routes output
36
+ app
54
37
  end
55
38
 
56
39
  # Public: yields configuration instance for customizing UI text
@@ -1,3 +1,3 @@
1
1
  module Flipper
2
- VERSION = '0.20.0'.freeze
2
+ VERSION = '0.25.2'.freeze
3
3
  end
@@ -1,5 +1,3 @@
1
- require 'helper'
2
-
3
1
  RSpec.describe Flipper::UI::Action do
4
2
  describe 'request methods' do
5
3
  let(:action_subclass) do