flipper-ui 1.4.0 → 1.4.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.
- checksums.yaml +4 -4
- data/lib/flipper/ui/actions/boolean_gate.rb +4 -0
- data/lib/flipper/ui/actions/import.rb +1 -0
- data/lib/flipper/ui/configuration.rb +30 -0
- data/lib/flipper/ui/views/disable_fully_enable.erb +3 -0
- data/lib/flipper/ui/views/feature.erb +17 -9
- data/lib/flipper/ui/views/layout.erb +7 -3
- data/lib/flipper/ui/views/settings.erb +2 -0
- data/lib/flipper/version.rb +1 -1
- data/spec/flipper/ui/actions/boolean_gate_spec.rb +71 -0
- data/spec/flipper/ui/actions/feature_spec.rb +26 -0
- data/spec/flipper/ui/actions/import_spec.rb +23 -1
- data/spec/flipper/ui/configuration_spec.rb +44 -0
- metadata +6 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0cb0f4ab52d69d747f65f3a49aa89656138bb326434e0d4b9e21a6827479fea0
|
|
4
|
+
data.tar.gz: 227e30ad6e4d67f2b69e73a4aed099c8e91fb7651aad1c4a5bb54881ab925f97
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bd9a23b242736d416fc7ea1a84a5c8af833916211ba6963340d30025b625bb16a4addc0342df706fdf10a528f93f2817c3dd1f7ff6f45a53d974e74de6b0edb7
|
|
7
|
+
data.tar.gz: 564f9d333365d60310f141efd208de1c3a5957c3c20b828f2f024922952d79992eb833073d4634813c4c6f208de8e75d04a1fb1a5492356ba08a84cf6cca9958
|
|
@@ -16,6 +16,10 @@ module Flipper
|
|
|
16
16
|
@feature = Decorators::Feature.new(feature)
|
|
17
17
|
|
|
18
18
|
if params['action'] == 'Enable'
|
|
19
|
+
if Flipper::UI.configuration.disable_fully_enable
|
|
20
|
+
status 403
|
|
21
|
+
halt view_response(:disable_fully_enable)
|
|
22
|
+
end
|
|
19
23
|
feature.enable
|
|
20
24
|
else
|
|
21
25
|
feature.disable
|
|
@@ -45,6 +45,11 @@ module Flipper
|
|
|
45
45
|
# false and it will go away. Defaults to true.
|
|
46
46
|
attr_accessor :cloud_recommendation
|
|
47
47
|
|
|
48
|
+
# Public: Set to false to disable the version check that fetches the
|
|
49
|
+
# latest release from flippercloud.io. Useful when a strict Content
|
|
50
|
+
# Security Policy is in place. Defaults to true.
|
|
51
|
+
attr_accessor :version_check_enabled
|
|
52
|
+
|
|
48
53
|
# Public: What should show up in the form to add actors. This can be
|
|
49
54
|
# different per application since flipper_id's can be whatever an
|
|
50
55
|
# application needs. Defaults to "a flipper id".
|
|
@@ -77,6 +82,20 @@ module Flipper
|
|
|
77
82
|
# Default is false.
|
|
78
83
|
attr_accessor :confirm_disable
|
|
79
84
|
|
|
85
|
+
# Public: Set to disable the Fully Enable button in the UI, preventing
|
|
86
|
+
# users from fully enabling features via the web interface. Set to true
|
|
87
|
+
# for a default message, or a string for a custom message. Defaults to nil.
|
|
88
|
+
#
|
|
89
|
+
# Note: This only affects the UI. If flipper-api is mounted, full enable
|
|
90
|
+
# is still possible via the API.
|
|
91
|
+
#
|
|
92
|
+
# Examples:
|
|
93
|
+
#
|
|
94
|
+
# config.disable_fully_enable = true
|
|
95
|
+
# config.disable_fully_enable = "Use deploy pipeline instead."
|
|
96
|
+
#
|
|
97
|
+
attr_accessor :disable_fully_enable
|
|
98
|
+
|
|
80
99
|
VALID_BANNER_CLASS_VALUES = %w(
|
|
81
100
|
danger
|
|
82
101
|
dark
|
|
@@ -90,6 +109,7 @@ module Flipper
|
|
|
90
109
|
|
|
91
110
|
DEFAULT_DESCRIPTIONS_SOURCE = ->(_keys) { {} }
|
|
92
111
|
DEFAULT_ACTOR_NAMES_SOURCE = ->(_keys) { {} }
|
|
112
|
+
DEFAULT_DISABLE_FULLY_ENABLE_MESSAGE = "Fully enabling features via the UI is disabled."
|
|
93
113
|
|
|
94
114
|
def initialize
|
|
95
115
|
@delete = Option.new("Danger Zone", "Deleting a feature removes it from the list of features and disables it for everyone.")
|
|
@@ -99,6 +119,7 @@ module Flipper
|
|
|
99
119
|
@feature_removal_enabled = true
|
|
100
120
|
@fun = true
|
|
101
121
|
@cloud_recommendation = true
|
|
122
|
+
@version_check_enabled = true
|
|
102
123
|
@add_actor_placeholder = "a flipper id"
|
|
103
124
|
@descriptions_source = DEFAULT_DESCRIPTIONS_SOURCE
|
|
104
125
|
@actor_names_source = DEFAULT_ACTOR_NAMES_SOURCE
|
|
@@ -106,6 +127,7 @@ module Flipper
|
|
|
106
127
|
@actors_separator = ','
|
|
107
128
|
@confirm_fully_enable = false
|
|
108
129
|
@confirm_disable = true
|
|
130
|
+
@disable_fully_enable = nil
|
|
109
131
|
@read_only = false
|
|
110
132
|
@nav_items = [
|
|
111
133
|
{ title: "Features", href: "features" },
|
|
@@ -121,6 +143,14 @@ module Flipper
|
|
|
121
143
|
using_descriptions? && @show_feature_description_in_list
|
|
122
144
|
end
|
|
123
145
|
|
|
146
|
+
def disable_fully_enable_message
|
|
147
|
+
if @disable_fully_enable.is_a?(String)
|
|
148
|
+
@disable_fully_enable
|
|
149
|
+
else
|
|
150
|
+
DEFAULT_DISABLE_FULLY_ENABLE_MESSAGE
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
124
154
|
def banner_class=(value)
|
|
125
155
|
unless VALID_BANNER_CLASS_VALUES.include?(value)
|
|
126
156
|
raise InvalidConfigurationValue, "The banner_class provided '#{value}' is " \
|
|
@@ -255,16 +255,24 @@
|
|
|
255
255
|
<div class="row">
|
|
256
256
|
<% unless @feature.boolean_value %>
|
|
257
257
|
<div class="col d-grid">
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
>
|
|
264
|
-
<span class="d-block" data-bs-toggle="tooltip" title="Enable for everyone">
|
|
265
|
-
Fully Enable
|
|
258
|
+
<% if Flipper::UI.configuration.disable_fully_enable %>
|
|
259
|
+
<span class="d-inline-block" tabindex="0" data-bs-toggle="tooltip" title="<%= Flipper::UI.configuration.disable_fully_enable_message %>">
|
|
260
|
+
<button type="submit" name="action" value="Enable" class="btn btn-outline-success w-100" disabled style="pointer-events: none;">
|
|
261
|
+
Fully Enable
|
|
262
|
+
</button>
|
|
266
263
|
</span>
|
|
267
|
-
|
|
264
|
+
<% else %>
|
|
265
|
+
<button type="submit" name="action" value="Enable" class="btn btn-outline-success"
|
|
266
|
+
<% if Flipper::UI.configuration.confirm_fully_enable %>
|
|
267
|
+
data-confirmation-prompt="Are you sure you want to fully enable this feature for everyone? Please enter the name of the feature to confirm it: <%= feature_name %>"
|
|
268
|
+
data-confirmation-text="<%= feature_name %>"
|
|
269
|
+
<% end %>
|
|
270
|
+
>
|
|
271
|
+
<span class="d-block" data-bs-toggle="tooltip" title="Enable for everyone">
|
|
272
|
+
Fully Enable
|
|
273
|
+
</span>
|
|
274
|
+
</button>
|
|
275
|
+
<% end %>
|
|
268
276
|
</div>
|
|
269
277
|
<% end %>
|
|
270
278
|
|
|
@@ -18,8 +18,10 @@
|
|
|
18
18
|
<%- end -%>
|
|
19
19
|
|
|
20
20
|
<div class="text-muted small text-end">
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
<% if Flipper::UI.configuration.version_check_enabled %>
|
|
22
|
+
<a href="#" class="badge text-bg-warning ms-2 d-none" style="font-size:100%" id="new-version-badge" data-version="<%= Flipper::VERSION %>">
|
|
23
|
+
</a>
|
|
24
|
+
<% end %>
|
|
23
25
|
|
|
24
26
|
<% if Flipper.deprecated_ruby_version? %>
|
|
25
27
|
<a href="https://github.com/flippercloud/flipper/pull/776" class="badge text-bg-danger ms-2" style="font-size:100%">
|
|
@@ -83,6 +85,8 @@
|
|
|
83
85
|
<script src="<%= script_name + popper_js[:src] %>" integrity="<%= popper_js[:hash] %>" crossorigin="anonymous"></script>
|
|
84
86
|
<script src="<%= script_name + bootstrap_js[:src] %>" integrity="<%= bootstrap_js[:hash] %>" crossorigin="anonymous"></script>
|
|
85
87
|
<script src="<%= script_name %>/js/application.js?v=<%= Flipper::VERSION %>"></script>
|
|
86
|
-
|
|
88
|
+
<% if Flipper::UI.configuration.version_check_enabled %>
|
|
89
|
+
<script src="<%= script_name %>/js/version.js?v=<%= Flipper::VERSION %>"></script>
|
|
90
|
+
<% end %>
|
|
87
91
|
</body>
|
|
88
92
|
</html>
|
data/lib/flipper/version.rb
CHANGED
|
@@ -64,5 +64,76 @@ RSpec.describe Flipper::UI::Actions::BooleanGate do
|
|
|
64
64
|
expect(last_response.headers['location']).to eq('/features/search')
|
|
65
65
|
end
|
|
66
66
|
end
|
|
67
|
+
|
|
68
|
+
context 'when disable_fully_enable is false' do
|
|
69
|
+
before { Flipper::UI.configuration.disable_fully_enable = false }
|
|
70
|
+
after { Flipper::UI.configuration.disable_fully_enable = nil }
|
|
71
|
+
|
|
72
|
+
it 'allows enabling the feature' do
|
|
73
|
+
flipper.disable :search
|
|
74
|
+
post 'features/search/boolean',
|
|
75
|
+
{ 'action' => 'Enable', 'authenticity_token' => token },
|
|
76
|
+
'rack.session' => session
|
|
77
|
+
expect(flipper.enabled?(:search)).to be(true)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
context 'when disable_fully_enable is true' do
|
|
82
|
+
before { Flipper::UI.configuration.disable_fully_enable = true }
|
|
83
|
+
after { Flipper::UI.configuration.disable_fully_enable = nil }
|
|
84
|
+
|
|
85
|
+
context 'with enable' do
|
|
86
|
+
before do
|
|
87
|
+
flipper.disable :search
|
|
88
|
+
post 'features/search/boolean',
|
|
89
|
+
{ 'action' => 'Enable', 'authenticity_token' => token },
|
|
90
|
+
'rack.session' => session
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it 'does not enable the feature' do
|
|
94
|
+
expect(flipper.enabled?(:search)).to be(false)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it 'returns 403 status' do
|
|
98
|
+
expect(last_response.status).to be(403)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it 'renders the default disabled message' do
|
|
102
|
+
expect(last_response.body).to include('Fully enabling features via the UI is disabled.')
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
context 'with disable' do
|
|
107
|
+
before do
|
|
108
|
+
flipper.enable :search
|
|
109
|
+
post 'features/search/boolean',
|
|
110
|
+
{ 'action' => 'Disable', 'authenticity_token' => token },
|
|
111
|
+
'rack.session' => session
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it 'still allows disabling the feature' do
|
|
115
|
+
expect(flipper.enabled?(:search)).to be(false)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it 'redirects back to feature' do
|
|
119
|
+
expect(last_response.status).to be(302)
|
|
120
|
+
expect(last_response.headers['location']).to eq('/features/search')
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
context 'when disable_fully_enable is a custom message' do
|
|
126
|
+
before { Flipper::UI.configuration.disable_fully_enable = "Use deploy pipeline instead." }
|
|
127
|
+
after { Flipper::UI.configuration.disable_fully_enable = nil }
|
|
128
|
+
|
|
129
|
+
it 'renders the custom message on 403' do
|
|
130
|
+
flipper.disable :search
|
|
131
|
+
post 'features/search/boolean',
|
|
132
|
+
{ 'action' => 'Enable', 'authenticity_token' => token },
|
|
133
|
+
'rack.session' => session
|
|
134
|
+
expect(last_response.status).to be(403)
|
|
135
|
+
expect(last_response.body).to include('Use deploy pipeline instead.')
|
|
136
|
+
end
|
|
137
|
+
end
|
|
67
138
|
end
|
|
68
139
|
end
|
|
@@ -125,6 +125,32 @@ RSpec.describe Flipper::UI::Actions::Feature do
|
|
|
125
125
|
end
|
|
126
126
|
end
|
|
127
127
|
|
|
128
|
+
context "when disable_fully_enable is true" do
|
|
129
|
+
before { Flipper::UI.configuration.disable_fully_enable = true }
|
|
130
|
+
after { Flipper::UI.configuration.disable_fully_enable = nil }
|
|
131
|
+
|
|
132
|
+
it 'renders the Fully Enable button as disabled' do
|
|
133
|
+
get '/features/search'
|
|
134
|
+
expect(last_response.body).to include('Fully Enable')
|
|
135
|
+
expect(last_response.body).to match(/disabled\s*>/)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
it 'shows the default disabled tooltip' do
|
|
139
|
+
get '/features/search'
|
|
140
|
+
expect(last_response.body).to include('Fully enabling features via the UI is disabled.')
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
context "when disable_fully_enable is a custom message" do
|
|
145
|
+
before { Flipper::UI.configuration.disable_fully_enable = "Use deploy pipeline instead." }
|
|
146
|
+
after { Flipper::UI.configuration.disable_fully_enable = nil }
|
|
147
|
+
|
|
148
|
+
it 'shows custom disabled tooltip' do
|
|
149
|
+
get '/features/search'
|
|
150
|
+
expect(last_response.body).to include('Use deploy pipeline instead.')
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
128
154
|
context 'custom actor names' do
|
|
129
155
|
before do
|
|
130
156
|
actor = Flipper::Actor.new('some_actor_name')
|
|
@@ -11,11 +11,12 @@ RSpec.describe Flipper::UI::Actions::Import do
|
|
|
11
11
|
{ :csrf => token, 'csrf' => token, '_csrf_token' => token }
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
+
let(:path) { FlipperRoot.join("spec", "fixtures", "flipper_pstore_1679087600.json") }
|
|
15
|
+
|
|
14
16
|
describe "POST /settings/import" do
|
|
15
17
|
before do
|
|
16
18
|
flipper.enable :plausible
|
|
17
19
|
flipper.disable :google_analytics
|
|
18
|
-
path = FlipperRoot.join("spec", "fixtures", "flipper_pstore_1679087600.json")
|
|
19
20
|
|
|
20
21
|
post '/settings/import',
|
|
21
22
|
{
|
|
@@ -37,5 +38,26 @@ RSpec.describe Flipper::UI::Actions::Import do
|
|
|
37
38
|
expect(last_response.status).to be(302)
|
|
38
39
|
expect(last_response.headers['location']).to eq('/features')
|
|
39
40
|
end
|
|
41
|
+
|
|
42
|
+
context "when in read only mode" do
|
|
43
|
+
before do
|
|
44
|
+
allow(flipper).to receive(:read_only?) { true }
|
|
45
|
+
|
|
46
|
+
post '/settings/import',
|
|
47
|
+
{
|
|
48
|
+
'authenticity_token' => token,
|
|
49
|
+
'file' => Rack::Test::UploadedFile.new(path, "application/json"),
|
|
50
|
+
},
|
|
51
|
+
'rack.session' => session
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'returns 403' do
|
|
55
|
+
expect(last_response.status).to be(403)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'renders read only template' do
|
|
59
|
+
expect(last_response.body).to include('read only')
|
|
60
|
+
end
|
|
61
|
+
end
|
|
40
62
|
end
|
|
41
63
|
end
|
|
@@ -73,6 +73,17 @@ RSpec.describe Flipper::UI::Configuration do
|
|
|
73
73
|
end
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
+
describe "#version_check_enabled" do
|
|
77
|
+
it "has default value" do
|
|
78
|
+
expect(configuration.version_check_enabled).to eq(true)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it "can be updated" do
|
|
82
|
+
configuration.version_check_enabled = false
|
|
83
|
+
expect(configuration.version_check_enabled).to eq(false)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
76
87
|
describe "#feature_removal_enabled" do
|
|
77
88
|
it "has default value" do
|
|
78
89
|
expect(configuration.feature_removal_enabled).to eq(true)
|
|
@@ -146,6 +157,39 @@ RSpec.describe Flipper::UI::Configuration do
|
|
|
146
157
|
end
|
|
147
158
|
end
|
|
148
159
|
|
|
160
|
+
describe "#disable_fully_enable" do
|
|
161
|
+
it "defaults to nil" do
|
|
162
|
+
expect(configuration.disable_fully_enable).to be_nil
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
it "can be set to true" do
|
|
166
|
+
configuration.disable_fully_enable = true
|
|
167
|
+
expect(configuration.disable_fully_enable).to eq(true)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
it "can be set to false" do
|
|
171
|
+
configuration.disable_fully_enable = false
|
|
172
|
+
expect(configuration.disable_fully_enable).to eq(false)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
it "can be set to a custom message" do
|
|
176
|
+
configuration.disable_fully_enable = "Use deploy pipeline instead."
|
|
177
|
+
expect(configuration.disable_fully_enable).to eq("Use deploy pipeline instead.")
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
describe "#disable_fully_enable_message" do
|
|
182
|
+
it "returns default message when set to true" do
|
|
183
|
+
configuration.disable_fully_enable = true
|
|
184
|
+
expect(configuration.disable_fully_enable_message).to eq("Fully enabling features via the UI is disabled.")
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
it "returns custom message when set to a string" do
|
|
188
|
+
configuration.disable_fully_enable = "Use deploy pipeline instead."
|
|
189
|
+
expect(configuration.disable_fully_enable_message).to eq("Use deploy pipeline instead.")
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
149
193
|
describe "#show_feature_description_in_list" do
|
|
150
194
|
it "has default value" do
|
|
151
195
|
expect(configuration.show_feature_description_in_list).to eq(false)
|
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: 1.4.
|
|
4
|
+
version: 1.4.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- John Nunemaker
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-05-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rack
|
|
@@ -76,14 +76,14 @@ dependencies:
|
|
|
76
76
|
requirements:
|
|
77
77
|
- - "~>"
|
|
78
78
|
- !ruby/object:Gem::Version
|
|
79
|
-
version: 1.4.
|
|
79
|
+
version: 1.4.2
|
|
80
80
|
type: :runtime
|
|
81
81
|
prerelease: false
|
|
82
82
|
version_requirements: !ruby/object:Gem::Requirement
|
|
83
83
|
requirements:
|
|
84
84
|
- - "~>"
|
|
85
85
|
- !ruby/object:Gem::Version
|
|
86
|
-
version: 1.4.
|
|
86
|
+
version: 1.4.2
|
|
87
87
|
- !ruby/object:Gem::Dependency
|
|
88
88
|
name: erubi
|
|
89
89
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -166,6 +166,7 @@ files:
|
|
|
166
166
|
- lib/flipper/ui/views/add_actor.erb
|
|
167
167
|
- lib/flipper/ui/views/add_feature.erb
|
|
168
168
|
- lib/flipper/ui/views/add_group.erb
|
|
169
|
+
- lib/flipper/ui/views/disable_fully_enable.erb
|
|
169
170
|
- lib/flipper/ui/views/feature.erb
|
|
170
171
|
- lib/flipper/ui/views/feature_creation_disabled.erb
|
|
171
172
|
- lib/flipper/ui/views/feature_removal_disabled.erb
|
|
@@ -202,7 +203,7 @@ metadata:
|
|
|
202
203
|
homepage_uri: https://www.flippercloud.io
|
|
203
204
|
source_code_uri: https://github.com/flippercloud/flipper
|
|
204
205
|
bug_tracker_uri: https://github.com/flippercloud/flipper/issues
|
|
205
|
-
changelog_uri: https://github.com/flippercloud/flipper/releases/tag/v1.4.
|
|
206
|
+
changelog_uri: https://github.com/flippercloud/flipper/releases/tag/v1.4.2
|
|
206
207
|
funding_uri: https://github.com/sponsors/flippercloud
|
|
207
208
|
post_install_message:
|
|
208
209
|
rdoc_options: []
|