Skip to content

Commit 04215f7

Browse files
authored
Merge pull request #979 from flippercloud/worktree-wild-watching-cloud
Add config to disable the Fully Enable button
2 parents ca399d9 + 640f32f commit 04215f7

8 files changed

Lines changed: 180 additions & 9 deletions

File tree

CLAUDE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,5 @@ For outbound HTTP requests, use `Flipper::Adapters::Http::Client` instead of raw
8585
### Testing
8686

8787
Uses both RSpec (currently preferred for new tests) and Minitest. Shared adapter specs ensure consistency across all storage backends. Extensive testing across multiple Rails versions (5.0-8.0).
88+
89+
`Flipper.configuration` is reset to nil before each spec (in `spec/spec_helper.rb`), but `Flipper::UI.configuration` is **not** globally reset. When modifying UI config in tests, set the value in `before` and reset it in `after` to match the existing pattern throughout the spec suite.

lib/flipper/ui/actions/boolean_gate.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ def post
1616
@feature = Decorators::Feature.new(feature)
1717

1818
if params['action'] == 'Enable'
19+
if Flipper::UI.configuration.disable_fully_enable
20+
status 403
21+
halt view_response(:disable_fully_enable)
22+
end
1923
feature.enable
2024
else
2125
feature.disable

lib/flipper/ui/configuration.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,20 @@ class Configuration
7777
# Default is false.
7878
attr_accessor :confirm_disable
7979

80+
# Public: Set to disable the Fully Enable button in the UI, preventing
81+
# users from fully enabling features via the web interface. Set to true
82+
# for a default message, or a string for a custom message. Defaults to nil.
83+
#
84+
# Note: This only affects the UI. If flipper-api is mounted, full enable
85+
# is still possible via the API.
86+
#
87+
# Examples:
88+
#
89+
# config.disable_fully_enable = true
90+
# config.disable_fully_enable = "Use deploy pipeline instead."
91+
#
92+
attr_accessor :disable_fully_enable
93+
8094
VALID_BANNER_CLASS_VALUES = %w(
8195
danger
8296
dark
@@ -90,6 +104,7 @@ class Configuration
90104

91105
DEFAULT_DESCRIPTIONS_SOURCE = ->(_keys) { {} }
92106
DEFAULT_ACTOR_NAMES_SOURCE = ->(_keys) { {} }
107+
DEFAULT_DISABLE_FULLY_ENABLE_MESSAGE = "Fully enabling features via the UI is disabled."
93108

94109
def initialize
95110
@delete = Option.new("Danger Zone", "Deleting a feature removes it from the list of features and disables it for everyone.")
@@ -106,6 +121,7 @@ def initialize
106121
@actors_separator = ','
107122
@confirm_fully_enable = false
108123
@confirm_disable = true
124+
@disable_fully_enable = nil
109125
@read_only = false
110126
@nav_items = [
111127
{ title: "Features", href: "features" },
@@ -121,6 +137,14 @@ def show_feature_description_in_list?
121137
using_descriptions? && @show_feature_description_in_list
122138
end
123139

140+
def disable_fully_enable_message
141+
if @disable_fully_enable.is_a?(String)
142+
@disable_fully_enable
143+
else
144+
DEFAULT_DISABLE_FULLY_ENABLE_MESSAGE
145+
end
146+
end
147+
124148
def banner_class=(value)
125149
unless VALID_BANNER_CLASS_VALUES.include?(value)
126150
raise InvalidConfigurationValue, "The banner_class provided '#{value}' is " \
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<div class="alert alert-danger">
2+
<%= Flipper::UI.configuration.disable_fully_enable_message %>
3+
</div>

lib/flipper/ui/views/feature.erb

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -255,16 +255,24 @@
255255
<div class="row">
256256
<% unless @feature.boolean_value %>
257257
<div class="col d-grid">
258-
<button type="submit" name="action" value="Enable" class="btn btn-outline-success"
259-
<% if Flipper::UI.configuration.confirm_fully_enable %>
260-
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 %>"
261-
data-confirmation-text="<%= feature_name %>"
262-
<% end %>
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>
266263
</span>
267-
</button>
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 %>
268276
</div>
269277
<% end %>
270278

spec/flipper/ui/actions/boolean_gate_spec.rb

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,5 +64,76 @@
6464
expect(last_response.headers['location']).to eq('/features/search')
6565
end
6666
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
67138
end
68139
end

spec/flipper/ui/actions/feature_spec.rb

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,32 @@
125125
end
126126
end
127127

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+
128154
context 'custom actor names' do
129155
before do
130156
actor = Flipper::Actor.new('some_actor_name')

spec/flipper/ui/configuration_spec.rb

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,39 @@
146146
end
147147
end
148148

149+
describe "#disable_fully_enable" do
150+
it "defaults to nil" do
151+
expect(configuration.disable_fully_enable).to be_nil
152+
end
153+
154+
it "can be set to true" do
155+
configuration.disable_fully_enable = true
156+
expect(configuration.disable_fully_enable).to eq(true)
157+
end
158+
159+
it "can be set to false" do
160+
configuration.disable_fully_enable = false
161+
expect(configuration.disable_fully_enable).to eq(false)
162+
end
163+
164+
it "can be set to a custom message" do
165+
configuration.disable_fully_enable = "Use deploy pipeline instead."
166+
expect(configuration.disable_fully_enable).to eq("Use deploy pipeline instead.")
167+
end
168+
end
169+
170+
describe "#disable_fully_enable_message" do
171+
it "returns default message when set to true" do
172+
configuration.disable_fully_enable = true
173+
expect(configuration.disable_fully_enable_message).to eq("Fully enabling features via the UI is disabled.")
174+
end
175+
176+
it "returns custom message when set to a string" do
177+
configuration.disable_fully_enable = "Use deploy pipeline instead."
178+
expect(configuration.disable_fully_enable_message).to eq("Use deploy pipeline instead.")
179+
end
180+
end
181+
149182
describe "#show_feature_description_in_list" do
150183
it "has default value" do
151184
expect(configuration.show_feature_description_in_list).to eq(false)

0 commit comments

Comments
 (0)