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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 00b09ef9134ecfe7b076edae579e9ff91827964ada4053fa8d5b5167b2965992
4
- data.tar.gz: 22b200ae581fd91ced63c0936bfa03ce9ee306779bf09c7ed40809ec97e9d4c6
3
+ metadata.gz: 0cb0f4ab52d69d747f65f3a49aa89656138bb326434e0d4b9e21a6827479fea0
4
+ data.tar.gz: 227e30ad6e4d67f2b69e73a4aed099c8e91fb7651aad1c4a5bb54881ab925f97
5
5
  SHA512:
6
- metadata.gz: d85919950864694f4156846ffd1cf20c758451821409c6ae7fb566b11759740842368af8393669617c5d8c5764ac5a9d11c7f91207641c51892dec0169cd09ad
7
- data.tar.gz: 62b4831c0a38c4321ab627480e79ab5043140d4b57869c80a3792288d9b2d21421133f701ba1a8df72b6d5d707854df65714cce7009cb383aee039c04177718a
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
@@ -8,6 +8,7 @@ module Flipper
8
8
  route %r{\A/settings\/import/?\Z}
9
9
 
10
10
  def post
11
+ render_read_only if read_only?
11
12
  contents = params['file'][:tempfile].read
12
13
  export = Flipper::Exporters::Json::Export.new(contents: contents)
13
14
  flipper.import(export)
@@ -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 " \
@@ -0,0 +1,3 @@
1
+ <div class="alert alert-danger">
2
+ <%= Flipper::UI.configuration.disable_fully_enable_message %>
3
+ </div>
@@ -255,16 +255,24 @@
255
255
  <div class="row">
256
256
  <% unless @feature.boolean_value %>
257
257
  <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>
266
263
  </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 %>
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
- <a href="#" class="badge text-bg-warning ms-2 d-none" style="font-size:100%" id="new-version-badge" data-version="<%= Flipper::VERSION %>">
22
- </a>
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
- <script src="<%= script_name %>/js/version.js?v=<%= Flipper::VERSION %>"></script>
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>
@@ -38,6 +38,7 @@
38
38
  </div>
39
39
  </div>
40
40
 
41
+ <% if write_allowed? %>
41
42
  <div class="card">
42
43
  <div class="card-header">
43
44
  <h4 class="m-0">Import</h4>
@@ -51,3 +52,4 @@
51
52
  </form>
52
53
  </div>
53
54
  </div>
55
+ <% end %>
@@ -1,5 +1,5 @@
1
1
  module Flipper
2
- VERSION = '1.4.0'.freeze
2
+ VERSION = '1.4.2'.freeze
3
3
 
4
4
  REQUIRED_RUBY_VERSION = '2.6'.freeze
5
5
  NEXT_REQUIRED_RUBY_VERSION = '3.0'.freeze
@@ -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.0
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-02-26 00:00:00.000000000 Z
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.0
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.0
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.0
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: []