Skip to content

Commit 3be8f72

Browse files
authored
Merge pull request #772 from radar/hide-ui-elements-in-read-only-mode
Hide UI elements when Flipper is in read-only mode
2 parents 1bb1040 + fa5ec07 commit 3be8f72

21 files changed

+150
-62
lines changed

lib/flipper/adapter.rb

+4
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ def from(source)
2424
end
2525
end
2626

27+
def read_only?
28+
false
29+
end
30+
2731
# Public: Get all features and gate values in one call. Defaults to one call
2832
# to features and another to get_multi. Feel free to override per adapter to
2933
# make this more efficient.

lib/flipper/adapters/memoizable.rb

+5
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,11 @@ def disable(feature, gate, thing)
117117
@adapter.disable(feature, gate, thing).tap { expire_feature(feature) }
118118
end
119119

120+
# Public
121+
def read_only?
122+
@adapter.read_only?
123+
end
124+
120125
def import(source)
121126
@adapter.import(source).tap { cache.clear if memoizing? }
122127
end

lib/flipper/adapters/read_only.rb

+4
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ def features
2121
@adapter.features
2222
end
2323

24+
def read_only?
25+
true
26+
end
27+
2428
def get(feature)
2529
@adapter.get(feature)
2630
end

lib/flipper/dsl.rb

+4
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,10 @@ def features
272272
adapter.features.map { |name| feature(name) }.to_set
273273
end
274274

275+
def read_only?
276+
adapter.read_only?
277+
end
278+
275279
# Cloud DSL method that does nothing for open source version.
276280
def sync
277281
end

lib/flipper/ui/action.rb

+9-1
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ def valid_request_method?
276276

277277
# Internal: Method to call when the UI is in read only mode and you want
278278
# to inform people of that fact.
279-
def read_only
279+
def render_read_only
280280
status 403
281281

282282
breadcrumb 'Home', '/'
@@ -286,6 +286,14 @@ def read_only
286286
halt view_response(:read_only)
287287
end
288288

289+
def read_only?
290+
Flipper::UI.configuration.read_only || flipper.read_only?
291+
end
292+
293+
def write_allowed?
294+
!read_only?
295+
end
296+
289297
def bootstrap_css
290298
SOURCES[:bootstrap_css]
291299
end

lib/flipper/ui/actions/actors_gate.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def get
2323
end
2424

2525
def post
26-
read_only if Flipper::UI.configuration.read_only
26+
render_read_only if read_only?
2727

2828
feature = flipper[feature_name]
2929
value = params['value'].to_s.strip

lib/flipper/ui/actions/add_feature.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ class AddFeature < UI::Action
88
route %r{\A/features/new/?\Z}
99

1010
def get
11-
read_only if Flipper::UI.configuration.read_only
11+
render_read_only if read_only?
1212

1313
unless Flipper::UI.configuration.feature_creation_enabled
1414
status 403

lib/flipper/ui/actions/boolean_gate.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class BooleanGate < UI::Action
1010
route %r{\A/features/(?<feature_name>.*)/boolean/?\Z}
1111

1212
def post
13-
read_only if Flipper::UI.configuration.read_only
13+
render_read_only if read_only?
1414

1515
feature = flipper[feature_name]
1616
@feature = Decorators::Feature.new(feature)

lib/flipper/ui/actions/feature.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def get
2626
end
2727

2828
def delete
29-
read_only if Flipper::UI.configuration.read_only
29+
render_read_only if read_only?
3030

3131
unless Flipper::UI.configuration.feature_removal_enabled
3232
status 403

lib/flipper/ui/actions/features.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def get
3636
end
3737

3838
def post
39-
read_only if Flipper::UI.configuration.read_only
39+
render_read_only if read_only?
4040

4141
unless Flipper::UI.configuration.feature_creation_enabled
4242
status 403

lib/flipper/ui/actions/groups_gate.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def get
2222
end
2323

2424
def post
25-
read_only if Flipper::UI.configuration.read_only
25+
render_read_only if read_only?
2626

2727
feature = flipper[feature_name]
2828
value = params['value'].to_s.strip

lib/flipper/ui/actions/percentage_of_actors_gate.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class PercentageOfActorsGate < UI::Action
1010
route %r{\A/features/(?<feature_name>.*)/percentage_of_actors/?\Z}
1111

1212
def post
13-
read_only if Flipper::UI.configuration.read_only
13+
render_read_only if read_only?
1414

1515
feature = flipper[feature_name]
1616
@feature = Decorators::Feature.new(feature)

lib/flipper/ui/actions/percentage_of_time_gate.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class PercentageOfTimeGate < UI::Action
1010
route %r{\A/features/(?<feature_name>.*)/percentage_of_time/?\Z}
1111

1212
def post
13-
read_only if Flipper::UI.configuration.read_only
13+
render_read_only if read_only?
1414

1515
feature = flipper[feature_name]
1616
@feature = Decorators::Feature.new(feature)

lib/flipper/ui/views/feature.erb

+41-48
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
</strong>
4141
</h6>
4242
</div>
43-
<% unless Flipper::UI.configuration.read_only %>
43+
<% if write_allowed? %>
4444
<div class="col col-auto toggle-block-when-off">
4545
<button class="btn btn-outline-secondary js-toggle-trigger" data-toggle="collapse" data-target="#add-actor">Add an actor</button>
4646
</div>
@@ -73,7 +73,7 @@
7373
</h6>
7474
</div>
7575
<div class="col col-auto">
76-
<% unless Flipper::UI.configuration.read_only %>
76+
<% if write_allowed? %>
7777
<form action="<%= script_name %>/features/<%= @feature.key %>/actors" method="post">
7878
<%== csrf_input_tag %>
7979
<input type="hidden" name="operation" value="disable">
@@ -102,7 +102,7 @@
102102
</strong>
103103
</h6>
104104
</div>
105-
<% unless Flipper::UI.configuration.read_only %>
105+
<% if write_allowed? %>
106106
<div class="col col-auto toggle-block-when-off">
107107
<button class="btn btn-outline-secondary js-toggle-trigger" data-toggle="collapse" data-target="#add-actor">Add a group</button>
108108
</div>
@@ -138,7 +138,7 @@
138138
<h6 class="m-0"><%= item %></h6>
139139
</div>
140140
<div class="col col-auto">
141-
<% unless Flipper::UI.configuration.read_only %>
141+
<% if write_allowed? %>
142142
<form action="<%= script_name %>/features/<%= @feature.key %>/groups" method="post">
143143
<%== csrf_input_tag %>
144144
<input type="hidden" name="operation" value="disable">
@@ -160,7 +160,7 @@
160160
<div class="col col-mr-auto">
161161
<h6 class="m-0"><strong class="<%= "text-muted" unless @feature.percentage_of_actors_value > 0 %>">Enabled for <%= @feature.percentage_of_actors_value %>% of actors</strong></h6>
162162
</div>
163-
<% unless Flipper::UI.configuration.read_only %>
163+
<% if write_allowed? %>
164164
<div class="col col-auto toggle-block-when-off">
165165
<button class="btn btn-outline-secondary js-toggle-trigger">Edit</button>
166166
</div>
@@ -171,7 +171,7 @@
171171
</div>
172172
</div>
173173

174-
<% unless Flipper::UI.configuration.read_only %>
174+
<% if write_allowed? %>
175175
<div class="card-body border-bottom toggle-block-when-on bg-lightest">
176176
<div class="row">
177177
<div class="col-12 col-md-6 mb-3 mb-md-0">
@@ -203,7 +203,7 @@
203203
<div class="col col-mr-auto">
204204
<h6 class="m-0"><strong class="<%= "text-muted" unless @feature.percentage_of_time_value > 0 %>">Enabled for <%= @feature.percentage_of_time_value %>% of time</strong></h6>
205205
</div>
206-
<% unless Flipper::UI.configuration.read_only %>
206+
<% if write_allowed? %>
207207
<div class="col col-auto toggle-block-when-off">
208208
<button class="btn btn-outline-secondary js-toggle-trigger">Edit</button>
209209
</div>
@@ -214,7 +214,7 @@
214214
</div>
215215
</div>
216216

217-
<% unless Flipper::UI.configuration.read_only %>
217+
<% if write_allowed? %>
218218
<div class="card-body border-bottom toggle-block-when-on bg-lightest">
219219
<div class="row">
220220
<div class="col-12 col-md-6 mb-3 mb-md-0">
@@ -240,52 +240,45 @@
240240
</div>
241241
<% end %>
242242

243-
<div class="card-body">
244-
<form action="<%= script_name %>/features/<%= @feature.key %>/boolean" method="post">
245-
<%== csrf_input_tag %>
243+
<% if write_allowed? %>
244+
<div class="card-body">
245+
<form action="<%= script_name %>/features/<%= @feature.key %>/boolean" method="post">
246+
<%== csrf_input_tag %>
246247

247-
<div class="row">
248-
<% unless @feature.boolean_value %>
249-
<div class="col">
250-
<button type="submit" name="action" value="Enable" <% if Flipper::UI.configuration.confirm_fully_enable %>id="enable_feature__button"<% end %> class="btn btn-outline-success btn-block" <% if Flipper::UI.configuration.read_only %>disabled<% end %>>
251-
<span class="d-block" data-toggle="tooltip"
252-
<% if Flipper::UI.configuration.confirm_fully_enable %>
253-
data-confirmation-text="<%= feature_name %>"
254-
<% end %>
255-
<% if Flipper::UI.configuration.read_only %>
256-
title="Fully enable is not allowed in read only mode."
257-
<% else %>
258-
title="Enable for everyone"
259-
<% end %>
260-
>
261-
Fully Enable
262-
</span>
263-
</button>
264-
</div>
265-
<% end %>
248+
<div class="row">
249+
<% unless @feature.boolean_value %>
250+
<div class="col">
251+
<button type="submit" name="action" value="Enable" <% if Flipper::UI.configuration.confirm_fully_enable %>id="enable_feature__button"<% end %> class="btn btn-outline-success btn-block">
252+
<span class="d-block" data-toggle="tooltip"
253+
<% if Flipper::UI.configuration.confirm_fully_enable %>
254+
data-confirmation-text="<%= feature_name %>"
255+
<% end %>
256+
title="Enable for everyone"
257+
>
258+
Fully Enable
259+
</span>
260+
</button>
261+
</div>
262+
<% end %>
266263

267-
<% unless @feature.off? %>
268-
<div class="col">
269-
<button type="submit" name="action" value="Disable" class="btn btn-outline-danger btn-block" <% if Flipper::UI.configuration.read_only %>disabled<% end %>>
270-
<span class="d-block" data-toggle="tooltip"
271-
<% if Flipper::UI.configuration.read_only %>
272-
title="Disable is not allowed in read only mode.">
273-
<% else %>
274-
title="Disable for everyone by clearing all percentages, groups and actors.">
275-
<% end %>
276-
Disable
277-
</span>
278-
</button>
279-
</div>
280-
<% end %>
281-
</div>
282-
</form>
283-
</div>
264+
<% unless @feature.off? %>
265+
<div class="col">
266+
<button type="submit" name="action" value="Disable" class="btn btn-outline-danger btn-block">
267+
<span class="d-block" data-toggle="tooltip" title="Disable for everyone by clearing all percentages, groups and actors.">
268+
Disable
269+
</span>
270+
</button>
271+
</div>
272+
<% end %>
273+
</div>
274+
</form>
275+
</div>
276+
<% end %>
284277
</div>
285278
</div>
286279
</div>
287280

288-
<% if !Flipper::UI.configuration.read_only && Flipper::UI.configuration.feature_removal_enabled %>
281+
<% if write_allowed? && Flipper::UI.configuration.feature_removal_enabled %>
289282
<div class="row">
290283
<div class="col">
291284
<div class="card border">

lib/flipper/ui/views/features.erb

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@
33
<% if Flipper::UI.configuration.fun %>
44
<h4>But I've got a blank space baby...</h4>
55
<p>And I'll flip your features.</p>
6-
<%- if !Flipper::UI.configuration.read_only && Flipper::UI.configuration.feature_creation_enabled -%>
6+
<%- if write_allowed? && Flipper::UI.configuration.feature_creation_enabled -%>
77
<p>
88
<a class="btn btn-primary btn-sm" href="<%= script_name %>/features/new">Add Feature</a>
99
</p>
1010
<%- end -%>
1111
<% else %>
1212
<h4>Getting Started</h4>
1313
<p class="mb-1">You have not added any features to configure yet.</p>
14-
<%- if !Flipper::UI.configuration.read_only && Flipper::UI.configuration.feature_creation_enabled -%>
14+
<%- if write_allowed? && Flipper::UI.configuration.feature_creation_enabled -%>
1515
<p class="mt-2">
1616
<a class="btn btn-primary btn-sm" href="<%= script_name %>/features/new">Add Feature</a>
1717
</p>
@@ -26,7 +26,7 @@
2626
<% else %>
2727
<div class="card">
2828
<div class="card-header">
29-
<%- if !Flipper::UI.configuration.read_only && Flipper::UI.configuration.feature_creation_enabled -%>
29+
<%- if write_allowed? && Flipper::UI.configuration.feature_creation_enabled -%>
3030
<div class="float-right">
3131
<a class="btn btn-primary btn-sm" href="<%= script_name %>/features/new">Add Feature</a>
3232
</div>

lib/flipper/ui/views/read_only.erb

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
11
<div class="alert alert-danger">
2-
The UI is currently in read only mode. To change this, you'll need to set <code>Flipper::UI.configuration.read_only = false</code> wherever flipper is running from.
2+
<p>The UI is currently in read only mode.</p>
3+
4+
<p>
5+
To change this, you'll need to set <code>Flipper::UI.configuration.read_only = false</code> wherever flipper is running from.
6+
</p>
7+
8+
<p>
9+
Alternatively, you may be using the <code>Flipper::Adapters::ReadOnly</code> adapter. This will also set Flipper into read only mode.
10+
</p>
311
</div>

spec/flipper/adapters/read_only_spec.rb

+4
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@
7272
expect(subject.features).to eq(Set['stats'])
7373
end
7474

75+
it 'is configured as read only' do
76+
expect(subject.read_only?).to eq(true)
77+
end
78+
7579
it 'raises error on add' do
7680
expect { subject.add(feature) }.to raise_error(Flipper::Adapters::ReadOnly::WriteAttempted)
7781
end

spec/flipper/ui/actions/actors_gate_spec.rb

+17
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,23 @@
129129
end
130130
end
131131

132+
context "when a readonly adapter is configured" do
133+
let(:value) { 'User;6' }
134+
135+
before do
136+
allow(flipper).to receive(:read_only?) { true }
137+
end
138+
139+
it "does not allow an actor to be added" do
140+
post 'features/search/actors',
141+
{ 'value' => value, 'operation' => 'enable', 'authenticity_token' => token },
142+
'rack.session' => session
143+
144+
expect(flipper[:search].actors_value).not_to include('User;6')
145+
expect(last_response.body).to include("The UI is currently in read only mode.")
146+
end
147+
end
148+
132149
context 'disabling an actor' do
133150
let(:value) { 'User;6' }
134151
let(:multi_value) { 'User;5, User;7, User;9, User;12' }

spec/flipper/ui/actions/add_feature_spec.rb

+15
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,19 @@
3939
expect(last_response.body).to include('Feature creation is disabled.')
4040
end
4141
end
42+
43+
describe 'GET /features/new when an adpter is set to readonly' do
44+
before do
45+
allow(flipper).to receive(:read_only?) { true }
46+
get '/features/new'
47+
end
48+
49+
it 'returns 403' do
50+
expect(last_response.status).to be(403)
51+
end
52+
53+
it 'shows that the UI is currently read-only mode' do
54+
expect(last_response.body).to include('The UI is currently in read only mode.')
55+
end
56+
end
4257
end

0 commit comments

Comments
 (0)