switchlet 0.1.4 → 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: aef30a43a9f953b56491c74e2569110d8f71ac8ed664643776fa8ae2fb066a8f
4
- data.tar.gz: 394b31eed795afb2cf2d8da5f5db665cbde32dd1b514e31acd2eaf6b82922c5c
3
+ metadata.gz: a8309b12036dc97dd351cf840b260b942f7a481b16f2d2256449f3d14afe1175
4
+ data.tar.gz: b89973e26a4365e673141e017246d67c066af6a5704f860ce76cc7efc311ffa6
5
5
  SHA512:
6
- metadata.gz: ea64546f7de6ab36485fb05c2e74f5db764b9b15f6abc58f4280c40d34df1a48d449c8a6570beaa885d8d05068ea93d7304847df7795adaa1106ec62d8435f1d
7
- data.tar.gz: c83defe48ee72b6d2f7f7121032800589c69424cb85f637d61dac1489c9dc85540218190715b951af224cbc471df1724137d73188e918aa473a7a4c78a732db3
6
+ metadata.gz: 5693ae735fde0e0a420e09433a7601f0dda586aa2afd0cf3309a56a0e6430beafc6036e02cbd8cebfdbf9af98c40bbc1e87e4cc16daeefe91893d613e5431f1e
7
+ data.tar.gz: d1e017ab424b46f7e28d82905e6ab281305cd188ccf873f7b645c5c7888e2b865c2c3a069fdcad28d04f3e0b974bcd42257f153b7c78d92353b0a7f5448c3119
data/README.md CHANGED
@@ -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;
@@ -136,6 +146,36 @@
136
146
  padding: 40px;
137
147
  color: #7f8c8d;
138
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
+ }
139
179
  </style>
140
180
  </head>
141
181
  <body>
@@ -154,8 +194,11 @@
154
194
  <h2>Create New Flag</h2>
155
195
  <%= form_with url: switchlet.flags_path, local: true do |form| %>
156
196
  <div class="form-group">
157
- <%= form.text_field :flag_name, placeholder: "Enter flag name...", required: true %>
158
- <%= 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 %>
159
202
  </div>
160
203
  <% end %>
161
204
  </div>
@@ -170,6 +213,7 @@
170
213
  <thead>
171
214
  <tr>
172
215
  <th>Flag Name</th>
216
+ <th>Description</th>
173
217
  <th>Status</th>
174
218
  <th>Last Updated</th>
175
219
  <th>Actions</th>
@@ -179,6 +223,28 @@
179
223
  <% @flags.each do |flag| %>
180
224
  <tr>
181
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>
182
248
  <td>
183
249
  <span class="<%= flag[:enabled] ? 'status-enabled' : 'status-disabled' %>">
184
250
  <%= flag[:enabled] ? 'ENABLED' : 'DISABLED' %>
@@ -206,5 +272,39 @@
206
272
  </table>
207
273
  <% end %>
208
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>
209
309
  </body>
210
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.4"
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
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.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
@@ -79,6 +81,7 @@ files:
79
81
  - switchlet-0.1.1.gem
80
82
  - switchlet-0.1.2.gem
81
83
  - switchlet-0.1.3.gem
84
+ - switchlet-0.1.4.gem
82
85
  homepage: https://github.com/komagata/switchlet
83
86
  licenses:
84
87
  - MIT