switchlet 0.2.0 → 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: a8309b12036dc97dd351cf840b260b942f7a481b16f2d2256449f3d14afe1175
4
- data.tar.gz: b89973e26a4365e673141e017246d67c066af6a5704f860ce76cc7efc311ffa6
3
+ metadata.gz: f5df5ba1da7343518f0a7886f3e8cb26a7b125d217fddae79a43106e58a07ca3
4
+ data.tar.gz: e2005adb28bcd955ea762047e462f6befd04502d99df79fffdb69119d426addf
5
5
  SHA512:
6
- metadata.gz: 5693ae735fde0e0a420e09433a7601f0dda586aa2afd0cf3309a56a0e6430beafc6036e02cbd8cebfdbf9af98c40bbc1e87e4cc16daeefe91893d613e5431f1e
7
- data.tar.gz: d1e017ab424b46f7e28d82905e6ab281305cd188ccf873f7b645c5c7888e2b865c2c3a069fdcad28d04f3e0b974bcd42257f153b7c78d92353b0a7f5448c3119
6
+ metadata.gz: 3ece132acb395196ea895f118a6899857ffafe3d07930eaaccee2fe0115573d7addb904f2236758b8e1cee91ff2ed51af771b27335061ef4ceee1e9f884b8570
7
+ data.tar.gz: 880b12eb1b49bcb90122be722b5ee9c8a0c359b558af95c961ee58471374fff013965513115fb246abd4eb751453202cbf6dd17c100dacebc60cd77b4c4a0aed
data/README.md CHANGED
@@ -28,16 +28,18 @@ Switchlet.enabled?(:my_feature) # => false
28
28
  Switchlet.enable!(:my_feature) # => true
29
29
 
30
30
  # Enable a feature with description
31
- Switchlet.enable!(:my_feature, description: "New payment system") # => true
31
+ Switchlet.enable!(:my_feature) # => true
32
32
 
33
33
  # Disable a feature
34
34
  Switchlet.disable!(:my_feature) # => false
35
35
 
36
36
  # Disable with description
37
- Switchlet.disable!(:my_feature, description: "Temporarily disabled") # => false
37
+ Switchlet.disable!(:my_feature) # => false
38
38
 
39
- # Set or update description
40
- Switchlet.set_description!(:my_feature, "Updated description") # => "Updated description"
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
41
43
 
42
44
  # Delete a feature flag
43
45
  Switchlet.delete!(:my_feature) # => nil
@@ -46,6 +48,45 @@ Switchlet.delete!(:my_feature) # => nil
46
48
  Switchlet.list # => [{ name: "my_feature", enabled: true, description: "New payment system", updated_at: Time }]
47
49
  ```
48
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
88
+ ```
89
+
49
90
  ## Web UI
50
91
 
51
92
  Switchlet includes a web interface for managing feature flags:
@@ -6,18 +6,6 @@ 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
@@ -32,9 +20,24 @@ module Switchlet
32
20
 
33
21
  def update
34
22
  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}'"
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
38
41
  end
39
42
 
40
43
  def destroy
@@ -6,163 +6,207 @@
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
53
  flex-direction: column;
57
- gap: 10px;
54
+ gap: 8px;
58
55
  }
59
56
  .form-row {
60
57
  display: flex;
61
- gap: 10px;
58
+ gap: 8px;
62
59
  align-items: center;
63
60
  }
64
- input[type="text"], textarea {
61
+ .flag-name-input {
62
+ flex: 0 0 200px;
63
+ }
64
+ .description-input-inline {
65
65
  flex: 1;
66
- padding: 8px 12px;
67
- border: 1px solid #ddd;
68
- border-radius: 4px;
66
+ }
67
+ input, textarea {
68
+ padding: 8px;
69
+ border: 1px solid #ccc;
70
+ border-radius: 3px;
69
71
  font-size: 14px;
70
- font-family: inherit;
71
72
  }
72
73
  textarea {
73
74
  resize: vertical;
74
- min-height: 40px;
75
75
  }
76
76
  .btn {
77
- padding: 8px 16px;
78
- border: none;
79
- border-radius: 4px;
77
+ padding: 8px 12px;
78
+ border: 1px solid #ccc;
79
+ border-radius: 3px;
80
+ background: #f8f8f8;
80
81
  cursor: pointer;
81
- text-decoration: none;
82
82
  font-size: 14px;
83
+ text-decoration: none;
83
84
  display: inline-block;
84
85
  }
86
+ .btn:hover {
87
+ background: #e8e8e8;
88
+ }
85
89
  .btn-primary {
86
- background-color: #3498db;
87
- color: white;
90
+ background: #0969da;
91
+ color: #fff;
92
+ border-color: #0969da;
88
93
  }
89
94
  .btn-primary:hover {
90
- background-color: #2980b9;
95
+ background: #0860ca;
91
96
  }
92
97
  .btn-success {
93
- background-color: #27ae60;
94
- color: white;
98
+ background: #1a7f37;
99
+ color: #fff;
100
+ border-color: #1a7f37;
95
101
  }
96
102
  .btn-success:hover {
97
- background-color: #219a52;
103
+ background: #116329;
98
104
  }
99
105
  .btn-danger {
100
- background-color: #e74c3c;
101
- color: white;
106
+ background: #d1242f;
107
+ color: #fff;
108
+ border-color: #d1242f;
102
109
  }
103
110
  .btn-danger:hover {
104
- background-color: #c0392b;
111
+ background: #a40e26;
105
112
  }
106
- .btn-secondary {
107
- background-color: #95a5a6;
108
- color: white;
109
- }
110
- .btn-secondary:hover {
111
- background-color: #7f8c8d;
113
+ .btn-sm {
114
+ padding: 4px 8px;
115
+ font-size: 12px;
112
116
  }
113
- .flags-table {
117
+ table {
114
118
  width: 100%;
115
119
  border-collapse: collapse;
116
- margin-top: 20px;
120
+ border: 1px solid #ddd;
117
121
  }
118
- .flags-table th,
119
- .flags-table td {
120
- padding: 12px;
122
+ th, td {
123
+ padding: 8px 12px;
121
124
  text-align: left;
122
125
  border-bottom: 1px solid #ddd;
123
126
  }
124
- .flags-table th {
125
- background-color: #f8f9fa;
127
+ th {
128
+ background: #f6f8fa;
126
129
  font-weight: 600;
127
- color: #2c3e50;
130
+ font-size: 14px;
128
131
  }
129
132
  .status-enabled {
130
- color: #27ae60;
131
- font-weight: bold;
133
+ color: #1a7f37;
134
+ font-weight: 600;
132
135
  }
133
136
  .status-disabled {
134
- color: #e74c3c;
135
- font-weight: bold;
137
+ color: #d1242f;
138
+ font-weight: 600;
136
139
  }
137
140
  .actions {
138
141
  display: flex;
139
- gap: 8px;
142
+ gap: 4px;
143
+ align-items: center;
140
144
  }
141
145
  .actions form {
142
146
  margin: 0;
143
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
+ }
144
194
  .empty-state {
145
195
  text-align: center;
146
- padding: 40px;
147
- color: #7f8c8d;
196
+ padding: 48px 16px;
197
+ color: #656d76;
148
198
  }
149
199
  .text-muted {
150
- color: #7f8c8d;
151
- font-style: italic;
152
- }
153
- .description-cell {
154
- min-width: 200px;
200
+ color: #656d76;
155
201
  }
156
202
  .description-text {
157
203
  cursor: pointer;
158
- display: block;
159
- min-height: 20px;
160
204
  padding: 4px;
161
- border-radius: 4px;
162
- transition: background-color 0.2s;
205
+ border-radius: 3px;
206
+ min-height: 16px;
163
207
  }
164
208
  .description-text:hover {
165
- background-color: #f8f9fa;
209
+ background: #f6f8fa;
166
210
  }
167
211
  .description-input {
168
212
  width: 100%;
@@ -172,10 +216,6 @@
172
216
  display: flex;
173
217
  gap: 4px;
174
218
  }
175
- .btn-sm {
176
- padding: 4px 8px;
177
- font-size: 12px;
178
- }
179
219
  </style>
180
220
  </head>
181
221
  <body>
@@ -193,12 +233,10 @@
193
233
  <div class="create-form">
194
234
  <h2>Create New Flag</h2>
195
235
  <%= form_with url: switchlet.flags_path, local: true do |form| %>
196
- <div class="form-group">
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 %>
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" %>
239
+ <%= form.submit "Create Flag", class: "btn btn-primary" %>
202
240
  </div>
203
241
  <% end %>
204
242
  </div>
@@ -233,7 +271,9 @@
233
271
  <% end %>
234
272
  </span>
235
273
  <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| %>
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' %>
237
277
  <%= form.text_area :description, value: flag[:description], rows: 2,
238
278
  class: "description-input",
239
279
  placeholder: "Enter description..." %>
@@ -253,14 +293,19 @@
253
293
  <td><%= flag[:updated_at].strftime("%Y-%m-%d %H:%M") %></td>
254
294
  <td>
255
295
  <div class="actions">
256
- <%= button_to switchlet.toggle_flag_path(flag[:name]),
257
- method: :patch,
258
- class: "btn #{flag[:enabled] ? 'btn-secondary' : 'btn-success'}" do %>
259
- <%= 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>
260
305
  <% end %>
261
306
  <%= button_to switchlet.flag_path(flag[:name]),
262
307
  method: :delete,
263
- class: "btn btn-danger",
308
+ class: "btn btn-danger btn-sm",
264
309
  confirm: "Are you sure you want to delete '#{flag[:name]}'?" do %>
265
310
  Delete
266
311
  <% end %>
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, :update, :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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Switchlet
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.1"
5
5
  end
data/lib/switchlet.rb CHANGED
@@ -53,9 +53,12 @@ module Switchlet
53
53
  end
54
54
  end
55
55
 
56
- def self.set_description!(name, description)
56
+ def self.update!(name, description: nil, enabled: nil)
57
57
  flag = Flag.find_or_create_by(name: name.to_s)
58
- flag.update!(description: description)
59
- flag.description
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
60
63
  end
61
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.2.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - komagata
@@ -65,9 +65,7 @@ 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
69
68
  - lib/generators/switchlet/install_generator.rb
70
- - lib/generators/switchlet/templates/add_description_to_switchlet_flags.rb
71
69
  - lib/generators/switchlet/templates/create_switchlet_flags.rb
72
70
  - lib/generators/switchlet/templates/switchlet.rb
73
71
  - lib/switchlet.rb
@@ -78,10 +76,6 @@ files:
78
76
  - lib/switchlet/version.rb
79
77
  - lib/tasks/switchlet.rake
80
78
  - sig/switchlet.rbs
81
- - switchlet-0.1.1.gem
82
- - switchlet-0.1.2.gem
83
- - switchlet-0.1.3.gem
84
- - switchlet-0.1.4.gem
85
79
  homepage: https://github.com/komagata/switchlet
86
80
  licenses:
87
81
  - MIT
@@ -1,22 +0,0 @@
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
@@ -1,7 +0,0 @@
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
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
data/switchlet-0.1.4.gem DELETED
Binary file