flipper-ui 0.15.0 → 0.16.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6588ef43bcee1f02024cf32085148eae82ffa9f4
4
- data.tar.gz: fd986a24960b953f56510bb5b96a18373276f628
3
+ metadata.gz: 25aa2745af2e31218cbd4b27af8193e58737cf9d
4
+ data.tar.gz: cdda6a59fe7edcbdc5b06be258d2d685a2796636
5
5
  SHA512:
6
- metadata.gz: b4f66474ac658466d3a2a87e67f8c4168935457e1a62d9da3cb8d9891ec50864956cb5a800529bc757a1f879ba943da691c62b5565dffbbd2e32ad1e584a1bf6
7
- data.tar.gz: f0ca4057925cb29dcd2b08116751b71900037cf6319520056a1f5da7a5ef0b559c93e8d84ff36127c0bb8742cd4ee06443f68bea3d47ad52d8bf48de4ca0b2f9
6
+ metadata.gz: c834961bc28b80d3890bcb150ef26a9dffc2f7450acb5d0f2dc6e49d5be0f13bbf435c49916a1c08adf6c1944e1100500cf49f00d147b6a28d5f05bd97ed4399
7
+ data.tar.gz: 505af952e52ec8ccba2b37368c1fb84fcde104308d09a666f5479769677d6453edeea3088f70f74f49c4241a19f52eaa985f02f4c6400124b6b6a38cb5abc2ea
@@ -107,7 +107,6 @@ Minimal example for Rack:
107
107
  # config.ru
108
108
 
109
109
  require 'flipper-ui'
110
- require 'flipper/adapters/memory'
111
110
 
112
111
  adapter = Flipper::Adapters::Memory.new
113
112
  flipper = Flipper.new(adapter)
@@ -14,6 +14,7 @@ $:.unshift(lib_path)
14
14
 
15
15
  require "flipper-ui"
16
16
  require "flipper/adapters/pstore"
17
+ require "active_support/notifications"
17
18
 
18
19
  Flipper.register(:admins) { |actor|
19
20
  actor.respond_to?(:admin?) && actor.admin?
@@ -24,10 +25,11 @@ Flipper.register(:early_access) { |actor|
24
25
  }
25
26
 
26
27
  # Setup logging of flipper calls.
27
- $logger = Logger.new(STDOUT)
28
- require "active_support/notifications"
29
- require "flipper/instrumentation/log_subscriber"
30
- Flipper::Instrumentation::LogSubscriber.logger = $logger
28
+ if ENV["LOG"] == "1"
29
+ $logger = Logger.new(STDOUT)
30
+ require "flipper/instrumentation/log_subscriber"
31
+ Flipper::Instrumentation::LogSubscriber.logger = $logger
32
+ end
31
33
 
32
34
  adapter = Flipper::Adapters::PStore.new
33
35
  flipper = Flipper.new(adapter, instrumenter: ActiveSupport::Notifications)
@@ -42,6 +44,7 @@ flipper = Flipper.new(adapter, instrumenter: ActiveSupport::Notifications)
42
44
  # flipper[:secrets].enable_group :early_access
43
45
  # flipper[:logging].enable_percentage_of_time 5
44
46
  # flipper[:new_cache].enable_percentage_of_actors 15
47
+ # flipper["a/b"].add
45
48
 
46
49
  run Flipper::UI.app(flipper) { |builder|
47
50
  builder.use Rack::Session::Cookie, secret: "_super_secret"
@@ -1,5 +1,6 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
  require File.expand_path('../lib/flipper/version', __FILE__)
3
+ require File.expand_path('../lib/flipper/metadata', __FILE__)
3
4
 
4
5
  flipper_ui_files = lambda do |file|
5
6
  file =~ %r{(docs|examples|flipper)[\/-]ui}
@@ -18,6 +19,7 @@ Gem::Specification.new do |gem|
18
19
  gem.name = 'flipper-ui'
19
20
  gem.require_paths = ['lib']
20
21
  gem.version = Flipper::VERSION
22
+ gem.metadata = Flipper::METADATA
21
23
 
22
24
  gem.add_dependency 'rack', '>= 1.4', '< 3'
23
25
  gem.add_dependency 'rack-protection', '>= 1.5.3', '< 2.1.0'
@@ -9,8 +9,6 @@ end
9
9
  require 'rack/protection'
10
10
 
11
11
  require 'flipper'
12
- require 'flipper/middleware/setup_env'
13
- require 'flipper/middleware/memoizer'
14
12
  require 'flipper/ui/middleware'
15
13
  require 'flipper/ui/configuration'
16
14
 
@@ -6,6 +6,16 @@ require 'json'
6
6
  module Flipper
7
7
  module UI
8
8
  class Action
9
+ module FeatureNameFromRoute
10
+ def feature_name
11
+ @feature_name ||= begin
12
+ match = request.path_info.match(self.class.route_regex)
13
+ match ? match[:feature_name] : nil
14
+ end
15
+ end
16
+ private :feature_name
17
+ end
18
+
9
19
  extend Forwardable
10
20
 
11
21
  VALID_REQUEST_METHOD_NAMES = Set.new([
@@ -21,7 +31,17 @@ module Flipper
21
31
  #
22
32
  # Returns nothing.
23
33
  def self.route(regex)
24
- @regex = regex
34
+ @route_regex = regex
35
+ end
36
+
37
+ # Internal: Does this action's route match the path.
38
+ def self.route_match?(path)
39
+ path.match(route_regex)
40
+ end
41
+
42
+ # Internal: The regex that matches which routes this action will work for.
43
+ def self.route_regex
44
+ @route_regex || raise("#{name}.route is not set")
25
45
  end
26
46
 
27
47
  # Internal: Initializes and runs an action for a given request.
@@ -34,11 +54,6 @@ module Flipper
34
54
  new(flipper, request).run
35
55
  end
36
56
 
37
- # Internal: The regex that matches which routes this action will work for.
38
- def self.regex
39
- @regex || raise("#{name}.route is not set")
40
- end
41
-
42
57
  # Private: The path to the views folder.
43
58
  def self.views_path
44
59
  @views_path ||= Flipper::UI.root.join('views')
@@ -12,7 +12,7 @@ module Flipper
12
12
 
13
13
  def action_for_request(request)
14
14
  @action_classes.detect do |action_class|
15
- request.path_info =~ action_class.regex
15
+ action_class.route_match?(request.path_info)
16
16
  end
17
17
  end
18
18
  end
@@ -6,11 +6,12 @@ module Flipper
6
6
  module UI
7
7
  module Actions
8
8
  class ActorsGate < UI::Action
9
- route %r{features/[^/]*/actors/?\Z}
9
+ include FeatureNameFromRoute
10
+
11
+ route %r{\A/features/(?<feature_name>.*)/actors/?\Z}
10
12
 
11
13
  def get
12
- feature_name = Rack::Utils.unescape(request.path.split('/')[-2])
13
- feature = flipper[feature_name.to_sym]
14
+ feature = flipper[feature_name]
14
15
  @feature = Decorators::Feature.new(feature)
15
16
 
16
17
  breadcrumb 'Home', '/'
@@ -22,8 +23,7 @@ module Flipper
22
23
  end
23
24
 
24
25
  def post
25
- feature_name = Rack::Utils.unescape(request.path.split('/')[-2])
26
- feature = flipper[feature_name.to_sym]
26
+ feature = flipper[feature_name]
27
27
  value = params['value'].to_s.strip
28
28
 
29
29
  if Util.blank?(value)
@@ -5,7 +5,7 @@ module Flipper
5
5
  module UI
6
6
  module Actions
7
7
  class AddFeature < UI::Action
8
- route %r{features/new/?\Z}
8
+ route %r{\A/features/new/?\Z}
9
9
 
10
10
  def get
11
11
  unless Flipper::UI.configuration.feature_creation_enabled
@@ -5,11 +5,12 @@ module Flipper
5
5
  module UI
6
6
  module Actions
7
7
  class BooleanGate < UI::Action
8
- route %r{features/[^/]*/boolean/?\Z}
8
+ include FeatureNameFromRoute
9
+
10
+ route %r{\A/features/(?<feature_name>.*)/boolean/?\Z}
9
11
 
10
12
  def post
11
- feature_name = Rack::Utils.unescape(request.path.split('/')[-2])
12
- feature = flipper[feature_name.to_sym]
13
+ feature = flipper[feature_name]
13
14
  @feature = Decorators::Feature.new(feature)
14
15
 
15
16
  if params['action'] == 'Enable'
@@ -5,10 +5,11 @@ module Flipper
5
5
  module UI
6
6
  module Actions
7
7
  class Feature < UI::Action
8
- route %r{features/[^/]*/?\Z}
8
+ include FeatureNameFromRoute
9
+
10
+ route %r{\A/features/(?<feature_name>.*)\Z}
9
11
 
10
12
  def get
11
- feature_name = Rack::Utils.unescape(request.path.split('/').last)
12
13
  @feature = Decorators::Feature.new(flipper[feature_name])
13
14
  @page_title = "#{@feature.key} // Features"
14
15
  @percentages = [0, 1, 5, 10, 15, 25, 50, 75, 100]
@@ -30,7 +31,6 @@ module Flipper
30
31
  halt view_response(:feature_removal_disabled)
31
32
  end
32
33
 
33
- feature_name = Rack::Utils.unescape(request.path.split('/').last)
34
34
  feature = flipper[feature_name]
35
35
  feature.remove
36
36
  redirect_to '/features'
@@ -6,7 +6,7 @@ module Flipper
6
6
  module UI
7
7
  module Actions
8
8
  class Features < UI::Action
9
- route %r{features/?\Z}
9
+ route %r{\A/features/?\Z}
10
10
 
11
11
  def get
12
12
  @page_title = 'Features'
@@ -5,11 +5,12 @@ module Flipper
5
5
  module UI
6
6
  module Actions
7
7
  class GroupsGate < UI::Action
8
- route %r{features/[^/]*/groups/?\Z}
8
+ include FeatureNameFromRoute
9
+
10
+ route %r{\A/features/(?<feature_name>.*)/groups/?\Z}
9
11
 
10
12
  def get
11
- feature_name = Rack::Utils.unescape(request.path.split('/')[-2])
12
- feature = flipper[feature_name.to_sym]
13
+ feature = flipper[feature_name]
13
14
  @feature = Decorators::Feature.new(feature)
14
15
 
15
16
  breadcrumb 'Home', '/'
@@ -21,8 +22,7 @@ module Flipper
21
22
  end
22
23
 
23
24
  def post
24
- feature_name = Rack::Utils.unescape(request.path.split('/')[-2])
25
- feature = flipper[feature_name.to_sym]
25
+ feature = flipper[feature_name]
26
26
  value = params['value'].to_s.strip
27
27
 
28
28
  if Flipper.group_exists?(value)
@@ -5,7 +5,7 @@ module Flipper
5
5
  module UI
6
6
  module Actions
7
7
  class Home < UI::Action
8
- route %r{/?\Z}
8
+ route %r{\A/?\Z}
9
9
 
10
10
  def get
11
11
  redirect_to '/features'
@@ -5,11 +5,12 @@ module Flipper
5
5
  module UI
6
6
  module Actions
7
7
  class PercentageOfActorsGate < UI::Action
8
- route %r{features/[^/]*/percentage_of_actors/?\Z}
8
+ include FeatureNameFromRoute
9
+
10
+ route %r{\A/features/(?<feature_name>.*)/percentage_of_actors/?\Z}
9
11
 
10
12
  def post
11
- feature_name = Rack::Utils.unescape(request.path.split('/')[-2])
12
- feature = flipper[feature_name.to_sym]
13
+ feature = flipper[feature_name]
13
14
  @feature = Decorators::Feature.new(feature)
14
15
 
15
16
  begin
@@ -5,11 +5,12 @@ module Flipper
5
5
  module UI
6
6
  module Actions
7
7
  class PercentageOfTimeGate < UI::Action
8
- route %r{features/[^/]*/percentage_of_time/?\Z}
8
+ include FeatureNameFromRoute
9
+
10
+ route %r{\A/features/(?<feature_name>.*)/percentage_of_time/?\Z}
9
11
 
10
12
  def post
11
- feature_name = Rack::Utils.unescape(request.path.split('/')[-2])
12
- feature = flipper[feature_name.to_sym]
13
+ feature = flipper[feature_name]
13
14
  @feature = Decorators::Feature.new(feature)
14
15
 
15
16
  begin
@@ -17,13 +17,12 @@ module Flipper
17
17
 
18
18
  # UI
19
19
  @action_collection.add UI::Actions::AddFeature
20
- @action_collection.add UI::Actions::Feature
21
20
  @action_collection.add UI::Actions::ActorsGate
22
21
  @action_collection.add UI::Actions::GroupsGate
23
22
  @action_collection.add UI::Actions::BooleanGate
24
23
  @action_collection.add UI::Actions::PercentageOfTimeGate
25
24
  @action_collection.add UI::Actions::PercentageOfActorsGate
26
- @action_collection.add UI::Actions::Gate
25
+ @action_collection.add UI::Actions::Feature
27
26
  @action_collection.add UI::Actions::Features
28
27
 
29
28
  # Static Assets/Files
@@ -14,7 +14,7 @@
14
14
  </h4>
15
15
  <div class="card-body">
16
16
  <p>
17
- <% if @feature.on? %>
17
+ <% if @feature.boolean_value %>
18
18
  The feature is enabled for <strong>everyone</strong>. Disable this feature with one click.
19
19
  <% else %>
20
20
  Enable or disable this feature for <strong>everyone</strong> with one click.
@@ -24,7 +24,7 @@
24
24
  <form action="<%= script_name %>/features/<%= @feature.key %>/boolean" method="post">
25
25
  <%== csrf_input_tag %>
26
26
 
27
- <% unless @feature.on? %>
27
+ <% unless @feature.boolean_value %>
28
28
  <button type="submit" name="action" value="Enable" class="btn btn-danger" data-toggle="tooltip" title="Enable for everyone">Enable</button>
29
29
  <% end %>
30
30
 
@@ -1,3 +1,3 @@
1
1
  module Flipper
2
- VERSION = '0.15.0'.freeze
2
+ VERSION = '0.16.0'.freeze
3
3
  end
@@ -27,6 +27,21 @@ RSpec.describe Flipper::UI::Actions::ActorsGate do
27
27
  end
28
28
  end
29
29
 
30
+ describe 'GET /features/:feature/actors with slash in feature name' do
31
+ before do
32
+ get 'features/a/b/actors'
33
+ end
34
+
35
+ it 'responds with success' do
36
+ expect(last_response.status).to be(200)
37
+ end
38
+
39
+ it 'renders add new actor form' do
40
+ form = '<form action="/features/a/b/actors" method="post" class="form-inline">'
41
+ expect(last_response.body).to include(form)
42
+ end
43
+ end
44
+
30
45
  describe 'POST /features/:feature/actors' do
31
46
  context 'enabling an actor' do
32
47
  let(:value) { 'User:6' }
@@ -107,4 +107,24 @@ RSpec.describe Flipper::UI::Actions::Feature do
107
107
  expect(last_response.body).to include('Percentage of Actors')
108
108
  end
109
109
  end
110
+
111
+ describe 'GET /features/:feature with slash in feature name' do
112
+ before do
113
+ get '/features/a/b'
114
+ end
115
+
116
+ it 'responds with success' do
117
+ expect(last_response.status).to be(200)
118
+ end
119
+
120
+ it 'renders template' do
121
+ expect(last_response.body).to include('a/b')
122
+ expect(last_response.body).to include('Enable')
123
+ expect(last_response.body).to include('Disable')
124
+ expect(last_response.body).to include('Actors')
125
+ expect(last_response.body).to include('Groups')
126
+ expect(last_response.body).to include('Percentage of Time')
127
+ expect(last_response.body).to include('Percentage of Actors')
128
+ end
129
+ end
110
130
  end
@@ -1,5 +1,4 @@
1
1
  require 'helper'
2
- require 'flipper/adapters/memory'
3
2
 
4
3
  RSpec.describe Flipper::UI::Decorators::Feature do
5
4
  let(:source) { {} }
@@ -1,5 +1,4 @@
1
1
  require 'helper'
2
- require 'flipper/adapters/memory'
3
2
  require 'flipper/ui/decorators/gate'
4
3
 
5
4
  RSpec.describe Flipper::UI::Decorators::Gate do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: flipper-ui
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.15.0
4
+ version: 0.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Nunemaker
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-05-12 00:00:00.000000000 Z
11
+ date: 2018-08-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -56,14 +56,14 @@ dependencies:
56
56
  requirements:
57
57
  - - "~>"
58
58
  - !ruby/object:Gem::Version
59
- version: 0.15.0
59
+ version: 0.16.0
60
60
  type: :runtime
61
61
  prerelease: false
62
62
  version_requirements: !ruby/object:Gem::Requirement
63
63
  requirements:
64
64
  - - "~>"
65
65
  - !ruby/object:Gem::Version
66
- version: 0.15.0
66
+ version: 0.16.0
67
67
  - !ruby/object:Gem::Dependency
68
68
  name: erubis
69
69
  requirement: !ruby/object:Gem::Requirement
@@ -103,7 +103,6 @@ files:
103
103
  - lib/flipper/ui/actions/feature.rb
104
104
  - lib/flipper/ui/actions/features.rb
105
105
  - lib/flipper/ui/actions/file.rb
106
- - lib/flipper/ui/actions/gate.rb
107
106
  - lib/flipper/ui/actions/groups_gate.rb
108
107
  - lib/flipper/ui/actions/home.rb
109
108
  - lib/flipper/ui/actions/percentage_of_actors_gate.rb
@@ -259,7 +258,6 @@ files:
259
258
  - spec/flipper/ui/actions/feature_spec.rb
260
259
  - spec/flipper/ui/actions/features_spec.rb
261
260
  - spec/flipper/ui/actions/file_spec.rb
262
- - spec/flipper/ui/actions/gate_spec.rb
263
261
  - spec/flipper/ui/actions/groups_gate_spec.rb
264
262
  - spec/flipper/ui/actions/home_spec.rb
265
263
  - spec/flipper/ui/actions/percentage_of_actors_gate_spec.rb
@@ -272,7 +270,8 @@ files:
272
270
  homepage: https://github.com/jnunemaker/flipper
273
271
  licenses:
274
272
  - MIT
275
- metadata: {}
273
+ metadata:
274
+ changelog_uri: https://github.com/jnunemaker/flipper/blob/master/Changelog.md
276
275
  post_install_message:
277
276
  rdoc_options: []
278
277
  require_paths:
@@ -301,7 +300,6 @@ test_files:
301
300
  - spec/flipper/ui/actions/feature_spec.rb
302
301
  - spec/flipper/ui/actions/features_spec.rb
303
302
  - spec/flipper/ui/actions/file_spec.rb
304
- - spec/flipper/ui/actions/gate_spec.rb
305
303
  - spec/flipper/ui/actions/groups_gate_spec.rb
306
304
  - spec/flipper/ui/actions/home_spec.rb
307
305
  - spec/flipper/ui/actions/percentage_of_actors_gate_spec.rb
@@ -1,39 +0,0 @@
1
- require 'flipper/ui/action'
2
- require 'flipper/ui/decorators/feature'
3
-
4
- module Flipper
5
- module UI
6
- module Actions
7
- class Gate < UI::Action
8
- route %r{features/[^/]*/[^/]*/?\Z}
9
-
10
- def post
11
- feature_name, gate_name = request.path.split('/').pop(2)
12
- .map(&Rack::Utils.method(:unescape))
13
- update_gate_method_name = "update_#{gate_name}"
14
-
15
- feature = flipper[feature_name.to_sym]
16
- @feature = Decorators::Feature.new(feature)
17
-
18
- if respond_to?(update_gate_method_name, true)
19
- send(update_gate_method_name, feature)
20
- else
21
- update_gate_method_undefined(gate_name)
22
- end
23
-
24
- redirect_to "/features/#{@feature.key}"
25
- end
26
-
27
- private
28
-
29
- # Private: Returns error response that gate update method is not defined.
30
- def update_gate_method_undefined(gate_name)
31
- error = Rack::Utils.escape(
32
- "#{gate_name.inspect} gate does not exist therefore it cannot be updated."
33
- )
34
- redirect_to("/features/#{@feature.key}?error=#{error}")
35
- end
36
- end
37
- end
38
- end
39
- end
@@ -1,37 +0,0 @@
1
- require 'helper'
2
-
3
- RSpec.describe Flipper::UI::Actions::Gate do
4
- let(:token) do
5
- if Rack::Protection::AuthenticityToken.respond_to?(:random_token)
6
- Rack::Protection::AuthenticityToken.random_token
7
- else
8
- 'a'
9
- end
10
- end
11
- let(:session) do
12
- { :csrf => token, 'csrf' => token, '_csrf_token' => token }
13
- end
14
-
15
- describe 'POST /features/:feature/non-existent-gate' do
16
- before do
17
- post '/features/search/non-existent-gate',
18
- { 'authenticity_token' => token },
19
- 'rack.session' => session
20
- end
21
-
22
- it 'responds with redirect' do
23
- expect(last_response.status).to be(302)
24
- end
25
-
26
- # rubocop:disable Metrics/LineLength
27
- it 'escapes error message' do
28
- expect(last_response.headers['Location']).to eq('/features/search?error=%22non-existent-gate%22+gate+does+not+exist+therefore+it+cannot+be+updated.')
29
- end
30
- # rubocop:enable Metrics/LineLength
31
-
32
- it 'renders error in template' do
33
- follow_redirect!
34
- expect(last_response.body).to match(/non-existent-gate.*gate does not exist/)
35
- end
36
- end
37
- end