magick-feature-flags 1.2.0 → 1.2.2
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/app/views/layouts/application.html.erb +849 -361
- data/app/views/magick/adminui/features/edit.html.erb +174 -71
- data/app/views/magick/adminui/features/index.html.erb +47 -48
- data/app/views/magick/adminui/features/show.html.erb +138 -109
- data/app/views/magick/adminui/stats/show.html.erb +48 -38
- data/lib/magick/version.rb +1 -1
- metadata +1 -1
|
@@ -1,67 +1,90 @@
|
|
|
1
|
+
<div class="breadcrumb">
|
|
2
|
+
<a href="<%= magick_admin_ui.features_path %>">Features</a>
|
|
3
|
+
<span class="breadcrumb-sep">/</span>
|
|
4
|
+
<a href="<%= magick_admin_ui.feature_path(@feature.name) %>"><%= @feature.display_name || @feature.name %></a>
|
|
5
|
+
<span class="breadcrumb-sep">/</span>
|
|
6
|
+
<span class="breadcrumb-current">Edit</span>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; flex-wrap: wrap; gap: 12px;">
|
|
10
|
+
<h1 class="page-title">Edit Feature</h1>
|
|
11
|
+
<a href="<%= magick_admin_ui.feature_path(@feature.name) %>" class="btn btn-secondary btn-sm">Cancel</a>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
1
14
|
<div class="card">
|
|
2
15
|
<div class="card-header">
|
|
3
|
-
<h2>
|
|
4
|
-
<div class="btn-group">
|
|
5
|
-
<a href="<%= magick_admin_ui.feature_path(@feature.name) %>" class="btn btn-secondary btn-sm">← Back</a>
|
|
6
|
-
</div>
|
|
16
|
+
<h2>Feature Settings</h2>
|
|
7
17
|
</div>
|
|
18
|
+
<div class="card-body">
|
|
19
|
+
<%= form_with url: magick_admin_ui.feature_path(@feature.name), method: :put, local: true do |f| %>
|
|
20
|
+
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">
|
|
21
|
+
<div class="form-group" style="margin-bottom: 0;">
|
|
22
|
+
<label class="form-label">Feature Key</label>
|
|
23
|
+
<input type="text" value="<%= @feature.name %>" disabled>
|
|
24
|
+
<p class="form-hint">Cannot be changed after creation</p>
|
|
25
|
+
</div>
|
|
8
26
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
</div>
|
|
27
|
+
<div class="form-group" style="margin-bottom: 0;">
|
|
28
|
+
<label class="form-label">Type</label>
|
|
29
|
+
<input type="text" value="<%= @feature.type.to_s.capitalize %>" disabled>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
15
32
|
|
|
16
|
-
|
|
17
|
-
<label>Type</label>
|
|
18
|
-
<input type="text" value="<%= @feature.type.to_s.capitalize %>" disabled>
|
|
19
|
-
</div>
|
|
33
|
+
<div style="margin-top: 20px;"></div>
|
|
20
34
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
35
|
+
<div class="form-group">
|
|
36
|
+
<label class="form-label">Group</label>
|
|
37
|
+
<%= text_field_tag 'group', @feature.group, placeholder: 'e.g., Authentication, Payment, UI', class: 'form-control' %>
|
|
38
|
+
<p class="form-hint">Organize features into groups for easier filtering</p>
|
|
39
|
+
</div>
|
|
26
40
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
<label style="display: inline-flex; align-items: center; cursor: pointer;">
|
|
41
|
+
<div class="form-group">
|
|
42
|
+
<label class="form-label">Value</label>
|
|
43
|
+
<% if @feature.type == :boolean %>
|
|
44
|
+
<label class="toggle-switch">
|
|
32
45
|
<%= check_box_tag 'value', 'true', @feature.enabled?, id: 'feature_value' %>
|
|
33
|
-
<span
|
|
46
|
+
<span class="toggle-track"></span>
|
|
47
|
+
<span class="toggle-label-text"><%= @feature.enabled? ? 'Enabled' : 'Disabled' %></span>
|
|
34
48
|
</label>
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
</div>
|
|
49
|
+
<%= hidden_field_tag 'value', 'false' %>
|
|
50
|
+
<p class="form-hint">Toggle to enable or disable this feature globally</p>
|
|
51
|
+
<% elsif @feature.type == :string %>
|
|
52
|
+
<%= text_field_tag 'value', @feature.get_value, class: 'form-control' %>
|
|
53
|
+
<% elsif @feature.type == :number %>
|
|
54
|
+
<%= number_field_tag 'value', @feature.get_value, class: 'form-control' %>
|
|
55
|
+
<% end %>
|
|
56
|
+
</div>
|
|
44
57
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
58
|
+
<div style="padding-top: 20px; border-top: 1px solid var(--color-border);">
|
|
59
|
+
<div class="btn-group">
|
|
60
|
+
<%= submit_tag 'Save Changes', class: 'btn btn-primary' %>
|
|
61
|
+
<a href="<%= magick_admin_ui.feature_path(@feature.name) %>" class="btn btn-secondary">Cancel</a>
|
|
62
|
+
</div>
|
|
49
63
|
</div>
|
|
50
|
-
|
|
51
|
-
|
|
64
|
+
<% end %>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
52
67
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
68
|
+
<% if @feature.type == :boolean %>
|
|
69
|
+
<div class="card">
|
|
70
|
+
<div class="card-header">
|
|
71
|
+
<h2>Quick Actions</h2>
|
|
72
|
+
</div>
|
|
73
|
+
<div class="card-body">
|
|
74
|
+
<p style="font-size: 13px; color: var(--color-text-secondary); margin-bottom: 16px;">Override value and clear targeting rules</p>
|
|
56
75
|
<div class="btn-group">
|
|
57
76
|
<%= button_to 'Enable Globally', magick_admin_ui.enable_feature_path(@feature.name), method: :put, class: 'btn btn-success btn-sm' %>
|
|
58
77
|
<%= button_to 'Disable Globally', magick_admin_ui.disable_feature_path(@feature.name), method: :put, class: 'btn btn-danger btn-sm' %>
|
|
59
78
|
</div>
|
|
60
79
|
</div>
|
|
61
|
-
|
|
80
|
+
</div>
|
|
81
|
+
<% end %>
|
|
62
82
|
|
|
63
|
-
|
|
64
|
-
|
|
83
|
+
<div class="card">
|
|
84
|
+
<div class="card-header">
|
|
85
|
+
<h2>Targeting Rules</h2>
|
|
86
|
+
</div>
|
|
87
|
+
<div class="card-body">
|
|
65
88
|
<%= form_with url: magick_admin_ui.update_targeting_feature_path(@feature.name), method: :put, local: true do |f| %>
|
|
66
89
|
<% targeting = @feature.instance_variable_get(:@targeting) || {} %>
|
|
67
90
|
<% current_roles = targeting[:role].is_a?(Array) ? targeting[:role] : (targeting[:role] ? [targeting[:role]] : []) %>
|
|
@@ -69,15 +92,14 @@
|
|
|
69
92
|
|
|
70
93
|
<% if roles_list.any? %>
|
|
71
94
|
<div class="form-group">
|
|
72
|
-
<label>
|
|
95
|
+
<label class="form-label">Roles</label>
|
|
73
96
|
<div class="tags-search-container">
|
|
74
|
-
<input type="text" id="roles_search" class="tags-search-input" placeholder="Search roles...
|
|
97
|
+
<input type="text" id="roles_search" class="tags-search-input" placeholder="Search roles..." autocomplete="off">
|
|
75
98
|
<div class="tags-count" id="roles_count">
|
|
76
|
-
<span id="roles_visible_count"><%= roles_list.length %></span> of <span id="roles_total_count"><%= roles_list.length %></span>
|
|
99
|
+
<span id="roles_visible_count"><%= roles_list.length %></span> of <span id="roles_total_count"><%= roles_list.length %></span>
|
|
77
100
|
</div>
|
|
78
101
|
</div>
|
|
79
102
|
<div class="tags-checkbox-container" id="roles_checkbox_container">
|
|
80
|
-
<%# Render checked roles first, then unchecked %>
|
|
81
103
|
<% checked_roles, unchecked_roles = roles_list.partition { |role| current_roles.include?(role.to_s) } %>
|
|
82
104
|
<% (checked_roles + unchecked_roles).each do |role| %>
|
|
83
105
|
<% role_name = role.to_s %>
|
|
@@ -89,7 +111,7 @@
|
|
|
89
111
|
</label>
|
|
90
112
|
<% end %>
|
|
91
113
|
</div>
|
|
92
|
-
<
|
|
114
|
+
<p class="form-hint">Select roles that should have access to this feature</p>
|
|
93
115
|
</div>
|
|
94
116
|
<% else %>
|
|
95
117
|
<div class="alert alert-info">
|
|
@@ -102,15 +124,14 @@
|
|
|
102
124
|
<% tags_list = available_tags %>
|
|
103
125
|
<% if tags_list.any? %>
|
|
104
126
|
<div class="form-group">
|
|
105
|
-
<label>
|
|
127
|
+
<label class="form-label">Tags</label>
|
|
106
128
|
<div class="tags-search-container">
|
|
107
|
-
<input type="text" id="tags_search" class="tags-search-input" placeholder="Search tags...
|
|
129
|
+
<input type="text" id="tags_search" class="tags-search-input" placeholder="Search tags..." autocomplete="off">
|
|
108
130
|
<div class="tags-count" id="tags_count">
|
|
109
|
-
<span id="tags_visible_count"><%= tags_list.length %></span> of <span id="tags_total_count"><%= tags_list.length %></span>
|
|
131
|
+
<span id="tags_visible_count"><%= tags_list.length %></span> of <span id="tags_total_count"><%= tags_list.length %></span>
|
|
110
132
|
</div>
|
|
111
133
|
</div>
|
|
112
134
|
<div class="tags-checkbox-container" id="tags_checkbox_container">
|
|
113
|
-
<%# Render checked tags first, then unchecked %>
|
|
114
135
|
<% checked_tags, unchecked_tags = tags_list.partition do |tag|
|
|
115
136
|
tag_id = tag.respond_to?(:id) ? tag.id.to_s : tag.to_s
|
|
116
137
|
current_tags.include?(tag_id.to_s)
|
|
@@ -125,7 +146,7 @@
|
|
|
125
146
|
</label>
|
|
126
147
|
<% end %>
|
|
127
148
|
</div>
|
|
128
|
-
<
|
|
149
|
+
<p class="form-hint">Select tags that should have access to this feature</p>
|
|
129
150
|
</div>
|
|
130
151
|
<% else %>
|
|
131
152
|
<div class="alert alert-info">
|
|
@@ -135,7 +156,7 @@
|
|
|
135
156
|
<% end %>
|
|
136
157
|
|
|
137
158
|
<div class="form-group">
|
|
138
|
-
<label>
|
|
159
|
+
<label class="form-label">User IDs</label>
|
|
139
160
|
<div class="user-ids-input-container">
|
|
140
161
|
<div class="user-ids-tags" id="user_ids_tags">
|
|
141
162
|
<% current_user_ids = targeting[:user].is_a?(Array) ? targeting[:user] : (targeting[:user] ? [targeting[:user]] : []) %>
|
|
@@ -149,27 +170,109 @@
|
|
|
149
170
|
<input type="text" id="user_ids_input" class="form-control user-ids-input" placeholder="Type user ID and press Enter" autocomplete="off">
|
|
150
171
|
<%= hidden_field_tag 'targeting[user_ids]', current_user_ids.join(','), id: 'user_ids_hidden' %>
|
|
151
172
|
</div>
|
|
152
|
-
<
|
|
173
|
+
<p class="form-hint">Type a user ID and press Enter to add. Supports comma-separated values.</p>
|
|
153
174
|
</div>
|
|
154
175
|
|
|
155
|
-
<div
|
|
156
|
-
<
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
176
|
+
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">
|
|
177
|
+
<div class="form-group" style="margin-bottom: 0;">
|
|
178
|
+
<label class="form-label">Percentage of Users</label>
|
|
179
|
+
<% current_percentage_users = targeting[:percentage_users] || '' %>
|
|
180
|
+
<%= number_field_tag 'targeting[percentage_users]', current_percentage_users, min: 0, max: 100, step: 0.1, placeholder: '0-100', class: 'form-control' %>
|
|
181
|
+
<p class="form-hint">Consistent user-based rollout (0-100)</p>
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
<div class="form-group" style="margin-bottom: 0;">
|
|
185
|
+
<label class="form-label">Percentage of Requests</label>
|
|
186
|
+
<% current_percentage_requests = targeting[:percentage_requests] || '' %>
|
|
187
|
+
<%= number_field_tag 'targeting[percentage_requests]', current_percentage_requests, min: 0, max: 100, step: 0.1, placeholder: '0-100', class: 'form-control' %>
|
|
188
|
+
<p class="form-hint">Random request-based sampling (0-100)</p>
|
|
189
|
+
</div>
|
|
160
190
|
</div>
|
|
161
191
|
|
|
162
|
-
<
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
<
|
|
192
|
+
<hr class="section-divider">
|
|
193
|
+
|
|
194
|
+
<div class="exclusion-border">
|
|
195
|
+
<h3 class="section-title section-title-danger">Exclusions</h3>
|
|
196
|
+
<p style="font-size: 13px; color: var(--color-text-secondary); margin-bottom: 20px;">Exclusions override all inclusion rules. Users matching any exclusion will be denied access.</p>
|
|
197
|
+
|
|
198
|
+
<% current_excluded_user_ids = targeting[:excluded_users].is_a?(Array) ? targeting[:excluded_users] : (targeting[:excluded_users] ? [targeting[:excluded_users]] : []) %>
|
|
199
|
+
<div class="form-group">
|
|
200
|
+
<label class="form-label">Excluded User IDs</label>
|
|
201
|
+
<div class="user-ids-input-container">
|
|
202
|
+
<div class="user-ids-tags" id="excluded_user_ids_tags">
|
|
203
|
+
<% current_excluded_user_ids.each do |user_id| %>
|
|
204
|
+
<span class="user-id-tag excluded">
|
|
205
|
+
<%= user_id %>
|
|
206
|
+
<span class="tag-remove" data-user-id="<%= user_id %>">×</span>
|
|
207
|
+
</span>
|
|
208
|
+
<% end %>
|
|
209
|
+
</div>
|
|
210
|
+
<input type="text" id="excluded_user_ids_input" class="form-control user-ids-input" placeholder="Type user ID and press Enter" autocomplete="off">
|
|
211
|
+
<%= hidden_field_tag 'targeting[excluded_user_ids]', current_excluded_user_ids.join(','), id: 'excluded_user_ids_hidden' %>
|
|
212
|
+
</div>
|
|
213
|
+
<p class="form-hint">These users will be blocked even if they match inclusion rules.</p>
|
|
214
|
+
</div>
|
|
215
|
+
|
|
216
|
+
<% if roles_list.any? %>
|
|
217
|
+
<% current_excluded_roles = targeting[:excluded_roles].is_a?(Array) ? targeting[:excluded_roles] : (targeting[:excluded_roles] ? [targeting[:excluded_roles]] : []) %>
|
|
218
|
+
<div class="form-group">
|
|
219
|
+
<label class="form-label">Excluded Roles</label>
|
|
220
|
+
<div class="tags-search-container">
|
|
221
|
+
<input type="text" id="excluded_roles_search" class="tags-search-input" placeholder="Search roles to exclude..." autocomplete="off">
|
|
222
|
+
<div class="tags-count" id="excluded_roles_count">
|
|
223
|
+
<span id="excluded_roles_visible_count"><%= roles_list.length %></span> of <span id="excluded_roles_total_count"><%= roles_list.length %></span>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
<div class="tags-checkbox-container" id="excluded_roles_checkbox_container">
|
|
227
|
+
<% checked_excluded_roles, unchecked_excluded_roles = roles_list.partition { |role| current_excluded_roles.include?(role.to_s) } %>
|
|
228
|
+
<% (checked_excluded_roles + unchecked_excluded_roles).each do |role| %>
|
|
229
|
+
<% role_name = role.to_s %>
|
|
230
|
+
<% role_display = role.to_s.humanize %>
|
|
231
|
+
<% is_checked = current_excluded_roles.include?(role.to_s) %>
|
|
232
|
+
<label class="tag-checkbox-label <%= 'checked' if is_checked %>" data-role-name="<%= role_name.downcase %>" data-checked="<%= is_checked %>">
|
|
233
|
+
<%= check_box_tag 'targeting[excluded_roles][]', role, is_checked, id: "excluded_role_#{role}", class: 'tag-checkbox' %>
|
|
234
|
+
<span class="tag-checkbox-text"><%= role_display %></span>
|
|
235
|
+
</label>
|
|
236
|
+
<% end %>
|
|
237
|
+
</div>
|
|
238
|
+
<p class="form-hint">Users with these roles will be blocked from this feature.</p>
|
|
239
|
+
</div>
|
|
240
|
+
<% end %>
|
|
241
|
+
|
|
242
|
+
<% if tags_list.any? %>
|
|
243
|
+
<% current_excluded_tags = targeting[:excluded_tags].is_a?(Array) ? targeting[:excluded_tags] : (targeting[:excluded_tags] ? [targeting[:excluded_tags]] : []) %>
|
|
244
|
+
<div class="form-group">
|
|
245
|
+
<label class="form-label">Excluded Tags</label>
|
|
246
|
+
<div class="tags-search-container">
|
|
247
|
+
<input type="text" id="excluded_tags_search" class="tags-search-input" placeholder="Search tags to exclude..." autocomplete="off">
|
|
248
|
+
<div class="tags-count" id="excluded_tags_count">
|
|
249
|
+
<span id="excluded_tags_visible_count"><%= tags_list.length %></span> of <span id="excluded_tags_total_count"><%= tags_list.length %></span>
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
<div class="tags-checkbox-container" id="excluded_tags_checkbox_container">
|
|
253
|
+
<% checked_excluded_tags, unchecked_excluded_tags = tags_list.partition do |tag|
|
|
254
|
+
tag_id = tag.respond_to?(:id) ? tag.id.to_s : tag.to_s
|
|
255
|
+
current_excluded_tags.include?(tag_id.to_s)
|
|
256
|
+
end %>
|
|
257
|
+
<% (checked_excluded_tags + unchecked_excluded_tags).each do |tag| %>
|
|
258
|
+
<% tag_id = tag.respond_to?(:id) ? tag.id.to_s : tag.to_s %>
|
|
259
|
+
<% tag_name = tag.respond_to?(:name) ? tag.name : (tag.respond_to?(:to_s) ? tag.to_s : tag_id) %>
|
|
260
|
+
<% is_checked = current_excluded_tags.include?(tag_id.to_s) %>
|
|
261
|
+
<label class="tag-checkbox-label <%= 'checked' if is_checked %>" data-tag-name="<%= tag_name.downcase %>" data-tag-id="<%= tag_id.downcase %>" data-checked="<%= is_checked %>">
|
|
262
|
+
<%= check_box_tag 'targeting[excluded_tags][]', tag_id, is_checked, id: "excluded_tag_#{tag_id}", class: 'tag-checkbox' %>
|
|
263
|
+
<span class="tag-checkbox-text"><%= tag_name %></span>
|
|
264
|
+
</label>
|
|
265
|
+
<% end %>
|
|
266
|
+
</div>
|
|
267
|
+
<p class="form-hint">Users with these tags will be blocked from this feature.</p>
|
|
268
|
+
</div>
|
|
269
|
+
<% end %>
|
|
167
270
|
</div>
|
|
168
271
|
|
|
169
|
-
<div
|
|
272
|
+
<div style="padding-top: 24px; border-top: 1px solid var(--color-border); margin-top: 8px;">
|
|
170
273
|
<div class="btn-group">
|
|
171
|
-
<%= submit_tag '
|
|
172
|
-
<a href="<%= magick_admin_ui.feature_path(@feature.name) %>" class="btn btn-secondary
|
|
274
|
+
<%= submit_tag 'Save Targeting Rules', class: 'btn btn-primary' %>
|
|
275
|
+
<a href="<%= magick_admin_ui.feature_path(@feature.name) %>" class="btn btn-secondary">Cancel</a>
|
|
173
276
|
</div>
|
|
174
277
|
</div>
|
|
175
278
|
<% end %>
|
|
@@ -1,29 +1,26 @@
|
|
|
1
|
-
<div
|
|
2
|
-
<div
|
|
3
|
-
<
|
|
4
|
-
<
|
|
5
|
-
<span class="badge badge-info"><%= @features.size %> features</span>
|
|
6
|
-
</div>
|
|
1
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px;">
|
|
2
|
+
<div>
|
|
3
|
+
<h1 class="page-title">Features</h1>
|
|
4
|
+
<p class="page-subtitle" id="feature_count_text"><%= @features.size %> feature<%= @features.size != 1 ? 's' : '' %></p>
|
|
7
5
|
</div>
|
|
6
|
+
</div>
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
<div style="padding: 16px; border-bottom: 1px solid
|
|
11
|
-
|
|
12
|
-
<div style="flex: 1; min-width: 200px;">
|
|
13
|
-
<
|
|
14
|
-
|
|
8
|
+
<div class="card">
|
|
9
|
+
<div style="padding: 12px 16px; border-bottom: 1px solid var(--color-border);">
|
|
10
|
+
<div class="filter-bar" style="display: flex; gap: 12px; align-items: center;">
|
|
11
|
+
<div style="flex: 1; min-width: 200px; position: relative;">
|
|
12
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="position: absolute; left: 10px; top: 50%; transform: translateY(-50%); color: var(--color-text-muted); pointer-events: none;"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
|
|
13
|
+
<input type="text" id="feature_search_input" placeholder="Search features..." style="padding-left: 34px;" autocomplete="off" value="<%= params[:search] %>">
|
|
15
14
|
</div>
|
|
16
15
|
<div style="flex: 0 0 180px;">
|
|
17
|
-
<
|
|
18
|
-
|
|
16
|
+
<select id="feature_group_select">
|
|
17
|
+
<option value="">All groups</option>
|
|
18
|
+
<% @available_groups.each do |g| %>
|
|
19
|
+
<option value="<%= g %>" <%= 'selected' if params[:group] == g %>><%= g %></option>
|
|
20
|
+
<% end %>
|
|
21
|
+
</select>
|
|
19
22
|
</div>
|
|
20
|
-
|
|
21
|
-
<%= submit_tag 'Filter', class: 'btn btn-primary btn-sm', style: "height: 38px;" %>
|
|
22
|
-
<% if params[:search].present? || params[:group].present? %>
|
|
23
|
-
<%= link_to 'Clear', magick_admin_ui.features_path, class: 'btn btn-secondary btn-sm', style: "height: 38px;" %>
|
|
24
|
-
<% end %>
|
|
25
|
-
</div>
|
|
26
|
-
<% end %>
|
|
23
|
+
</div>
|
|
27
24
|
</div>
|
|
28
25
|
|
|
29
26
|
<% if @features.empty? %>
|
|
@@ -40,68 +37,70 @@
|
|
|
40
37
|
<th>Type</th>
|
|
41
38
|
<th>Status</th>
|
|
42
39
|
<th>Value</th>
|
|
43
|
-
<th>
|
|
44
|
-
<th>Actions</th>
|
|
40
|
+
<th style="text-align: right;">Actions</th>
|
|
45
41
|
</tr>
|
|
46
42
|
</thead>
|
|
47
|
-
<tbody>
|
|
43
|
+
<tbody id="features_table_body">
|
|
48
44
|
<% @features.each do |feature| %>
|
|
49
|
-
<tr
|
|
45
|
+
<tr data-feature
|
|
46
|
+
data-feature-name="<%= feature.name %>"
|
|
47
|
+
data-feature-display="<%= feature.display_name %>"
|
|
48
|
+
data-feature-desc="<%= feature.description %>"
|
|
49
|
+
data-feature-group="<%= feature.group %>">
|
|
50
50
|
<td data-label="Name">
|
|
51
|
-
<
|
|
52
|
-
<
|
|
53
|
-
<small style="color: #999;"><%= feature.name %></small>
|
|
51
|
+
<div style="font-weight: 600; color: var(--color-text);"><%= feature.display_name || feature.name %></div>
|
|
52
|
+
<div style="font-size: 12px; color: var(--color-text-muted); font-family: var(--font-mono);"><%= feature.name %></div>
|
|
54
53
|
</td>
|
|
55
54
|
<td data-label="Group">
|
|
56
55
|
<% if feature.group.present? %>
|
|
57
|
-
<span class="badge badge-
|
|
56
|
+
<span class="badge badge-neutral"><%= feature.group %></span>
|
|
58
57
|
<% else %>
|
|
59
|
-
<
|
|
58
|
+
<span style="color: var(--color-text-muted);">—</span>
|
|
60
59
|
<% end %>
|
|
61
60
|
</td>
|
|
62
61
|
<td data-label="Type">
|
|
63
|
-
<span
|
|
64
|
-
<%= feature.type.to_s.capitalize %>
|
|
65
|
-
</span>
|
|
62
|
+
<span style="font-size: 13px; color: var(--color-text-secondary);"><%= feature.type.to_s.capitalize %></span>
|
|
66
63
|
</td>
|
|
67
64
|
<td data-label="Status">
|
|
68
65
|
<% case feature.status.to_sym %>
|
|
69
66
|
<% when :active %>
|
|
70
|
-
<span class="
|
|
67
|
+
<span class="status-indicator"><span class="status-dot status-dot-green"></span> Active</span>
|
|
71
68
|
<% when :deprecated %>
|
|
72
|
-
<span class="
|
|
69
|
+
<span class="status-indicator"><span class="status-dot status-dot-yellow"></span> Deprecated</span>
|
|
73
70
|
<% when :inactive %>
|
|
74
|
-
<span class="
|
|
71
|
+
<span class="status-indicator"><span class="status-dot status-dot-red"></span> Inactive</span>
|
|
75
72
|
<% else %>
|
|
76
|
-
<span class="
|
|
73
|
+
<span class="status-indicator"><%= feature.status.to_s.capitalize %></span>
|
|
77
74
|
<% end %>
|
|
78
75
|
</td>
|
|
79
76
|
<td data-label="Value">
|
|
80
77
|
<% if feature.type == :boolean %>
|
|
81
78
|
<% targeting = feature.instance_variable_get(:@targeting) || {} %>
|
|
82
79
|
<% if targeting.any? %>
|
|
83
|
-
<span class="
|
|
80
|
+
<span class="toggle-indicator toggle-partial">PARTIAL</span>
|
|
84
81
|
<% elsif feature.enabled? %>
|
|
85
|
-
<span class="
|
|
82
|
+
<span class="toggle-indicator toggle-on">ON</span>
|
|
86
83
|
<% else %>
|
|
87
|
-
<span class="
|
|
84
|
+
<span class="toggle-indicator toggle-off">OFF</span>
|
|
88
85
|
<% end %>
|
|
89
86
|
<% else %>
|
|
90
87
|
<code><%= feature.get_value.inspect %></code>
|
|
91
88
|
<% end %>
|
|
92
89
|
</td>
|
|
93
|
-
<td data-label="
|
|
94
|
-
<%= feature.
|
|
95
|
-
</td>
|
|
96
|
-
<td data-label="Actions">
|
|
97
|
-
<div class="btn-group">
|
|
98
|
-
<a href="<%= magick_admin_ui.feature_path(feature.name) %>" class="btn btn-primary btn-sm">View</a>
|
|
99
|
-
<a href="<%= magick_admin_ui.edit_feature_path(feature.name) %>" class="btn btn-secondary btn-sm">Edit</a>
|
|
100
|
-
</div>
|
|
90
|
+
<td data-label="Actions" style="text-align: right;">
|
|
91
|
+
<a href="<%= magick_admin_ui.feature_path(feature.name) %>" class="link-subtle">View</a>
|
|
101
92
|
</td>
|
|
102
93
|
</tr>
|
|
103
94
|
<% end %>
|
|
104
95
|
</tbody>
|
|
105
96
|
</table>
|
|
97
|
+
<tr id="features_empty_filter" style="display: none;">
|
|
98
|
+
<td colspan="6">
|
|
99
|
+
<div class="empty-state" style="padding: 40px 20px;">
|
|
100
|
+
<h3>No matching features</h3>
|
|
101
|
+
<p>Try adjusting your search or filter.</p>
|
|
102
|
+
</div>
|
|
103
|
+
</td>
|
|
104
|
+
</tr>
|
|
106
105
|
<% end %>
|
|
107
106
|
</div>
|