flipper-ui 0.10.2 → 0.11.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/flipper-ui.gemspec +12 -12
- data/lib/flipper/ui.rb +6 -4
- data/lib/flipper/ui/action.rb +22 -21
- data/lib/flipper/ui/action_collection.rb +2 -2
- data/lib/flipper/ui/actions/actors_gate.rb +7 -7
- data/lib/flipper/ui/actions/add_feature.rb +6 -6
- data/lib/flipper/ui/actions/boolean_gate.rb +2 -2
- data/lib/flipper/ui/actions/feature.rb +5 -6
- data/lib/flipper/ui/actions/features.rb +9 -10
- data/lib/flipper/ui/actions/file.rb +0 -1
- data/lib/flipper/ui/actions/gate.rb +5 -2
- data/lib/flipper/ui/actions/groups_gate.rb +16 -14
- data/lib/flipper/ui/actions/home.rb +1 -2
- data/lib/flipper/ui/actions/percentage_of_actors_gate.rb +2 -2
- data/lib/flipper/ui/actions/percentage_of_time_gate.rb +2 -2
- data/lib/flipper/ui/decorators/feature.rb +9 -9
- data/lib/flipper/ui/decorators/gate.rb +7 -6
- data/lib/flipper/ui/middleware.rb +2 -27
- data/lib/flipper/ui/util.rb +2 -2
- data/lib/flipper/version.rb +1 -1
- data/spec/flipper/ui/action_spec.rb +22 -22
- data/spec/flipper/ui/actions/actors_gate_spec.rb +49 -45
- data/spec/flipper/ui/actions/add_feature_spec.rb +9 -9
- data/spec/flipper/ui/actions/boolean_gate_spec.rb +22 -22
- data/spec/flipper/ui/actions/feature_spec.rb +34 -34
- data/spec/flipper/ui/actions/features_spec.rb +44 -40
- data/spec/flipper/ui/actions/file_spec.rb +8 -8
- data/spec/flipper/ui/actions/gate_spec.rb +17 -15
- data/spec/flipper/ui/actions/groups_gate_spec.rb +57 -50
- data/spec/flipper/ui/actions/home_spec.rb +4 -4
- data/spec/flipper/ui/actions/percentage_of_actors_gate_spec.rb +24 -22
- data/spec/flipper/ui/actions/percentage_of_time_gate_spec.rb +24 -22
- data/spec/flipper/ui/decorators/feature_spec.rb +27 -27
- data/spec/flipper/ui/decorators/gate_spec.rb +10 -10
- data/spec/flipper/ui/util_spec.rb +4 -4
- data/spec/flipper/ui_spec.rb +58 -59
- metadata +7 -7
@@ -23,20 +23,20 @@ module Flipper
|
|
23
23
|
'id' => name.to_s,
|
24
24
|
'name' => pretty_name,
|
25
25
|
'state' => state.to_s,
|
26
|
-
'gates' => gates.map
|
26
|
+
'gates' => gates.map do |gate|
|
27
27
|
Decorators::Gate.new(gate, gate_values[gate.key]).as_json
|
28
|
-
|
28
|
+
end,
|
29
29
|
}
|
30
30
|
end
|
31
31
|
|
32
32
|
def color_class
|
33
33
|
case feature.state
|
34
34
|
when :on
|
35
|
-
|
35
|
+
'text-open'
|
36
36
|
when :off
|
37
|
-
|
37
|
+
'text-closed'
|
38
38
|
when :conditional
|
39
|
-
|
39
|
+
'text-pending'
|
40
40
|
end
|
41
41
|
end
|
42
42
|
|
@@ -45,10 +45,10 @@ module Flipper
|
|
45
45
|
end
|
46
46
|
|
47
47
|
StateSortMap = {
|
48
|
-
:
|
49
|
-
:
|
50
|
-
:
|
51
|
-
}
|
48
|
+
on: 1,
|
49
|
+
conditional: 2,
|
50
|
+
off: 3,
|
51
|
+
}.freeze
|
52
52
|
|
53
53
|
def <=>(other)
|
54
54
|
if state == other.state
|
@@ -17,12 +17,13 @@ module Flipper
|
|
17
17
|
|
18
18
|
# Public: Returns instance as hash that is ready to be json dumped.
|
19
19
|
def as_json
|
20
|
-
value_as_json =
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
20
|
+
value_as_json =
|
21
|
+
case data_type
|
22
|
+
when :set
|
23
|
+
value.to_a # json doesn't like sets
|
24
|
+
else
|
25
|
+
value
|
26
|
+
end
|
26
27
|
|
27
28
|
{
|
28
29
|
'key' => gate.key.to_s,
|
@@ -9,31 +9,9 @@ end
|
|
9
9
|
module Flipper
|
10
10
|
module UI
|
11
11
|
class Middleware
|
12
|
-
|
13
|
-
#
|
14
|
-
# app - The app this middleware is included in.
|
15
|
-
# flipper_or_block - The Flipper::DSL instance or a block that yields a
|
16
|
-
# Flipper::DSL instance to use for all operations.
|
17
|
-
#
|
18
|
-
# Examples
|
19
|
-
#
|
20
|
-
# flipper = Flipper.new(...)
|
21
|
-
#
|
22
|
-
# # using with a normal flipper instance
|
23
|
-
# use Flipper::UI::Middleware, flipper
|
24
|
-
#
|
25
|
-
# # using with a block that yields a flipper instance
|
26
|
-
# use Flipper::UI::Middleware, lambda { Flipper.new(...) }
|
27
|
-
#
|
28
|
-
def initialize(app, flipper_or_block)
|
12
|
+
def initialize(app)
|
29
13
|
@app = app
|
30
14
|
|
31
|
-
if flipper_or_block.respond_to?(:call)
|
32
|
-
@flipper_block = flipper_or_block
|
33
|
-
else
|
34
|
-
@flipper = flipper_or_block
|
35
|
-
end
|
36
|
-
|
37
15
|
@action_collection = ActionCollection.new
|
38
16
|
|
39
17
|
# UI
|
@@ -54,10 +32,6 @@ module Flipper
|
|
54
32
|
@action_collection.add UI::Actions::Home
|
55
33
|
end
|
56
34
|
|
57
|
-
def flipper
|
58
|
-
@flipper ||= @flipper_block.call
|
59
|
-
end
|
60
|
-
|
61
35
|
def call(env)
|
62
36
|
dup.call!(env)
|
63
37
|
end
|
@@ -69,6 +43,7 @@ module Flipper
|
|
69
43
|
if action_class.nil?
|
70
44
|
@app.call(env)
|
71
45
|
else
|
46
|
+
flipper = env.fetch('flipper')
|
72
47
|
action_class.run(flipper, request)
|
73
48
|
end
|
74
49
|
end
|
data/lib/flipper/ui/util.rb
CHANGED
@@ -2,14 +2,14 @@ module Flipper
|
|
2
2
|
module UI
|
3
3
|
module Util
|
4
4
|
# Private: 0x3000: fullwidth whitespace
|
5
|
-
NON_WHITESPACE_REGEXP =
|
5
|
+
NON_WHITESPACE_REGEXP = /[^\s#{[0x3000].pack("U")}]/
|
6
6
|
|
7
7
|
def self.blank?(str)
|
8
8
|
str.to_s !~ NON_WHITESPACE_REGEXP
|
9
9
|
end
|
10
10
|
|
11
11
|
def self.titleize(str)
|
12
|
-
str.to_s.split(
|
12
|
+
str.to_s.split('_').map(&:capitalize).join(' ')
|
13
13
|
end
|
14
14
|
end
|
15
15
|
end
|
data/lib/flipper/version.rb
CHANGED
@@ -1,59 +1,59 @@
|
|
1
1
|
require 'helper'
|
2
2
|
|
3
3
|
RSpec.describe Flipper::UI::Action do
|
4
|
-
let(:action_subclass)
|
4
|
+
let(:action_subclass) do
|
5
5
|
Class.new(described_class) do
|
6
6
|
def noooope
|
7
|
-
raise
|
7
|
+
raise 'should never run this'
|
8
8
|
end
|
9
9
|
|
10
10
|
def get
|
11
|
-
[200, {},
|
11
|
+
[200, {}, 'get']
|
12
12
|
end
|
13
13
|
|
14
14
|
def post
|
15
|
-
[200, {},
|
15
|
+
[200, {}, 'post']
|
16
16
|
end
|
17
17
|
|
18
18
|
def put
|
19
|
-
[200, {},
|
19
|
+
[200, {}, 'put']
|
20
20
|
end
|
21
21
|
|
22
22
|
def delete
|
23
|
-
[200, {},
|
23
|
+
[200, {}, 'delete']
|
24
24
|
end
|
25
25
|
end
|
26
|
-
|
26
|
+
end
|
27
27
|
|
28
28
|
it "won't run method that isn't whitelisted" do
|
29
|
-
fake_request = Struct.new(:request_method, :env, :session).new(
|
29
|
+
fake_request = Struct.new(:request_method, :env, :session).new('NOOOOPE', {}, {})
|
30
30
|
action = action_subclass.new(flipper, fake_request)
|
31
|
-
expect
|
31
|
+
expect do
|
32
32
|
action.run
|
33
|
-
|
33
|
+
end.to raise_error(Flipper::UI::RequestMethodNotSupported)
|
34
34
|
end
|
35
35
|
|
36
|
-
it
|
37
|
-
fake_request = Struct.new(:request_method, :env, :session).new(
|
36
|
+
it 'will run get' do
|
37
|
+
fake_request = Struct.new(:request_method, :env, :session).new('GET', {}, {})
|
38
38
|
action = action_subclass.new(flipper, fake_request)
|
39
|
-
expect(action.run).to eq([200, {},
|
39
|
+
expect(action.run).to eq([200, {}, 'get'])
|
40
40
|
end
|
41
41
|
|
42
|
-
it
|
43
|
-
fake_request = Struct.new(:request_method, :env, :session).new(
|
42
|
+
it 'will run post' do
|
43
|
+
fake_request = Struct.new(:request_method, :env, :session).new('POST', {}, {})
|
44
44
|
action = action_subclass.new(flipper, fake_request)
|
45
|
-
expect(action.run).to eq([200, {},
|
45
|
+
expect(action.run).to eq([200, {}, 'post'])
|
46
46
|
end
|
47
47
|
|
48
|
-
it
|
49
|
-
fake_request = Struct.new(:request_method, :env, :session).new(
|
48
|
+
it 'will run put' do
|
49
|
+
fake_request = Struct.new(:request_method, :env, :session).new('PUT', {}, {})
|
50
50
|
action = action_subclass.new(flipper, fake_request)
|
51
|
-
expect(action.run).to eq([200, {},
|
51
|
+
expect(action.run).to eq([200, {}, 'put'])
|
52
52
|
end
|
53
53
|
|
54
|
-
it
|
55
|
-
fake_request = Struct.new(:request_method, :env, :session).new(
|
54
|
+
it 'will run delete' do
|
55
|
+
fake_request = Struct.new(:request_method, :env, :session).new('DELETE', {}, {})
|
56
56
|
action = action_subclass.new(flipper, fake_request)
|
57
|
-
expect(action.run).to eq([200, {},
|
57
|
+
expect(action.run).to eq([200, {}, 'delete'])
|
58
58
|
end
|
59
59
|
end
|
@@ -1,107 +1,111 @@
|
|
1
1
|
require 'helper'
|
2
2
|
|
3
3
|
RSpec.describe Flipper::UI::Actions::ActorsGate do
|
4
|
-
let(:token)
|
4
|
+
let(:token) do
|
5
5
|
if Rack::Protection::AuthenticityToken.respond_to?(:random_token)
|
6
6
|
Rack::Protection::AuthenticityToken.random_token
|
7
7
|
else
|
8
|
-
|
8
|
+
'a'
|
9
9
|
end
|
10
|
-
|
11
|
-
let(:session)
|
10
|
+
end
|
11
|
+
let(:session) do
|
12
12
|
if Rack::Protection::AuthenticityToken.respond_to?(:random_token)
|
13
|
-
{:
|
13
|
+
{ csrf: token }
|
14
14
|
else
|
15
|
-
{
|
15
|
+
{ '_csrf_token' => token }
|
16
16
|
end
|
17
|
-
|
17
|
+
end
|
18
18
|
|
19
|
-
describe
|
19
|
+
describe 'GET /features/:feature/actors' do
|
20
20
|
before do
|
21
|
-
get
|
21
|
+
get 'features/search/actors'
|
22
22
|
end
|
23
23
|
|
24
|
-
it
|
24
|
+
it 'responds with success' do
|
25
25
|
expect(last_response.status).to be(200)
|
26
26
|
end
|
27
27
|
|
28
|
-
it
|
28
|
+
it 'renders add new actor form' do
|
29
29
|
expect(last_response.body).to include('<form action="/features/search/actors" method="post">')
|
30
30
|
end
|
31
31
|
end
|
32
32
|
|
33
|
-
describe
|
34
|
-
context
|
35
|
-
let(:value) {
|
33
|
+
describe 'POST /features/:feature/actors' do
|
34
|
+
context 'enabling an actor' do
|
35
|
+
let(:value) { 'User:6' }
|
36
36
|
|
37
37
|
before do
|
38
|
-
post
|
39
|
-
|
40
|
-
|
38
|
+
post 'features/search/actors',
|
39
|
+
{ 'value' => value, 'operation' => 'enable', 'authenticity_token' => token },
|
40
|
+
'rack.session' => session
|
41
41
|
end
|
42
42
|
|
43
|
-
it
|
44
|
-
expect(flipper[:search].actors_value).to include(
|
43
|
+
it 'adds item to members' do
|
44
|
+
expect(flipper[:search].actors_value).to include('User:6')
|
45
45
|
end
|
46
46
|
|
47
|
-
it
|
47
|
+
it 'redirects back to feature' do
|
48
48
|
expect(last_response.status).to be(302)
|
49
|
-
expect(last_response.headers[
|
49
|
+
expect(last_response.headers['Location']).to eq('/features/search')
|
50
50
|
end
|
51
51
|
|
52
52
|
context 'value contains whitespace' do
|
53
|
-
let(:value) {
|
53
|
+
let(:value) { ' User:6 ' }
|
54
54
|
|
55
|
-
it
|
56
|
-
expect(flipper[:search].actors_value).to include(
|
55
|
+
it 'adds item without whitespace' do
|
56
|
+
expect(flipper[:search].actors_value).to include('User:6')
|
57
57
|
end
|
58
58
|
end
|
59
59
|
|
60
|
-
context
|
61
|
-
context
|
62
|
-
let(:value) {
|
60
|
+
context 'for an invalid actor value' do
|
61
|
+
context 'empty value' do
|
62
|
+
let(:value) { '' }
|
63
63
|
|
64
|
-
|
64
|
+
# rubocop:disable Metrics/LineLength
|
65
|
+
it 'redirects back to feature' do
|
65
66
|
expect(last_response.status).to be(302)
|
66
|
-
expect(last_response.headers[
|
67
|
+
expect(last_response.headers['Location']).to eq('/features/search/actors?error=%22%22+is+not+a+valid+actor+value.')
|
67
68
|
end
|
69
|
+
# rubocop:enable Metrics/LineLength
|
68
70
|
end
|
69
71
|
|
70
|
-
context
|
72
|
+
context 'nil value' do
|
71
73
|
let(:value) { nil }
|
72
74
|
|
73
|
-
|
75
|
+
# rubocop:disable Metrics/LineLength
|
76
|
+
it 'redirects back to feature' do
|
74
77
|
expect(last_response.status).to be(302)
|
75
|
-
expect(last_response.headers[
|
78
|
+
expect(last_response.headers['Location']).to eq('/features/search/actors?error=%22%22+is+not+a+valid+actor+value.')
|
76
79
|
end
|
80
|
+
# rubocop:enable Metrics/LineLength
|
77
81
|
end
|
78
82
|
end
|
79
83
|
end
|
80
84
|
|
81
|
-
context
|
82
|
-
let(:value) {
|
85
|
+
context 'disabling an actor' do
|
86
|
+
let(:value) { 'User:6' }
|
83
87
|
|
84
88
|
before do
|
85
|
-
flipper[:search].enable_actor Flipper::UI::Actor.new(
|
86
|
-
post
|
87
|
-
|
88
|
-
|
89
|
+
flipper[:search].enable_actor Flipper::UI::Actor.new('User:6')
|
90
|
+
post 'features/search/actors',
|
91
|
+
{ 'value' => value, 'operation' => 'disable', 'authenticity_token' => token },
|
92
|
+
'rack.session' => session
|
89
93
|
end
|
90
94
|
|
91
|
-
it
|
92
|
-
expect(flipper[:search].actors_value).not_to include(
|
95
|
+
it 'removes item from members' do
|
96
|
+
expect(flipper[:search].actors_value).not_to include('User:6')
|
93
97
|
end
|
94
98
|
|
95
|
-
it
|
99
|
+
it 'redirects back to feature' do
|
96
100
|
expect(last_response.status).to be(302)
|
97
|
-
expect(last_response.headers[
|
101
|
+
expect(last_response.headers['Location']).to eq('/features/search')
|
98
102
|
end
|
99
103
|
|
100
104
|
context 'value contains whitespace' do
|
101
|
-
let(:value) {
|
105
|
+
let(:value) { ' User:6 ' }
|
102
106
|
|
103
|
-
it
|
104
|
-
expect(flipper[:search].actors_value).not_to include(
|
107
|
+
it 'removes item whitout whitespace' do
|
108
|
+
expect(flipper[:search].actors_value).not_to include('User:6')
|
105
109
|
end
|
106
110
|
end
|
107
111
|
end
|
@@ -1,43 +1,43 @@
|
|
1
1
|
require 'helper'
|
2
2
|
|
3
3
|
RSpec.describe Flipper::UI::Actions::AddFeature do
|
4
|
-
describe
|
4
|
+
describe 'GET /features/new with feature_creation_enabled set to true' do
|
5
5
|
before do
|
6
6
|
@original_feature_creation_enabled = Flipper::UI.feature_creation_enabled
|
7
7
|
Flipper::UI.feature_creation_enabled = true
|
8
|
-
get
|
8
|
+
get '/features/new'
|
9
9
|
end
|
10
10
|
|
11
11
|
after do
|
12
12
|
Flipper::UI.feature_creation_enabled = @original_feature_creation_enabled
|
13
13
|
end
|
14
14
|
|
15
|
-
it
|
15
|
+
it 'responds with success' do
|
16
16
|
expect(last_response.status).to be(200)
|
17
17
|
end
|
18
18
|
|
19
|
-
it
|
19
|
+
it 'renders template' do
|
20
20
|
expect(last_response.body).to include('<form action="/features" method="post">')
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
24
|
-
describe
|
24
|
+
describe 'GET /features/new with feature_creation_enabled set to false' do
|
25
25
|
before do
|
26
26
|
@original_feature_creation_enabled = Flipper::UI.feature_creation_enabled
|
27
27
|
Flipper::UI.feature_creation_enabled = false
|
28
|
-
get
|
28
|
+
get '/features/new'
|
29
29
|
end
|
30
30
|
|
31
31
|
after do
|
32
32
|
Flipper::UI.feature_creation_enabled = @original_feature_creation_enabled
|
33
33
|
end
|
34
34
|
|
35
|
-
it
|
35
|
+
it 'returns 403' do
|
36
36
|
expect(last_response.status).to be(403)
|
37
37
|
end
|
38
38
|
|
39
|
-
it
|
40
|
-
expect(last_response.body).to include(
|
39
|
+
it 'renders feature creation disabled template' do
|
40
|
+
expect(last_response.body).to include('Feature creation is disabled.')
|
41
41
|
end
|
42
42
|
end
|
43
43
|
end
|
@@ -1,55 +1,55 @@
|
|
1
1
|
require 'helper'
|
2
2
|
|
3
3
|
RSpec.describe Flipper::UI::Actions::BooleanGate do
|
4
|
-
let(:token)
|
4
|
+
let(:token) do
|
5
5
|
if Rack::Protection::AuthenticityToken.respond_to?(:random_token)
|
6
6
|
Rack::Protection::AuthenticityToken.random_token
|
7
7
|
else
|
8
|
-
|
8
|
+
'a'
|
9
9
|
end
|
10
|
-
|
11
|
-
let(:session)
|
10
|
+
end
|
11
|
+
let(:session) do
|
12
12
|
if Rack::Protection::AuthenticityToken.respond_to?(:random_token)
|
13
|
-
{:
|
13
|
+
{ csrf: token }
|
14
14
|
else
|
15
|
-
{
|
15
|
+
{ '_csrf_token' => token }
|
16
16
|
end
|
17
|
-
|
17
|
+
end
|
18
18
|
|
19
|
-
describe
|
20
|
-
context
|
19
|
+
describe 'POST /features/:feature/boolean' do
|
20
|
+
context 'with enable' do
|
21
21
|
before do
|
22
22
|
flipper.disable :search
|
23
|
-
post
|
24
|
-
|
25
|
-
|
23
|
+
post 'features/search/boolean',
|
24
|
+
{ 'action' => 'Enable', 'authenticity_token' => token },
|
25
|
+
'rack.session' => session
|
26
26
|
end
|
27
27
|
|
28
|
-
it
|
28
|
+
it 'enables the feature' do
|
29
29
|
expect(flipper.enabled?(:search)).to be(true)
|
30
30
|
end
|
31
31
|
|
32
|
-
it
|
32
|
+
it 'redirects back to feature' do
|
33
33
|
expect(last_response.status).to be(302)
|
34
|
-
expect(last_response.headers[
|
34
|
+
expect(last_response.headers['Location']).to eq('/features/search')
|
35
35
|
end
|
36
36
|
end
|
37
37
|
|
38
|
-
context
|
38
|
+
context 'with disable' do
|
39
39
|
before do
|
40
40
|
flipper.enable :search
|
41
|
-
post
|
42
|
-
|
43
|
-
|
41
|
+
post 'features/search/boolean',
|
42
|
+
{ 'action' => 'Disable', 'authenticity_token' => token },
|
43
|
+
'rack.session' => session
|
44
44
|
end
|
45
45
|
|
46
|
-
it
|
46
|
+
it 'disables the feature' do
|
47
47
|
expect(flipper.enabled?(:search)).to be(false)
|
48
48
|
end
|
49
49
|
|
50
|
-
it
|
50
|
+
it 'redirects back to feature' do
|
51
51
|
expect(last_response.status).to be(302)
|
52
|
-
expect(last_response.headers[
|
52
|
+
expect(last_response.headers['Location']).to eq('/features/search')
|
53
53
|
end
|
54
54
|
end
|
55
55
|
end
|