switchlet 0.1.2 → 0.2.0

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: 47fde1d24d92a56e88b54b1707c71c60ba49c95d7a2002f0cefad8cb47c0c9b7
4
- data.tar.gz: b42b29792a99f75dceeadae2a977aff51cb904284e9d7b2d24521e268af2add2
3
+ metadata.gz: a8309b12036dc97dd351cf840b260b942f7a481b16f2d2256449f3d14afe1175
4
+ data.tar.gz: b89973e26a4365e673141e017246d67c066af6a5704f860ce76cc7efc311ffa6
5
5
  SHA512:
6
- metadata.gz: 7b00c754844ff30a6198b17e2cf6a85d67ff4580a45b715322f297fd405954e0e0f7b5cbe1b2b8de72f33a0b70a29bd47497e8057ab6ca43158d1b710c95235d
7
- data.tar.gz: e7267ce2fc68c2154225b1cb5345bb3f37686338503d2906642fde14d50deef8eb5b0cea74a490d9620ebc5a1d6a3b05dcf4f2e9558751ecbd1c394b955a9121
6
+ metadata.gz: 5693ae735fde0e0a420e09433a7601f0dda586aa2afd0cf3309a56a0e6430beafc6036e02cbd8cebfdbf9af98c40bbc1e87e4cc16daeefe91893d613e5431f1e
7
+ data.tar.gz: d1e017ab424b46f7e28d82905e6ab281305cd188ccf873f7b645c5c7888e2b865c2c3a069fdcad28d04f3e0b974bcd42257f153b7c78d92353b0a7f5448c3119
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Switchlet
1
+ # 🎛️ Switchlet
2
2
 
3
3
  Minimal feature flag gem. Simple boolean feature flags stored in database with web admin interface.
4
4
 
@@ -27,14 +27,23 @@ 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, description: "New payment system") # => 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, description: "Temporarily disabled") # => false
38
+
39
+ # Set or update description
40
+ Switchlet.set_description!(:my_feature, "Updated description") # => "Updated description"
41
+
33
42
  # Delete a feature flag
34
43
  Switchlet.delete!(:my_feature) # => nil
35
44
 
36
- # List all feature flags
37
- Switchlet.list # => [{ name: "my_feature", enabled: true, updated_at: Time }]
45
+ # List all feature flags (includes descriptions)
46
+ Switchlet.list # => [{ name: "my_feature", enabled: true, description: "New payment system", updated_at: Time }]
38
47
  ```
39
48
 
40
49
  ## Web UI
@@ -47,9 +56,10 @@ mount Switchlet::Engine => "/switchlet"
47
56
  ```
48
57
 
49
58
  Then visit `/switchlet` in your browser to:
50
- - View all feature flags
59
+ - View all feature flags with descriptions
51
60
  - Toggle flags ON/OFF
52
- - Create new flags
61
+ - Create new flags with optional descriptions
62
+ - Edit descriptions inline (click to edit)
53
63
  - Delete existing flags
54
64
 
55
65
  ### Securing the Web Interface
@@ -21,14 +21,22 @@ module Switchlet
21
21
 
22
22
  def create
23
23
  flag_name = params[:flag_name].strip
24
+ description = params[:description]&.strip
24
25
  if flag_name.present?
25
- Switchlet.enable!(flag_name)
26
+ Switchlet.enable!(flag_name, description: description)
26
27
  redirect_to switchlet.flags_path, notice: "Flag '#{flag_name}' created and enabled"
27
28
  else
28
29
  redirect_to switchlet.flags_path, alert: "Flag name cannot be empty"
29
30
  end
30
31
  end
31
32
 
33
+ def update
34
+ flag_name = params[:name]
35
+ description = params[:description]
36
+ Switchlet.set_description!(flag_name, description)
37
+ redirect_to switchlet.flags_path, notice: "Description updated for '#{flag_name}'"
38
+ end
39
+
32
40
  def destroy
33
41
  flag_name = params[:name]
34
42
  Switchlet.delete!(flag_name)
@@ -52,16 +52,26 @@
52
52
  color: #2c3e50;
53
53
  }
54
54
  .form-group {
55
+ display: flex;
56
+ flex-direction: column;
57
+ gap: 10px;
58
+ }
59
+ .form-row {
55
60
  display: flex;
56
61
  gap: 10px;
57
62
  align-items: center;
58
63
  }
59
- input[type="text"] {
64
+ input[type="text"], textarea {
60
65
  flex: 1;
61
66
  padding: 8px 12px;
62
67
  border: 1px solid #ddd;
63
68
  border-radius: 4px;
64
69
  font-size: 14px;
70
+ font-family: inherit;
71
+ }
72
+ textarea {
73
+ resize: vertical;
74
+ min-height: 40px;
65
75
  }
66
76
  .btn {
67
77
  padding: 8px 16px;
@@ -128,11 +138,44 @@
128
138
  display: flex;
129
139
  gap: 8px;
130
140
  }
141
+ .actions form {
142
+ margin: 0;
143
+ }
131
144
  .empty-state {
132
145
  text-align: center;
133
146
  padding: 40px;
134
147
  color: #7f8c8d;
135
148
  }
149
+ .text-muted {
150
+ color: #7f8c8d;
151
+ font-style: italic;
152
+ }
153
+ .description-cell {
154
+ min-width: 200px;
155
+ }
156
+ .description-text {
157
+ cursor: pointer;
158
+ display: block;
159
+ min-height: 20px;
160
+ padding: 4px;
161
+ border-radius: 4px;
162
+ transition: background-color 0.2s;
163
+ }
164
+ .description-text:hover {
165
+ background-color: #f8f9fa;
166
+ }
167
+ .description-input {
168
+ width: 100%;
169
+ margin-bottom: 8px;
170
+ }
171
+ .description-actions {
172
+ display: flex;
173
+ gap: 4px;
174
+ }
175
+ .btn-sm {
176
+ padding: 4px 8px;
177
+ font-size: 12px;
178
+ }
136
179
  </style>
137
180
  </head>
138
181
  <body>
@@ -151,8 +194,11 @@
151
194
  <h2>Create New Flag</h2>
152
195
  <%= form_with url: switchlet.flags_path, local: true do |form| %>
153
196
  <div class="form-group">
154
- <%= form.text_field :flag_name, placeholder: "Enter flag name...", required: true %>
155
- <%= form.submit "Create Flag", class: "btn btn-primary" %>
197
+ <div class="form-row">
198
+ <%= form.text_field :flag_name, placeholder: "Enter flag name...", required: true %>
199
+ <%= form.submit "Create Flag", class: "btn btn-primary" %>
200
+ </div>
201
+ <%= form.text_area :description, placeholder: "Optional description...", rows: 2 %>
156
202
  </div>
157
203
  <% end %>
158
204
  </div>
@@ -167,6 +213,7 @@
167
213
  <thead>
168
214
  <tr>
169
215
  <th>Flag Name</th>
216
+ <th>Description</th>
170
217
  <th>Status</th>
171
218
  <th>Last Updated</th>
172
219
  <th>Actions</th>
@@ -176,6 +223,28 @@
176
223
  <% @flags.each do |flag| %>
177
224
  <tr>
178
225
  <td><strong><%= flag[:name] %></strong></td>
226
+ <td>
227
+ <div class="description-cell" data-flag="<%= flag[:name] %>">
228
+ <span class="description-text" onclick="editDescription('<%= flag[:name] %>')">
229
+ <% if flag[:description].present? %>
230
+ <%= flag[:description] %>
231
+ <% else %>
232
+ <span class="text-muted">Click to add description</span>
233
+ <% end %>
234
+ </span>
235
+ <div class="description-edit" style="display: none;">
236
+ <%= form_with url: switchlet.flag_path(flag[:name]), method: :patch, local: true, class: "description-form" do |form| %>
237
+ <%= form.text_area :description, value: flag[:description], rows: 2,
238
+ class: "description-input",
239
+ placeholder: "Enter description..." %>
240
+ <div class="description-actions">
241
+ <%= form.submit "Save", class: "btn btn-success btn-sm" %>
242
+ <button type="button" class="btn btn-secondary btn-sm" onclick="cancelEdit('<%= flag[:name] %>')">Cancel</button>
243
+ </div>
244
+ <% end %>
245
+ </div>
246
+ </div>
247
+ </td>
179
248
  <td>
180
249
  <span class="<%= flag[:enabled] ? 'status-enabled' : 'status-disabled' %>">
181
250
  <%= flag[:enabled] ? 'ENABLED' : 'DISABLED' %>
@@ -184,15 +253,15 @@
184
253
  <td><%= flag[:updated_at].strftime("%Y-%m-%d %H:%M") %></td>
185
254
  <td>
186
255
  <div class="actions">
187
- <%= link_to switchlet.toggle_flag_path(flag[:name]),
188
- method: :patch,
189
- class: "btn #{flag[:enabled] ? 'btn-secondary' : 'btn-success'}" do %>
256
+ <%= button_to switchlet.toggle_flag_path(flag[:name]),
257
+ method: :patch,
258
+ class: "btn #{flag[:enabled] ? 'btn-secondary' : 'btn-success'}" do %>
190
259
  <%= flag[:enabled] ? 'Disable' : 'Enable' %>
191
260
  <% end %>
192
- <%= link_to switchlet.flag_path(flag[:name]),
193
- method: :delete,
194
- class: "btn btn-danger",
195
- confirm: "Are you sure you want to delete '#{flag[:name]}'?" do %>
261
+ <%= button_to switchlet.flag_path(flag[:name]),
262
+ method: :delete,
263
+ class: "btn btn-danger",
264
+ confirm: "Are you sure you want to delete '#{flag[:name]}'?" do %>
196
265
  Delete
197
266
  <% end %>
198
267
  </div>
@@ -203,5 +272,39 @@
203
272
  </table>
204
273
  <% end %>
205
274
  </div>
275
+
276
+ <script>
277
+ function editDescription(flagName) {
278
+ const cell = document.querySelector(`[data-flag="${flagName}"]`);
279
+ const textSpan = cell.querySelector('.description-text');
280
+ const editDiv = cell.querySelector('.description-edit');
281
+ const input = cell.querySelector('.description-input');
282
+
283
+ textSpan.style.display = 'none';
284
+ editDiv.style.display = 'block';
285
+ input.focus();
286
+ input.select();
287
+ }
288
+
289
+ function cancelEdit(flagName) {
290
+ const cell = document.querySelector(`[data-flag="${flagName}"]`);
291
+ const textSpan = cell.querySelector('.description-text');
292
+ const editDiv = cell.querySelector('.description-edit');
293
+
294
+ textSpan.style.display = 'block';
295
+ editDiv.style.display = 'none';
296
+ }
297
+
298
+ // Escape key to cancel editing
299
+ document.addEventListener('keydown', function(e) {
300
+ if (e.key === 'Escape') {
301
+ const activeEdit = document.querySelector('.description-edit[style*="block"]');
302
+ if (activeEdit) {
303
+ const flagName = activeEdit.closest('[data-flag]').dataset.flag;
304
+ cancelEdit(flagName);
305
+ }
306
+ }
307
+ });
308
+ </script>
206
309
  </body>
207
310
  </html>
data/config/routes.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  Switchlet::Engine.routes.draw do
4
4
  root "flags#index"
5
5
 
6
- resources :flags, only: [:index, :create, :destroy], param: :name do
6
+ resources :flags, only: [:index, :create, :update, :destroy], param: :name do
7
7
  member do
8
8
  patch :toggle
9
9
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/migration"
5
+
6
+ module Switchlet
7
+ module Generators
8
+ class AddDescriptionGenerator < Rails::Generators::Base
9
+ include Rails::Generators::Migration
10
+
11
+ source_root File.expand_path("templates", __dir__)
12
+
13
+ def self.next_migration_number(path)
14
+ ActiveRecord::Generators::Base.next_migration_number(path)
15
+ end
16
+
17
+ def create_migration_file
18
+ migration_template "add_description_to_switchlet_flags.rb", "db/migrate/add_description_to_switchlet_flags.rb"
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddDescriptionToSwitchletFlags < ActiveRecord::Migration[6.1]
4
+ def change
5
+ add_column :switchlet_flags, :description, :text
6
+ end
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.2"
4
+ VERSION = "0.2.0"
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,15 @@ 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.set_description!(name, description)
57
+ flag = Flag.find_or_create_by(name: name.to_s)
58
+ flag.update!(description: description)
59
+ flag.description
60
+ end
50
61
  end
Binary file
Binary file
Binary file
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.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - komagata
@@ -65,7 +65,9 @@ files:
65
65
  - app/controllers/switchlet/flags_controller.rb
66
66
  - app/views/switchlet/flags/index.html.erb
67
67
  - config/routes.rb
68
+ - lib/generators/switchlet/add_description_generator.rb
68
69
  - lib/generators/switchlet/install_generator.rb
70
+ - lib/generators/switchlet/templates/add_description_to_switchlet_flags.rb
69
71
  - lib/generators/switchlet/templates/create_switchlet_flags.rb
70
72
  - lib/generators/switchlet/templates/switchlet.rb
71
73
  - lib/switchlet.rb
@@ -77,6 +79,9 @@ files:
77
79
  - lib/tasks/switchlet.rake
78
80
  - sig/switchlet.rbs
79
81
  - switchlet-0.1.1.gem
82
+ - switchlet-0.1.2.gem
83
+ - switchlet-0.1.3.gem
84
+ - switchlet-0.1.4.gem
80
85
  homepage: https://github.com/komagata/switchlet
81
86
  licenses:
82
87
  - MIT