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 +4 -4
- data/README.md +55 -4
- data/app/controllers/switchlet/flags_controller.rb +24 -13
- data/app/views/switchlet/flags/index.html.erb +217 -72
- data/config/routes.rb +3 -7
- data/lib/generators/switchlet/templates/create_switchlet_flags.rb +3 -2
- data/lib/switchlet/version.rb +1 -1
- data/lib/switchlet.rb +18 -4
- metadata +1 -4
- data/switchlet-0.1.1.gem +0 -0
- data/switchlet-0.1.2.gem +0 -0
- data/switchlet-0.1.3.gem +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f5df5ba1da7343518f0a7886f3e8cb26a7b125d217fddae79a43106e58a07ca3
|
|
4
|
+
data.tar.gz: e2005adb28bcd955ea762047e462f6befd04502d99df79fffdb69119d426addf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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,
|
|
9
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
10
10
|
margin: 0;
|
|
11
|
-
padding:
|
|
12
|
-
background
|
|
11
|
+
padding: 16px;
|
|
12
|
+
background: #fff;
|
|
13
13
|
color: #333;
|
|
14
|
+
line-height: 1.4;
|
|
14
15
|
}
|
|
15
16
|
.container {
|
|
16
|
-
max-width:
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
padding: 8px 12px;
|
|
28
|
+
margin-bottom: 16px;
|
|
29
|
+
border-radius: 3px;
|
|
30
|
+
font-size: 14px;
|
|
33
31
|
}
|
|
34
32
|
.alert-success {
|
|
35
|
-
background
|
|
36
|
-
color: #
|
|
37
|
-
border: 1px solid #c3e6cb;
|
|
33
|
+
background: #d4f7d4;
|
|
34
|
+
color: #0d5016;
|
|
38
35
|
}
|
|
39
36
|
.alert-danger {
|
|
40
|
-
background
|
|
37
|
+
background: #ffd6d6;
|
|
41
38
|
color: #721c24;
|
|
42
|
-
border: 1px solid #f5c6cb;
|
|
43
39
|
}
|
|
44
40
|
.create-form {
|
|
45
|
-
|
|
46
|
-
padding:
|
|
47
|
-
border
|
|
48
|
-
|
|
41
|
+
margin-bottom: 24px;
|
|
42
|
+
padding: 16px;
|
|
43
|
+
border: 1px solid #ddd;
|
|
44
|
+
border-radius: 3px;
|
|
49
45
|
}
|
|
50
46
|
.create-form h2 {
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
|
61
|
+
.flag-name-input {
|
|
62
|
+
flex: 0 0 200px;
|
|
63
|
+
}
|
|
64
|
+
.description-input-inline {
|
|
60
65
|
flex: 1;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
68
|
-
border:
|
|
69
|
-
border-radius:
|
|
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
|
|
77
|
-
color:
|
|
90
|
+
background: #0969da;
|
|
91
|
+
color: #fff;
|
|
92
|
+
border-color: #0969da;
|
|
78
93
|
}
|
|
79
94
|
.btn-primary:hover {
|
|
80
|
-
background
|
|
95
|
+
background: #0860ca;
|
|
81
96
|
}
|
|
82
97
|
.btn-success {
|
|
83
|
-
background
|
|
84
|
-
color:
|
|
98
|
+
background: #1a7f37;
|
|
99
|
+
color: #fff;
|
|
100
|
+
border-color: #1a7f37;
|
|
85
101
|
}
|
|
86
102
|
.btn-success:hover {
|
|
87
|
-
background
|
|
103
|
+
background: #116329;
|
|
88
104
|
}
|
|
89
105
|
.btn-danger {
|
|
90
|
-
background
|
|
91
|
-
color:
|
|
106
|
+
background: #d1242f;
|
|
107
|
+
color: #fff;
|
|
108
|
+
border-color: #d1242f;
|
|
92
109
|
}
|
|
93
110
|
.btn-danger:hover {
|
|
94
|
-
background
|
|
111
|
+
background: #a40e26;
|
|
95
112
|
}
|
|
96
|
-
.btn-
|
|
97
|
-
|
|
98
|
-
|
|
113
|
+
.btn-sm {
|
|
114
|
+
padding: 4px 8px;
|
|
115
|
+
font-size: 12px;
|
|
99
116
|
}
|
|
100
|
-
|
|
101
|
-
background-color: #7f8c8d;
|
|
102
|
-
}
|
|
103
|
-
.flags-table {
|
|
117
|
+
table {
|
|
104
118
|
width: 100%;
|
|
105
119
|
border-collapse: collapse;
|
|
106
|
-
|
|
120
|
+
border: 1px solid #ddd;
|
|
107
121
|
}
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
115
|
-
background
|
|
127
|
+
th {
|
|
128
|
+
background: #f6f8fa;
|
|
116
129
|
font-weight: 600;
|
|
117
|
-
|
|
130
|
+
font-size: 14px;
|
|
118
131
|
}
|
|
119
132
|
.status-enabled {
|
|
120
|
-
color: #
|
|
121
|
-
font-weight:
|
|
133
|
+
color: #1a7f37;
|
|
134
|
+
font-weight: 600;
|
|
122
135
|
}
|
|
123
136
|
.status-disabled {
|
|
124
|
-
color: #
|
|
125
|
-
font-weight:
|
|
137
|
+
color: #d1242f;
|
|
138
|
+
font-weight: 600;
|
|
126
139
|
}
|
|
127
140
|
.actions {
|
|
128
141
|
display: flex;
|
|
129
|
-
gap:
|
|
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:
|
|
137
|
-
color: #
|
|
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-
|
|
157
|
-
<%= form.text_field :flag_name, placeholder: "
|
|
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
|
-
<%=
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
|
7
|
-
|
|
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,
|
|
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,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
|
+
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
|