subflag-rails 0.6.0 → 0.6.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: 474015cc849b73a4360fa11418161fbc78c6bbc163596b5b9add7e45929b6db4
4
- data.tar.gz: 25f595010b6722d107b0604774ac089386e9d3ced39d7fef72411964a310140c
3
+ metadata.gz: 67d1ea6917f303ab8a78a5ffa90a9d5cfc3d6c30b7c13cd039f89cdd20365402
4
+ data.tar.gz: f8e5269abd5ef6ec77d8a4288b8a5ce91d114dcf96905d56c7aab5db368a1833
5
5
  SHA512:
6
- metadata.gz: afbe39727f8e70a3273b77e49bf8127debc459487b4d01bc14693c682082592b11fd3506388a8db2bd5021e9bbb914767e19f279a56a56a30c114fe79f10b0f2
7
- data.tar.gz: f124c9b3103ff13453615be92dfe7639fabfeb042d514bc4232b5a73e8fa1dcbe812e4d0fb418c45a35c33ac0e71753958d37cff01c6742ff991a408e637946f
6
+ metadata.gz: 7ba49356001ccb5676eccb9417e54bafc382bbc5ff0af19ad95a7567f4ac7a1fc1c2bd50505fad9d40ca4528e26e18c71900b423de149f91395c41686ce30b17
7
+ data.tar.gz: 18a47da01c9c44396faf29d4fe2234e4132f2479ff247aa5764f09fe0b27b361ceec05c65057f8cb8e5380ed2b9a319aa8a6fb7499882aa52ee38dcf7f0dcfab
data/CHANGELOG.md CHANGED
@@ -9,13 +9,14 @@ All notable changes to this project will be documented in this file.
9
9
  - **Percentage rollouts for ActiveRecord backend**: Gradually roll out features to a percentage of users
10
10
  - Deterministic assignment using MurmurHash3 (same user always gets the same result)
11
11
  - Combine with segment conditions (e.g., "50% of pro users")
12
- - Configure via Admin UI or targeting rules JSON
12
+ - Admin UI: percentage field in targeting rules editor
13
13
  - New dependency: `murmurhash3` gem for consistent hashing
14
14
 
15
15
  ### Changed
16
16
 
17
17
  - `TargetingEngine.evaluate` now accepts optional `flag_key:` parameter (required for percentage rollouts)
18
18
  - Targeting rules validation now accepts `percentage` as alternative to `conditions`
19
+ - Admin UI: targeting rules now saved via fetch API with proper redirect handling
19
20
 
20
21
  ## [0.5.1] - 2025-12-15
21
22
 
@@ -49,6 +49,8 @@
49
49
  .rule-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
50
50
  .rule-value { display: flex; gap: 10px; align-items: center; margin-bottom: 10px; }
51
51
  .rule-value input { width: 200px; }
52
+ .rule-percentage { display: flex; gap: 10px; align-items: center; margin-bottom: 10px; }
53
+ .rule-percentage input { width: 80px; }
52
54
  .rule-logic { margin-bottom: 10px; }
53
55
  .rule-logic label { margin-right: 15px; }
54
56
  .condition { display: flex; gap: 8px; align-items: center; margin-bottom: 8px; }
@@ -94,6 +96,8 @@
94
96
  function renderRule(rule, ruleIndex) {
95
97
  const conditions = rule.conditions?.conditions || [];
96
98
  const logicType = rule.conditions?.type || "AND";
99
+ const percentage = rule.percentage;
100
+ const hasConditions = conditions.length > 0;
97
101
 
98
102
  return `
99
103
  <div class="rule" data-rule-index="${ruleIndex}">
@@ -105,7 +109,13 @@
105
109
  <label>Return value:</label>
106
110
  <input type="text" value="${escapeHtml(rule.value || '')}" onchange="updateRuleValue(${ruleIndex}, this.value)" placeholder="value when matched">
107
111
  </div>
108
- <div class="rule-logic">
112
+ <div class="rule-percentage">
113
+ <label>Percentage rollout:</label>
114
+ <input type="number" min="0" max="100" value="${percentage ?? ''}" onchange="updateRulePercentage(${ruleIndex}, this.value)" placeholder="">
115
+ <span>% of users</span>
116
+ <small style="color: #666;">(leave empty to match all)</small>
117
+ </div>
118
+ <div class="rule-logic" ${!hasConditions && percentage ? 'style="opacity: 0.5;"' : ''}>
109
119
  <label><input type="radio" name="logic-${ruleIndex}" value="AND" ${logicType === 'AND' ? 'checked' : ''} onchange="updateLogic(${ruleIndex}, 'AND')"> ALL match</label>
110
120
  <label><input type="radio" name="logic-${ruleIndex}" value="OR" ${logicType === 'OR' ? 'checked' : ''} onchange="updateLogic(${ruleIndex}, 'OR')"> ANY match</label>
111
121
  </div>
@@ -113,6 +123,7 @@
113
123
  ${conditions.map((c, cIndex) => renderCondition(c, ruleIndex, cIndex)).join("")}
114
124
  </div>
115
125
  <button type="button" class="btn btn-sm" onclick="addCondition(${ruleIndex})">+ Add Condition</button>
126
+ <small style="display: block; margin-top: 5px; color: #666;">${hasConditions && percentage ? 'User must match conditions AND be in percentage' : ''}</small>
116
127
  </div>
117
128
  `;
118
129
  }
@@ -139,7 +150,7 @@
139
150
  function addRule() {
140
151
  rules.push({
141
152
  value: "",
142
- conditions: { type: "AND", conditions: [{ attribute: "", operator: "EQUALS", value: "" }] }
153
+ conditions: { type: "AND", conditions: [] }
143
154
  });
144
155
  renderRules();
145
156
  }
@@ -153,6 +164,15 @@
153
164
  rules[ruleIndex].value = value;
154
165
  }
155
166
 
167
+ function updateRulePercentage(ruleIndex, value) {
168
+ if (value === '' || value === null) {
169
+ delete rules[ruleIndex].percentage;
170
+ } else {
171
+ rules[ruleIndex].percentage = parseInt(value, 10);
172
+ }
173
+ renderRules();
174
+ }
175
+
156
176
  function updateLogic(ruleIndex, type) {
157
177
  rules[ruleIndex].conditions.type = type;
158
178
  }
@@ -180,55 +200,42 @@
180
200
  }
181
201
 
182
202
  function saveRules() {
183
- const form = document.createElement("form");
184
- form.method = "POST";
185
- form.action = "<%= flag_path(@flag) %>";
186
-
187
- const methodInput = document.createElement("input");
188
- methodInput.type = "hidden";
189
- methodInput.name = "_method";
190
- methodInput.value = "PATCH";
191
- form.appendChild(methodInput);
192
-
193
- const csrfInput = document.createElement("input");
194
- csrfInput.type = "hidden";
195
- csrfInput.name = "<%= request_forgery_protection_token %>";
196
- csrfInput.value = "<%= form_authenticity_token %>";
197
- form.appendChild(csrfInput);
198
-
199
- // Send all flag params to preserve other fields
200
- const keyInput = document.createElement("input");
201
- keyInput.type = "hidden";
202
- keyInput.name = "flag[key]";
203
- keyInput.value = "<%= @flag.key %>";
204
- form.appendChild(keyInput);
205
-
206
- const valueInput = document.createElement("input");
207
- valueInput.type = "hidden";
208
- valueInput.name = "flag[value]";
209
- valueInput.value = "<%= @flag.value %>";
210
- form.appendChild(valueInput);
211
-
212
- const typeInput = document.createElement("input");
213
- typeInput.type = "hidden";
214
- typeInput.name = "flag[value_type]";
215
- typeInput.value = "<%= @flag.value_type %>";
216
- form.appendChild(typeInput);
217
-
218
- const enabledInput = document.createElement("input");
219
- enabledInput.type = "hidden";
220
- enabledInput.name = "flag[enabled]";
221
- enabledInput.value = "<%= @flag.enabled? %>";
222
- form.appendChild(enabledInput);
223
-
224
- const rulesInput = document.createElement("input");
225
- rulesInput.type = "hidden";
226
- rulesInput.name = "flag[targeting_rules]";
227
- rulesInput.value = JSON.stringify(rules);
228
- form.appendChild(rulesInput);
229
-
230
- document.body.appendChild(form);
231
- form.submit();
203
+ // Clean up rules before saving - filter out invalid rules
204
+ const cleanedRules = rules.map(rule => {
205
+ const cleaned = { value: rule.value };
206
+ if (rule.percentage !== undefined && rule.percentage !== null && rule.percentage !== '') {
207
+ cleaned.percentage = rule.percentage;
208
+ }
209
+ const conditions = rule.conditions?.conditions?.filter(c => c.attribute && c.operator);
210
+ if (conditions && conditions.length > 0) {
211
+ cleaned.conditions = { type: rule.conditions?.type || "AND", conditions };
212
+ }
213
+ return cleaned;
214
+ }).filter(rule => rule.conditions || rule.percentage !== undefined);
215
+
216
+ const formData = new FormData();
217
+ formData.append("flag[key]", "<%= @flag.key %>");
218
+ formData.append("flag[value]", "<%= @flag.value %>");
219
+ formData.append("flag[value_type]", "<%= @flag.value_type %>");
220
+ formData.append("flag[enabled]", "<%= @flag.enabled? %>");
221
+ formData.append("flag[targeting_rules]", JSON.stringify(cleanedRules));
222
+
223
+ fetch("<%= flag_path(@flag) %>", {
224
+ method: "PATCH",
225
+ headers: {
226
+ "X-CSRF-Token": "<%= form_authenticity_token %>",
227
+ },
228
+ body: formData,
229
+ redirect: "manual"
230
+ }).then(response => {
231
+ if (response.ok || response.type === "opaqueredirect" || response.status === 302) {
232
+ window.location.reload();
233
+ } else {
234
+ alert("Failed to save rules: " + response.status);
235
+ }
236
+ }).catch(err => {
237
+ alert("Error saving rules: " + err.message);
238
+ });
232
239
  }
233
240
 
234
241
  function escapeHtml(str) {
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Subflag
4
4
  module Rails
5
- VERSION = "0.6.0"
5
+ VERSION = "0.6.1"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: subflag-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Subflag