magick-feature-flags 0.9.35 → 0.9.37
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 +36 -10
- data/app/controllers/magick/adminui/features_controller.rb +23 -0
- data/app/views/magick/adminui/features/edit.html.erb +6 -0
- data/app/views/magick/adminui/features/index.html.erb +28 -0
- data/app/views/magick/adminui/features/show.html.erb +8 -0
- data/lib/magick/feature.rb +28 -4
- data/lib/magick/version.rb +1 -1
- metadata +6 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8fc6c9effed1052fbf1f929d206c9714917c1a5281234d517b4679cf9da44ded
|
|
4
|
+
data.tar.gz: cb345d7d484c471906a91b47ef63c3450e7613015ac6ca335c24bd73390f95f3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3ad0164ef606ed0cf3112ee94013a084a45ae16a3fc9f8a3ed6647043be76aa367f868d38324dc7dcb5118e4263164902eedd7704846d90c90c993e09dba85d7
|
|
7
|
+
data.tar.gz: f4b37a363842a2b34019c92370f0446a565a1087c90e0fb467244103bc1213c3dfc57b97c8b2e7d1b5623329862f9af23f9e75d2c0d0641ffb2b323d2289ba0a
|
data/README.md
CHANGED
|
@@ -45,18 +45,15 @@ This will create `config/initializers/magick.rb` with a basic configuration.
|
|
|
45
45
|
|
|
46
46
|
### ActiveRecord Adapter (Optional)
|
|
47
47
|
|
|
48
|
-
If you want to use ActiveRecord as a persistent storage backend, generate the migration:
|
|
48
|
+
If you want to use ActiveRecord as a persistent storage backend, you **must** generate and run the migration:
|
|
49
49
|
|
|
50
50
|
```bash
|
|
51
51
|
rails generate magick:active_record
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
This will create a migration file that creates the `magick_features` table. Then run:
|
|
55
|
-
|
|
56
|
-
```bash
|
|
57
52
|
rails db:migrate
|
|
58
53
|
```
|
|
59
54
|
|
|
55
|
+
This will create a migration file that creates the `magick_features` table. **The adapter will not auto-create the table** - you must run migrations.
|
|
56
|
+
|
|
60
57
|
**Note:** The ActiveRecord adapter is optional and only needed if you want database-backed feature flags. The gem works perfectly fine with just the memory adapter or Redis adapter.
|
|
61
58
|
|
|
62
59
|
## Configuration
|
|
@@ -137,7 +134,8 @@ end
|
|
|
137
134
|
Magick.register_feature(:new_dashboard,
|
|
138
135
|
type: :boolean,
|
|
139
136
|
default_value: false,
|
|
140
|
-
description: "New dashboard UI"
|
|
137
|
+
description: "New dashboard UI",
|
|
138
|
+
group: "UI" # Optional: group features for organization
|
|
141
139
|
)
|
|
142
140
|
|
|
143
141
|
# Register a string feature
|
|
@@ -460,7 +458,7 @@ The ActiveRecord adapter provides database-backed persistent storage for feature
|
|
|
460
458
|
|
|
461
459
|
**Setup:**
|
|
462
460
|
|
|
463
|
-
1. Generate the migration:
|
|
461
|
+
1. **Generate and run the migration** (required):
|
|
464
462
|
```bash
|
|
465
463
|
rails generate magick:active_record
|
|
466
464
|
rails db:migrate
|
|
@@ -469,8 +467,11 @@ The ActiveRecord adapter provides database-backed persistent storage for feature
|
|
|
469
467
|
**With UUID primary keys:**
|
|
470
468
|
```bash
|
|
471
469
|
rails generate magick:active_record --uuid
|
|
470
|
+
rails db:migrate
|
|
472
471
|
```
|
|
473
472
|
|
|
473
|
+
**Important:** The adapter will **not** auto-create the table. You must run migrations before using the ActiveRecord adapter. If the table doesn't exist, the adapter will raise a clear error with instructions.
|
|
474
|
+
|
|
474
475
|
2. Configure in `config/initializers/magick.rb`:
|
|
475
476
|
```ruby
|
|
476
477
|
Magick.configure do
|
|
@@ -480,8 +481,6 @@ The ActiveRecord adapter provides database-backed persistent storage for feature
|
|
|
480
481
|
end
|
|
481
482
|
```
|
|
482
483
|
|
|
483
|
-
The adapter automatically creates the `magick_features` table if it doesn't exist, but using the generator is recommended for production applications.
|
|
484
|
-
|
|
485
484
|
**PostgreSQL Support:**
|
|
486
485
|
|
|
487
486
|
The generator automatically detects PostgreSQL and uses `jsonb` for the `data` column, providing:
|
|
@@ -566,6 +565,33 @@ Once mounted, visit `/magick` in your browser to access the Admin UI.
|
|
|
566
565
|
- **Visual Display**: See all active targeting rules with badges
|
|
567
566
|
- **Edit Features**: Update feature values (boolean, string, number) directly from the UI
|
|
568
567
|
- **Statistics**: View performance metrics and usage statistics for each feature
|
|
568
|
+
- **Feature Grouping**: Organize features into groups for easier management and filtering
|
|
569
|
+
- **Filtering**: Filter features by group, name, or description
|
|
570
|
+
|
|
571
|
+
**Feature Grouping:**
|
|
572
|
+
|
|
573
|
+
Features can be organized into groups for easier management and filtering:
|
|
574
|
+
|
|
575
|
+
1. **Setting Groups**:
|
|
576
|
+
- Set a group when registering a feature in code:
|
|
577
|
+
```ruby
|
|
578
|
+
Magick.register_feature(:new_payment_flow,
|
|
579
|
+
type: :boolean,
|
|
580
|
+
default_value: false,
|
|
581
|
+
group: 'Payment',
|
|
582
|
+
description: "New payment processing flow"
|
|
583
|
+
)
|
|
584
|
+
```
|
|
585
|
+
- Or set/update groups via the Admin UI when editing a feature
|
|
586
|
+
|
|
587
|
+
2. **Filtering by Group**:
|
|
588
|
+
- Use the group dropdown in the Admin UI to filter features by group
|
|
589
|
+
- Combine group filtering with search to find specific features quickly
|
|
590
|
+
|
|
591
|
+
3. **Benefits**:
|
|
592
|
+
- Organize features by functional area (e.g., "Authentication", "Payment", "UI")
|
|
593
|
+
- Quickly find related features
|
|
594
|
+
- Better organization for large feature flag sets
|
|
569
595
|
|
|
570
596
|
**Targeting Management:**
|
|
571
597
|
|
|
@@ -26,6 +26,24 @@ module Magick
|
|
|
26
26
|
|
|
27
27
|
def index
|
|
28
28
|
@features = Magick.features.values
|
|
29
|
+
|
|
30
|
+
# Filter by group if provided
|
|
31
|
+
if params[:group].present?
|
|
32
|
+
@features = @features.select { |f| f.group == params[:group] }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Filter by search query (name or description)
|
|
36
|
+
if params[:search].present?
|
|
37
|
+
search_term = params[:search].downcase
|
|
38
|
+
@features = @features.select do |f|
|
|
39
|
+
f.name.downcase.include?(search_term) ||
|
|
40
|
+
(f.display_name && f.display_name.downcase.include?(search_term)) ||
|
|
41
|
+
(f.description && f.description.downcase.include?(search_term))
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Get all available groups for filter dropdown
|
|
46
|
+
@available_groups = Magick.features.values.map(&:group).compact.uniq.sort
|
|
29
47
|
end
|
|
30
48
|
|
|
31
49
|
def show
|
|
@@ -35,6 +53,11 @@ module Magick
|
|
|
35
53
|
end
|
|
36
54
|
|
|
37
55
|
def update
|
|
56
|
+
# Update group if provided
|
|
57
|
+
if params.key?(:group)
|
|
58
|
+
@feature.set_group(params[:group])
|
|
59
|
+
end
|
|
60
|
+
|
|
38
61
|
if @feature.type == :boolean
|
|
39
62
|
# For boolean features, checkbox sends 'true' when checked, nothing when unchecked
|
|
40
63
|
# Rails form helpers handle this - if checkbox is unchecked, params[:value] will be nil
|
|
@@ -18,6 +18,12 @@
|
|
|
18
18
|
<input type="text" value="<%= @feature.type.to_s.capitalize %>" disabled>
|
|
19
19
|
</div>
|
|
20
20
|
|
|
21
|
+
<div class="form-group">
|
|
22
|
+
<label>Group</label>
|
|
23
|
+
<%= text_field_tag 'group', @feature.group, placeholder: 'e.g., Authentication, Payment, UI', class: 'form-control' %>
|
|
24
|
+
<small style="color: #999;">Optional: Group features together for easier organization and filtering</small>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
21
27
|
<div class="form-group">
|
|
22
28
|
<label>Current Value</label>
|
|
23
29
|
<% if @feature.type == :boolean %>
|
|
@@ -6,6 +6,26 @@
|
|
|
6
6
|
</div>
|
|
7
7
|
</div>
|
|
8
8
|
|
|
9
|
+
<!-- Filtering UI -->
|
|
10
|
+
<div style="padding: 16px; border-bottom: 1px solid #e0e0e0; background: #f9f9f9;">
|
|
11
|
+
<%= form_with url: magick_admin_ui.features_path, method: :get, local: true, style: "display: flex; gap: 12px; flex-wrap: wrap; align-items: flex-end;" do |f| %>
|
|
12
|
+
<div style="flex: 1; min-width: 200px;">
|
|
13
|
+
<label for="search" style="display: block; margin-bottom: 4px; font-weight: 500;">Search</label>
|
|
14
|
+
<%= text_field_tag :search, params[:search], placeholder: "Search by name or description...", class: 'form-control', style: "width: 100%;" %>
|
|
15
|
+
</div>
|
|
16
|
+
<div style="flex: 0 0 180px;">
|
|
17
|
+
<label for="group" style="display: block; margin-bottom: 4px; font-weight: 500;">Group</label>
|
|
18
|
+
<%= select_tag :group, options_for_select([['All Groups', '']] + @available_groups.map { |g| [g, g] }, params[:group]), class: 'form-control', style: "width: 100%;" %>
|
|
19
|
+
</div>
|
|
20
|
+
<div style="flex: 0 0 auto;">
|
|
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 %>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
9
29
|
<% if @features.empty? %>
|
|
10
30
|
<div class="empty-state">
|
|
11
31
|
<h3>No features found</h3>
|
|
@@ -16,6 +36,7 @@
|
|
|
16
36
|
<thead>
|
|
17
37
|
<tr>
|
|
18
38
|
<th>Name</th>
|
|
39
|
+
<th>Group</th>
|
|
19
40
|
<th>Type</th>
|
|
20
41
|
<th>Status</th>
|
|
21
42
|
<th>Value</th>
|
|
@@ -31,6 +52,13 @@
|
|
|
31
52
|
<br>
|
|
32
53
|
<small style="color: #999;"><%= feature.name %></small>
|
|
33
54
|
</td>
|
|
55
|
+
<td data-label="Group">
|
|
56
|
+
<% if feature.group.present? %>
|
|
57
|
+
<span class="badge badge-info"><%= feature.group %></span>
|
|
58
|
+
<% else %>
|
|
59
|
+
<em style="color: #999;">—</em>
|
|
60
|
+
<% end %>
|
|
61
|
+
</td>
|
|
34
62
|
<td data-label="Type">
|
|
35
63
|
<span class="badge badge-info">
|
|
36
64
|
<%= feature.type.to_s.capitalize %>
|
|
@@ -18,6 +18,14 @@
|
|
|
18
18
|
<span class="badge badge-info"><%= @feature.type.to_s.capitalize %></span>
|
|
19
19
|
</div>
|
|
20
20
|
</div>
|
|
21
|
+
<% if @feature.group.present? %>
|
|
22
|
+
<div class="detail-item">
|
|
23
|
+
<div class="detail-label">Group</div>
|
|
24
|
+
<div class="detail-value">
|
|
25
|
+
<span class="badge badge-info"><%= @feature.group %></span>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
<% end %>
|
|
21
29
|
<div class="detail-item">
|
|
22
30
|
<div class="detail-label">Status</div>
|
|
23
31
|
<div class="detail-value">
|
data/lib/magick/feature.rb
CHANGED
|
@@ -8,7 +8,7 @@ module Magick
|
|
|
8
8
|
VALID_TYPES = %i[boolean string number].freeze
|
|
9
9
|
VALID_STATUSES = %i[active inactive deprecated].freeze
|
|
10
10
|
|
|
11
|
-
attr_reader :name, :type, :status, :default_value, :description, :display_name, :adapter_registry
|
|
11
|
+
attr_reader :name, :type, :status, :default_value, :description, :display_name, :group, :adapter_registry
|
|
12
12
|
|
|
13
13
|
def initialize(name, adapter_registry, **options)
|
|
14
14
|
@name = name.to_s
|
|
@@ -18,6 +18,7 @@ module Magick
|
|
|
18
18
|
@default_value = options.fetch(:default_value, default_for_type)
|
|
19
19
|
@description = options[:description]
|
|
20
20
|
@display_name = options[:name] || options[:display_name]
|
|
21
|
+
@group = options[:group]
|
|
21
22
|
@targeting = {}
|
|
22
23
|
@dependencies = options[:dependencies] ? Array(options[:dependencies]) : []
|
|
23
24
|
@stored_value_initialized = false # Track if @stored_value has been explicitly set
|
|
@@ -405,6 +406,7 @@ module Magick
|
|
|
405
406
|
adapter_registry.set(name, 'default_value', default_value)
|
|
406
407
|
adapter_registry.set(name, 'description', description) if description
|
|
407
408
|
adapter_registry.set(name, 'display_name', display_name) if display_name
|
|
409
|
+
adapter_registry.set(name, 'group', group) if group
|
|
408
410
|
@stored_value = value
|
|
409
411
|
@stored_value_initialized = true # Mark as initialized
|
|
410
412
|
|
|
@@ -516,6 +518,24 @@ module Magick
|
|
|
516
518
|
true
|
|
517
519
|
end
|
|
518
520
|
|
|
521
|
+
def set_group(group_name)
|
|
522
|
+
if group_name.nil? || group_name.to_s.strip.empty?
|
|
523
|
+
@group = nil
|
|
524
|
+
# Clear group from adapter by setting to empty string (adapters handle this)
|
|
525
|
+
adapter_registry.set(name, 'group', nil)
|
|
526
|
+
else
|
|
527
|
+
@group = group_name.to_s.strip
|
|
528
|
+
adapter_registry.set(name, 'group', @group)
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
# Update registered feature instance if it exists
|
|
532
|
+
if Magick.features.key?(name)
|
|
533
|
+
Magick.features[name].instance_variable_set(:@group, @group)
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
true
|
|
537
|
+
end
|
|
538
|
+
|
|
519
539
|
def delete
|
|
520
540
|
adapter_registry.delete(name)
|
|
521
541
|
@stored_value = nil
|
|
@@ -541,6 +561,7 @@ module Magick
|
|
|
541
561
|
registered.instance_variable_set(:@status, @status)
|
|
542
562
|
registered.instance_variable_set(:@description, @description)
|
|
543
563
|
registered.instance_variable_set(:@display_name, @display_name)
|
|
564
|
+
registered.instance_variable_set(:@group, @group)
|
|
544
565
|
registered.instance_variable_set(:@targeting, @targeting.dup)
|
|
545
566
|
registered.instance_variable_set(:@_targeting_empty, @_targeting_empty)
|
|
546
567
|
registered.instance_variable_set(:@_perf_metrics_enabled, @_perf_metrics_enabled)
|
|
@@ -619,6 +640,10 @@ module Magick
|
|
|
619
640
|
@display_name = display_name_value if display_name_value
|
|
620
641
|
end
|
|
621
642
|
|
|
643
|
+
# Load group from adapter (can be set via DSL or Admin UI)
|
|
644
|
+
group_value = adapter_registry.get(name, 'group')
|
|
645
|
+
@group = group_value if group_value
|
|
646
|
+
|
|
622
647
|
targeting_value = adapter_registry.get(name, 'targeting')
|
|
623
648
|
if targeting_value.is_a?(Hash)
|
|
624
649
|
# Normalize keys to symbols and handle nested structures
|
|
@@ -636,9 +661,8 @@ module Magick
|
|
|
636
661
|
# The features.rb file is the source of truth for metadata
|
|
637
662
|
# This ensures metadata is always up-to-date even if feature already exists
|
|
638
663
|
adapter_registry.set(name, 'description', @description) if @description
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
adapter_registry.set(name, 'display_name', @display_name)
|
|
664
|
+
adapter_registry.set(name, 'display_name', @display_name) if @display_name
|
|
665
|
+
adapter_registry.set(name, 'group', @group) if @group
|
|
642
666
|
end
|
|
643
667
|
|
|
644
668
|
def load_value_from_adapter
|
data/lib/magick/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: magick-feature-flags
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.9.
|
|
4
|
+
version: 0.9.37
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrew Lobanov
|
|
@@ -43,14 +43,14 @@ dependencies:
|
|
|
43
43
|
requirements:
|
|
44
44
|
- - "~>"
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: '
|
|
46
|
+
version: '3.8'
|
|
47
47
|
type: :development
|
|
48
48
|
prerelease: false
|
|
49
49
|
version_requirements: !ruby/object:Gem::Requirement
|
|
50
50
|
requirements:
|
|
51
51
|
- - "~>"
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
|
-
version: '
|
|
53
|
+
version: '3.8'
|
|
54
54
|
- !ruby/object:Gem::Dependency
|
|
55
55
|
name: activerecord
|
|
56
56
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -77,14 +77,14 @@ dependencies:
|
|
|
77
77
|
requirements:
|
|
78
78
|
- - "~>"
|
|
79
79
|
- !ruby/object:Gem::Version
|
|
80
|
-
version: '
|
|
80
|
+
version: '2.0'
|
|
81
81
|
type: :development
|
|
82
82
|
prerelease: false
|
|
83
83
|
version_requirements: !ruby/object:Gem::Requirement
|
|
84
84
|
requirements:
|
|
85
85
|
- - "~>"
|
|
86
86
|
- !ruby/object:Gem::Version
|
|
87
|
-
version: '
|
|
87
|
+
version: '2.0'
|
|
88
88
|
description: Magick is a better free version of Flipper feature-toggle gem. It is
|
|
89
89
|
absolutely performant and memory efficient (by my opinion).
|
|
90
90
|
email:
|
|
@@ -160,7 +160,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
160
160
|
requirements:
|
|
161
161
|
- - ">="
|
|
162
162
|
- !ruby/object:Gem::Version
|
|
163
|
-
version: 3.
|
|
163
|
+
version: 3.2.0
|
|
164
164
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
165
165
|
requirements:
|
|
166
166
|
- - ">="
|