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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5a2d57b267028e1f3d30a1a5dc4e540d6b254d41ea1b436f5ab0e25278c0f213
4
- data.tar.gz: 0c1a022445c14e1534b312f4cfc71ec71b9a576d4b66a5d860323a480df79380
3
+ metadata.gz: 8fc6c9effed1052fbf1f929d206c9714917c1a5281234d517b4679cf9da44ded
4
+ data.tar.gz: cb345d7d484c471906a91b47ef63c3450e7613015ac6ca335c24bd73390f95f3
5
5
  SHA512:
6
- metadata.gz: a0576cc8537ef6df5ff4ce95f973994a477936d94cde874b24945505139fbf411798fb54d100544f1848ad1b2388164b64a95421732daa33d753bcef7c14cce5
7
- data.tar.gz: 803584894d4c2494c324df94613a80a34ca268f522ffabe36bc162e989ca25be8aaa9d1f919f10584e722b47863940a1610c6f7cbc75d6960b2070304e4efe55
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">
@@ -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
- return unless @display_name
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Magick
4
- VERSION = '0.9.35'
4
+ VERSION = '0.9.37'
5
5
  end
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.35
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: '2.22'
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: '2.22'
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: '1.6'
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: '1.6'
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.0.0
163
+ version: 3.2.0
164
164
  required_rubygems_version: !ruby/object:Gem::Requirement
165
165
  requirements:
166
166
  - - ">="