switchlet 0.1.4 → 0.3.1

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: aef30a43a9f953b56491c74e2569110d8f71ac8ed664643776fa8ae2fb066a8f
4
- data.tar.gz: 394b31eed795afb2cf2d8da5f5db665cbde32dd1b514e31acd2eaf6b82922c5c
3
+ metadata.gz: f5df5ba1da7343518f0a7886f3e8cb26a7b125d217fddae79a43106e58a07ca3
4
+ data.tar.gz: e2005adb28bcd955ea762047e462f6befd04502d99df79fffdb69119d426addf
5
5
  SHA512:
6
- metadata.gz: ea64546f7de6ab36485fb05c2e74f5db764b9b15f6abc58f4280c40d34df1a48d449c8a6570beaa885d8d05068ea93d7304847df7795adaa1106ec62d8435f1d
7
- data.tar.gz: c83defe48ee72b6d2f7f7121032800589c69424cb85f637d61dac1489c9dc85540218190715b951af224cbc471df1724137d73188e918aa473a7a4c78a732db3
6
+ metadata.gz: 3ece132acb395196ea895f118a6899857ffafe3d07930eaaccee2fe0115573d7addb904f2236758b8e1cee91ff2ed51af771b27335061ef4ceee1e9f884b8570
7
+ data.tar.gz: 880b12eb1b49bcb90122be722b5ee9c8a0c359b558af95c961ee58471374fff013965513115fb246abd4eb751453202cbf6dd17c100dacebc60cd77b4c4a0aed
data/README.md CHANGED
@@ -27,14 +27,64 @@ Switchlet.enabled?(:my_feature) # => false
27
27
  # Enable a feature
28
28
  Switchlet.enable!(:my_feature) # => true
29
29
 
30
+ # Enable a feature with description
31
+ Switchlet.enable!(:my_feature) # => true
32
+
30
33
  # Disable a feature
31
34
  Switchlet.disable!(:my_feature) # => false
32
35
 
36
+ # Disable with description
37
+ Switchlet.disable!(:my_feature) # => false
38
+
39
+ # Update description or enabled status
40
+ Switchlet.update!(:my_feature, description: "Updated description") # => Flag object
41
+ Switchlet.update!(:my_feature, enabled: false) # => Flag object
42
+ Switchlet.update!(:my_feature, description: "New description", enabled: true) # => Flag object
43
+
33
44
  # Delete a feature flag
34
45
  Switchlet.delete!(:my_feature) # => nil
35
46
 
36
- # List all feature flags
37
- Switchlet.list # => [{ name: "my_feature", enabled: true, updated_at: Time }]
47
+ # List all feature flags (includes descriptions)
48
+ Switchlet.list # => [{ name: "my_feature", enabled: true, description: "New payment system", updated_at: Time }]
49
+ ```
50
+
51
+ ### Common Usage Patterns
52
+
53
+ Feature flags are often combined with other conditions for more sophisticated control:
54
+
55
+ ```ruby
56
+ # Time-based rollout: Enable feature after specific date
57
+ if Switchlet.enabled?(:my_feature) && Time.current >= Time.zone.parse("2025-06-01 00:00:00")
58
+ # New feature implementation
59
+ render :new_checkout_process
60
+ else
61
+ # Fallback to old implementation
62
+ render :old_checkout_process
63
+ end
64
+
65
+ # User-based gradual rollout: Enable for specific user segments
66
+ if Switchlet.enabled?(:my_feature) && current_user.id % 4 == 0
67
+ # Enable for 25% of users (user IDs divisible by 4)
68
+ show_new_dashboard_ui
69
+ else
70
+ show_classic_dashboard_ui
71
+ end
72
+
73
+ # Role-based access: Enable only for specific user roles
74
+ if Switchlet.enabled?(:my_feature) && current_user.admin?
75
+ # Admin-only feature
76
+ render_admin_analytics_panel
77
+ else
78
+ render_basic_stats
79
+ end
80
+
81
+ # Complex conditions: Combine multiple criteria
82
+ if Switchlet.enabled?(:beta_features) &&
83
+ current_user.beta_tester? &&
84
+ Time.current.hour.between?(9, 17)
85
+ # Beta feature available only during business hours for beta testers
86
+ enable_experimental_features
87
+ end
38
88
  ```
39
89
 
40
90
  ## Web UI
@@ -47,9 +97,10 @@ mount Switchlet::Engine => "/switchlet"
47
97
  ```
48
98
 
49
99
  Then visit `/switchlet` in your browser to:
50
- - View all feature flags
100
+ - View all feature flags with descriptions
51
101
  - Toggle flags ON/OFF
52
- - Create new flags
102
+ - Create new flags with optional descriptions
103
+ - Edit descriptions inline (click to edit)
53
104
  - Delete existing flags
54
105
 
55
106
  ### Securing the Web Interface
@@ -6,29 +6,40 @@ module Switchlet
6
6
  @flags = Switchlet.list
7
7
  end
8
8
 
9
- def toggle
10
- flag_name = params[:name]
11
- current_state = Switchlet.enabled?(flag_name)
12
-
13
- if current_state
14
- Switchlet.disable!(flag_name)
15
- else
16
- Switchlet.enable!(flag_name)
17
- end
18
-
19
- redirect_to switchlet.flags_path, notice: "Flag '#{flag_name}' #{current_state ? 'disabled' : 'enabled'}"
20
- end
21
9
 
22
10
  def create
23
11
  flag_name = params[:flag_name].strip
12
+ description = params[:description]&.strip
24
13
  if flag_name.present?
25
- Switchlet.enable!(flag_name)
14
+ Switchlet.enable!(flag_name, description: description)
26
15
  redirect_to switchlet.flags_path, notice: "Flag '#{flag_name}' created and enabled"
27
16
  else
28
17
  redirect_to switchlet.flags_path, alert: "Flag name cannot be empty"
29
18
  end
30
19
  end
31
20
 
21
+ def update
22
+ flag_name = params[:name]
23
+
24
+ # Handle toggle action
25
+ if params[:action_type] == 'toggle'
26
+ current_state = Switchlet.enabled?(flag_name)
27
+
28
+ if current_state
29
+ Switchlet.disable!(flag_name)
30
+ else
31
+ Switchlet.enable!(flag_name)
32
+ end
33
+
34
+ redirect_to switchlet.flags_path, notice: "Flag '#{flag_name}' #{current_state ? 'disabled' : 'enabled'}"
35
+ # Handle description update
36
+ else
37
+ description = params[:description]
38
+ Switchlet.update!(flag_name, description: description)
39
+ redirect_to switchlet.flags_path, notice: "Description updated for '#{flag_name}'"
40
+ end
41
+ end
42
+
32
43
  def destroy
33
44
  flag_name = params[:name]
34
45
  Switchlet.delete!(flag_name)
@@ -6,135 +6,215 @@
6
6
  <%= csrf_meta_tags %>
7
7
  <style>
8
8
  body {
9
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
9
+ font-family: system-ui, -apple-system, sans-serif;
10
10
  margin: 0;
11
- padding: 20px;
12
- background-color: #f8f9fa;
11
+ padding: 16px;
12
+ background: #fff;
13
13
  color: #333;
14
+ line-height: 1.4;
14
15
  }
15
16
  .container {
16
- max-width: 800px;
17
+ max-width: 1000px;
17
18
  margin: 0 auto;
18
- background: white;
19
- padding: 30px;
20
- border-radius: 8px;
21
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
22
19
  }
23
20
  h1 {
24
- color: #2c3e50;
25
- margin-bottom: 30px;
26
- border-bottom: 2px solid #3498db;
27
- padding-bottom: 10px;
21
+ font-size: 24px;
22
+ font-weight: 600;
23
+ margin: 0 0 24px 0;
24
+ color: #000;
28
25
  }
29
26
  .alert {
30
- padding: 12px 16px;
31
- border-radius: 4px;
32
- margin-bottom: 20px;
27
+ padding: 8px 12px;
28
+ margin-bottom: 16px;
29
+ border-radius: 3px;
30
+ font-size: 14px;
33
31
  }
34
32
  .alert-success {
35
- background-color: #d4edda;
36
- color: #155724;
37
- border: 1px solid #c3e6cb;
33
+ background: #d4f7d4;
34
+ color: #0d5016;
38
35
  }
39
36
  .alert-danger {
40
- background-color: #f8d7da;
37
+ background: #ffd6d6;
41
38
  color: #721c24;
42
- border: 1px solid #f5c6cb;
43
39
  }
44
40
  .create-form {
45
- background: #f8f9fa;
46
- padding: 20px;
47
- border-radius: 4px;
48
- margin-bottom: 30px;
41
+ margin-bottom: 24px;
42
+ padding: 16px;
43
+ border: 1px solid #ddd;
44
+ border-radius: 3px;
49
45
  }
50
46
  .create-form h2 {
51
- margin-top: 0;
52
- color: #2c3e50;
47
+ font-size: 16px;
48
+ font-weight: 600;
49
+ margin: 0 0 12px 0;
53
50
  }
54
51
  .form-group {
55
52
  display: flex;
56
- gap: 10px;
53
+ flex-direction: column;
54
+ gap: 8px;
55
+ }
56
+ .form-row {
57
+ display: flex;
58
+ gap: 8px;
57
59
  align-items: center;
58
60
  }
59
- input[type="text"] {
61
+ .flag-name-input {
62
+ flex: 0 0 200px;
63
+ }
64
+ .description-input-inline {
60
65
  flex: 1;
61
- padding: 8px 12px;
62
- border: 1px solid #ddd;
63
- border-radius: 4px;
66
+ }
67
+ input, textarea {
68
+ padding: 8px;
69
+ border: 1px solid #ccc;
70
+ border-radius: 3px;
64
71
  font-size: 14px;
65
72
  }
73
+ textarea {
74
+ resize: vertical;
75
+ }
66
76
  .btn {
67
- padding: 8px 16px;
68
- border: none;
69
- border-radius: 4px;
77
+ padding: 8px 12px;
78
+ border: 1px solid #ccc;
79
+ border-radius: 3px;
80
+ background: #f8f8f8;
70
81
  cursor: pointer;
71
- text-decoration: none;
72
82
  font-size: 14px;
83
+ text-decoration: none;
73
84
  display: inline-block;
74
85
  }
86
+ .btn:hover {
87
+ background: #e8e8e8;
88
+ }
75
89
  .btn-primary {
76
- background-color: #3498db;
77
- color: white;
90
+ background: #0969da;
91
+ color: #fff;
92
+ border-color: #0969da;
78
93
  }
79
94
  .btn-primary:hover {
80
- background-color: #2980b9;
95
+ background: #0860ca;
81
96
  }
82
97
  .btn-success {
83
- background-color: #27ae60;
84
- color: white;
98
+ background: #1a7f37;
99
+ color: #fff;
100
+ border-color: #1a7f37;
85
101
  }
86
102
  .btn-success:hover {
87
- background-color: #219a52;
103
+ background: #116329;
88
104
  }
89
105
  .btn-danger {
90
- background-color: #e74c3c;
91
- color: white;
106
+ background: #d1242f;
107
+ color: #fff;
108
+ border-color: #d1242f;
92
109
  }
93
110
  .btn-danger:hover {
94
- background-color: #c0392b;
111
+ background: #a40e26;
95
112
  }
96
- .btn-secondary {
97
- background-color: #95a5a6;
98
- color: white;
113
+ .btn-sm {
114
+ padding: 4px 8px;
115
+ font-size: 12px;
99
116
  }
100
- .btn-secondary:hover {
101
- background-color: #7f8c8d;
102
- }
103
- .flags-table {
117
+ table {
104
118
  width: 100%;
105
119
  border-collapse: collapse;
106
- margin-top: 20px;
120
+ border: 1px solid #ddd;
107
121
  }
108
- .flags-table th,
109
- .flags-table td {
110
- padding: 12px;
122
+ th, td {
123
+ padding: 8px 12px;
111
124
  text-align: left;
112
125
  border-bottom: 1px solid #ddd;
113
126
  }
114
- .flags-table th {
115
- background-color: #f8f9fa;
127
+ th {
128
+ background: #f6f8fa;
116
129
  font-weight: 600;
117
- color: #2c3e50;
130
+ font-size: 14px;
118
131
  }
119
132
  .status-enabled {
120
- color: #27ae60;
121
- font-weight: bold;
133
+ color: #1a7f37;
134
+ font-weight: 600;
122
135
  }
123
136
  .status-disabled {
124
- color: #e74c3c;
125
- font-weight: bold;
137
+ color: #d1242f;
138
+ font-weight: 600;
126
139
  }
127
140
  .actions {
128
141
  display: flex;
129
- gap: 8px;
142
+ gap: 4px;
143
+ align-items: center;
130
144
  }
131
145
  .actions form {
132
146
  margin: 0;
133
147
  }
148
+ .toggle-form {
149
+ display: inline-block;
150
+ }
151
+ .toggle-switch {
152
+ position: relative;
153
+ display: inline-block;
154
+ width: 44px;
155
+ height: 24px;
156
+ margin-right: 8px;
157
+ }
158
+ .toggle-switch input {
159
+ opacity: 0;
160
+ width: 0;
161
+ height: 0;
162
+ }
163
+ .toggle-slider {
164
+ position: absolute;
165
+ cursor: pointer;
166
+ top: 0;
167
+ left: 0;
168
+ right: 0;
169
+ bottom: 0;
170
+ background-color: #ccc;
171
+ transition: .3s;
172
+ border-radius: 24px;
173
+ }
174
+ .toggle-slider:before {
175
+ position: absolute;
176
+ content: "";
177
+ height: 18px;
178
+ width: 18px;
179
+ left: 3px;
180
+ bottom: 3px;
181
+ background-color: white;
182
+ transition: .3s;
183
+ border-radius: 50%;
184
+ }
185
+ .toggle-switch input:checked + .toggle-slider {
186
+ background-color: #1a7f37;
187
+ }
188
+ .toggle-switch input:checked + .toggle-slider:before {
189
+ transform: translateX(20px);
190
+ }
191
+ .toggle-switch input:focus + .toggle-slider {
192
+ box-shadow: 0 0 0 2px rgba(26, 127, 55, 0.2);
193
+ }
134
194
  .empty-state {
135
195
  text-align: center;
136
- padding: 40px;
137
- color: #7f8c8d;
196
+ padding: 48px 16px;
197
+ color: #656d76;
198
+ }
199
+ .text-muted {
200
+ color: #656d76;
201
+ }
202
+ .description-text {
203
+ cursor: pointer;
204
+ padding: 4px;
205
+ border-radius: 3px;
206
+ min-height: 16px;
207
+ }
208
+ .description-text:hover {
209
+ background: #f6f8fa;
210
+ }
211
+ .description-input {
212
+ width: 100%;
213
+ margin-bottom: 8px;
214
+ }
215
+ .description-actions {
216
+ display: flex;
217
+ gap: 4px;
138
218
  }
139
219
  </style>
140
220
  </head>
@@ -153,8 +233,9 @@
153
233
  <div class="create-form">
154
234
  <h2>Create New Flag</h2>
155
235
  <%= form_with url: switchlet.flags_path, local: true do |form| %>
156
- <div class="form-group">
157
- <%= form.text_field :flag_name, placeholder: "Enter flag name...", required: true %>
236
+ <div class="form-row">
237
+ <%= form.text_field :flag_name, placeholder: "Flag name", required: true, class: "flag-name-input" %>
238
+ <%= form.text_field :description, placeholder: "Optional description", class: "description-input-inline" %>
158
239
  <%= form.submit "Create Flag", class: "btn btn-primary" %>
159
240
  </div>
160
241
  <% end %>
@@ -170,6 +251,7 @@
170
251
  <thead>
171
252
  <tr>
172
253
  <th>Flag Name</th>
254
+ <th>Description</th>
173
255
  <th>Status</th>
174
256
  <th>Last Updated</th>
175
257
  <th>Actions</th>
@@ -179,6 +261,30 @@
179
261
  <% @flags.each do |flag| %>
180
262
  <tr>
181
263
  <td><strong><%= flag[:name] %></strong></td>
264
+ <td>
265
+ <div class="description-cell" data-flag="<%= flag[:name] %>">
266
+ <span class="description-text" onclick="editDescription('<%= flag[:name] %>')">
267
+ <% if flag[:description].present? %>
268
+ <%= flag[:description] %>
269
+ <% else %>
270
+ <span class="text-muted">Click to add description</span>
271
+ <% end %>
272
+ </span>
273
+ <div class="description-edit" style="display: none;">
274
+ <%= form_with url: switchlet.flag_path(flag[:name]), local: true, class: "description-form" do |form| %>
275
+ <%= hidden_field_tag :authenticity_token, form_authenticity_token %>
276
+ <%= hidden_field_tag :_method, 'patch' %>
277
+ <%= form.text_area :description, value: flag[:description], rows: 2,
278
+ class: "description-input",
279
+ placeholder: "Enter description..." %>
280
+ <div class="description-actions">
281
+ <%= form.submit "Save", class: "btn btn-success btn-sm" %>
282
+ <button type="button" class="btn btn-secondary btn-sm" onclick="cancelEdit('<%= flag[:name] %>')">Cancel</button>
283
+ </div>
284
+ <% end %>
285
+ </div>
286
+ </div>
287
+ </td>
182
288
  <td>
183
289
  <span class="<%= flag[:enabled] ? 'status-enabled' : 'status-disabled' %>">
184
290
  <%= flag[:enabled] ? 'ENABLED' : 'DISABLED' %>
@@ -187,14 +293,19 @@
187
293
  <td><%= flag[:updated_at].strftime("%Y-%m-%d %H:%M") %></td>
188
294
  <td>
189
295
  <div class="actions">
190
- <%= button_to switchlet.toggle_flag_path(flag[:name]),
191
- method: :patch,
192
- class: "btn #{flag[:enabled] ? 'btn-secondary' : 'btn-success'}" do %>
193
- <%= flag[:enabled] ? 'Disable' : 'Enable' %>
296
+ <%= form_with url: switchlet.flag_path(flag[:name]), method: :patch, local: true, class: "toggle-form" do |form| %>
297
+ <%= hidden_field_tag :authenticity_token, form_authenticity_token %>
298
+ <%= hidden_field_tag :action_type, 'toggle' %>
299
+ <label class="toggle-switch">
300
+ <%= check_box_tag "enabled", "1", flag[:enabled],
301
+ onchange: "this.form.submit()",
302
+ class: "toggle-input" %>
303
+ <span class="toggle-slider"></span>
304
+ </label>
194
305
  <% end %>
195
306
  <%= button_to switchlet.flag_path(flag[:name]),
196
307
  method: :delete,
197
- class: "btn btn-danger",
308
+ class: "btn btn-danger btn-sm",
198
309
  confirm: "Are you sure you want to delete '#{flag[:name]}'?" do %>
199
310
  Delete
200
311
  <% end %>
@@ -206,5 +317,39 @@
206
317
  </table>
207
318
  <% end %>
208
319
  </div>
320
+
321
+ <script>
322
+ function editDescription(flagName) {
323
+ const cell = document.querySelector(`[data-flag="${flagName}"]`);
324
+ const textSpan = cell.querySelector('.description-text');
325
+ const editDiv = cell.querySelector('.description-edit');
326
+ const input = cell.querySelector('.description-input');
327
+
328
+ textSpan.style.display = 'none';
329
+ editDiv.style.display = 'block';
330
+ input.focus();
331
+ input.select();
332
+ }
333
+
334
+ function cancelEdit(flagName) {
335
+ const cell = document.querySelector(`[data-flag="${flagName}"]`);
336
+ const textSpan = cell.querySelector('.description-text');
337
+ const editDiv = cell.querySelector('.description-edit');
338
+
339
+ textSpan.style.display = 'block';
340
+ editDiv.style.display = 'none';
341
+ }
342
+
343
+ // Escape key to cancel editing
344
+ document.addEventListener('keydown', function(e) {
345
+ if (e.key === 'Escape') {
346
+ const activeEdit = document.querySelector('.description-edit[style*="block"]');
347
+ if (activeEdit) {
348
+ const flagName = activeEdit.closest('[data-flag]').dataset.flag;
349
+ cancelEdit(flagName);
350
+ }
351
+ }
352
+ });
353
+ </script>
209
354
  </body>
210
355
  </html>
data/config/routes.rb CHANGED
@@ -2,10 +2,6 @@
2
2
 
3
3
  Switchlet::Engine.routes.draw do
4
4
  root "flags#index"
5
-
6
- resources :flags, only: [:index, :create, :destroy], param: :name do
7
- member do
8
- patch :toggle
9
- end
10
- end
11
- end
5
+
6
+ resources :flags, only: [:index, :create, :update, :destroy], param: :name
7
+ end
@@ -3,8 +3,9 @@
3
3
  class CreateSwitchletFlags < ActiveRecord::Migration[6.1]
4
4
  def change
5
5
  create_table :switchlet_flags do |t|
6
- t.string :name, null: false
7
- t.boolean :enabled, null: false, default: false
6
+ t.string :name, null: false
7
+ t.boolean :enabled, null: false, default: false
8
+ t.text :description
8
9
  t.timestamps
9
10
  end
10
11
  add_index :switchlet_flags, :name, unique: true
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Switchlet
4
- VERSION = "0.1.4"
4
+ VERSION = "0.3.1"
5
5
  end
data/lib/switchlet.rb CHANGED
@@ -21,15 +21,19 @@ module Switchlet
21
21
  Flag.find_by(name: name.to_s)&.enabled || false
22
22
  end
23
23
 
24
- def self.enable!(name)
24
+ def self.enable!(name, description: nil)
25
25
  flag = Flag.find_or_create_by(name: name.to_s)
26
- flag.update!(enabled: true)
26
+ attrs = { enabled: true }
27
+ attrs[:description] = description if description
28
+ flag.update!(attrs)
27
29
  true
28
30
  end
29
31
 
30
- def self.disable!(name)
32
+ def self.disable!(name, description: nil)
31
33
  flag = Flag.find_or_create_by(name: name.to_s)
32
- flag.update!(enabled: false)
34
+ attrs = { enabled: false }
35
+ attrs[:description] = description if description
36
+ flag.update!(attrs)
33
37
  false
34
38
  end
35
39
 
@@ -43,8 +47,18 @@ module Switchlet
43
47
  {
44
48
  name: flag.name,
45
49
  enabled: flag.enabled,
50
+ description: flag.description,
46
51
  updated_at: flag.updated_at
47
52
  }
48
53
  end
49
54
  end
55
+
56
+ def self.update!(name, description: nil, enabled: nil)
57
+ flag = Flag.find_or_create_by(name: name.to_s)
58
+ attrs = {}
59
+ attrs[:description] = description unless description.nil?
60
+ attrs[:enabled] = enabled unless enabled.nil?
61
+ flag.update!(attrs) if attrs.any?
62
+ flag
63
+ end
50
64
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: switchlet
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - komagata
@@ -76,9 +76,6 @@ files:
76
76
  - lib/switchlet/version.rb
77
77
  - lib/tasks/switchlet.rake
78
78
  - sig/switchlet.rbs
79
- - switchlet-0.1.1.gem
80
- - switchlet-0.1.2.gem
81
- - switchlet-0.1.3.gem
82
79
  homepage: https://github.com/komagata/switchlet
83
80
  licenses:
84
81
  - MIT
data/switchlet-0.1.1.gem DELETED
Binary file
data/switchlet-0.1.2.gem DELETED
Binary file
data/switchlet-0.1.3.gem DELETED
Binary file