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 +4 -4
- data/README.md +15 -5
- data/app/controllers/switchlet/flags_controller.rb +9 -1
- data/app/views/switchlet/flags/index.html.erb +113 -10
- data/config/routes.rb +1 -1
- data/lib/generators/switchlet/add_description_generator.rb +22 -0
- data/lib/generators/switchlet/templates/add_description_to_switchlet_flags.rb +7 -0
- data/lib/generators/switchlet/templates/create_switchlet_flags.rb +3 -2
- data/lib/switchlet/version.rb +1 -1
- data/lib/switchlet.rb +15 -4
- data/switchlet-0.1.2.gem +0 -0
- data/switchlet-0.1.3.gem +0 -0
- data/switchlet-0.1.4.gem +0 -0
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a8309b12036dc97dd351cf840b260b942f7a481b16f2d2256449f3d14afe1175
|
|
4
|
+
data.tar.gz: b89973e26a4365e673141e017246d67c066af6a5704f860ce76cc7efc311ffa6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
155
|
-
|
|
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
|
-
<%=
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
<%=
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
@@ -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
|
|
@@ -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,
|
|
7
|
-
t.boolean :enabled,
|
|
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
|
data/lib/switchlet/version.rb
CHANGED
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
|
-
|
|
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
|
-
|
|
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
|
data/switchlet-0.1.2.gem
ADDED
|
Binary file
|
data/switchlet-0.1.3.gem
ADDED
|
Binary file
|
data/switchlet-0.1.4.gem
ADDED
|
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.
|
|
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
|