flipside 0.1.0
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 +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +78 -0
- data/app/models/flipside/checks.rb +13 -0
- data/app/models/flipside/entities.rb +15 -0
- data/app/models/flipside/entity.rb +11 -0
- data/app/models/flipside/feature.rb +40 -0
- data/app/models/flipside/key.rb +14 -0
- data/app/models/flipside/keys.rb +26 -0
- data/app/models/flipside/role.rb +10 -0
- data/app/models/flipside/roles.rb +26 -0
- data/lib/flipside/config/entities.rb +40 -0
- data/lib/flipside/config/registered_entity.rb +60 -0
- data/lib/flipside/config/registered_role.rb +43 -0
- data/lib/flipside/config/roles.rb +33 -0
- data/lib/flipside/feature_presenter.rb +111 -0
- data/lib/flipside/search_result.rb +11 -0
- data/lib/flipside/version.rb +5 -0
- data/lib/flipside/views/_datetime_modal.erb +37 -0
- data/lib/flipside/views/_feature_item.erb +10 -0
- data/lib/flipside/views/_search_result.erb +9 -0
- data/lib/flipside/views/_toggle_button.erb +12 -0
- data/lib/flipside/views/feature_entities.erb +68 -0
- data/lib/flipside/views/feature_roles.erb +68 -0
- data/lib/flipside/views/index.erb +15 -0
- data/lib/flipside/views/layout.erb +25 -0
- data/lib/flipside/views/not_found.erb +1 -0
- data/lib/flipside/views/show.erb +55 -0
- data/lib/flipside/web.rb +99 -0
- data/lib/flipside.rb +62 -0
- data/lib/generators/flipside/install/install_generator.rb +20 -0
- data/lib/generators/flipside/install/templates/20241122_create_flipside_migration.rb +28 -0
- metadata +146 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 10dcbfd4da7a00053fc1e9486c54312a0010096deeec169368e251dd173a6c41
|
4
|
+
data.tar.gz: 0327eac1507849ce0ae681a37af3ca5b09af9d642331619879cfd2e876f97462
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1380f6f03f37f95a4f567ea840534e371a963fa1380bde5424981651d558761c642f67ba9cbc9bc14fac90ee0dc9393020a3bb13ca809b0e5206cea7c542ffe9
|
7
|
+
data.tar.gz: f9827baa63d88ec20224c8e5bb356af5ac03271714a0db706359e53b157a28e03108a2be5c729fd83e58a39d443dc548b4e7bdcdebe591fce706b7839bae8d55
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2024 Sammy Henningsson
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
# Flipside
|
2
|
+
|
3
|
+
**Flipside** is a gem for managing feature flags in your Rails applications.
|
4
|
+
|
5
|
+
---
|
6
|
+
|
7
|
+
## Features
|
8
|
+
|
9
|
+
- enable or disable features globally
|
10
|
+
- enable features for specific records (e.g. users, organizations)
|
11
|
+
- enabled features for objects responding `true` to a certain method
|
12
|
+
- Setting a start and end time for when the feature is active
|
13
|
+
|
14
|
+
---
|
15
|
+
|
16
|
+
## Installation
|
17
|
+
|
18
|
+
Install the gem and add to the application's Gemfile by executing:
|
19
|
+
|
20
|
+
$ bundle add flipside
|
21
|
+
|
22
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
23
|
+
|
24
|
+
$ gem install flipside
|
25
|
+
|
26
|
+
Then run
|
27
|
+
|
28
|
+
$ rails generate flipside:install
|
29
|
+
|
30
|
+
This will create a migration file. Run the migration, to add the flipside tables
|
31
|
+
|
32
|
+
$ rails db:migrate
|
33
|
+
|
34
|
+
## Usage
|
35
|
+
|
36
|
+
1. Defining Features
|
37
|
+
|
38
|
+
Feature are created by running this (in a console or from code):
|
39
|
+
```ruby
|
40
|
+
Flipside::Feature.create(name: "MyFeature", description: "Some optional description about what this feature do")
|
41
|
+
```
|
42
|
+
|
43
|
+
By default features are turned off. If you would like it turned on from the beginning you could pass in `enabled: true`.
|
44
|
+
|
45
|
+
2. Checking Feature Status
|
46
|
+
|
47
|
+
#### Globally
|
48
|
+
|
49
|
+
Check if a feature is enabled globally:
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
Flipside.enabled? "MyFeature"
|
53
|
+
```
|
54
|
+
|
55
|
+
#### For a Specific Record
|
56
|
+
|
57
|
+
Check if a feature is enabled for a specific record (e.g. a user, an organization or a user responding `true` to `user.admin?`):
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
Flipside.enabled? "MyFeature", user
|
61
|
+
```
|
62
|
+
|
63
|
+
## Configuration
|
64
|
+
TODO
|
65
|
+
|
66
|
+
## Development
|
67
|
+
|
68
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rspec` to run the tests.
|
69
|
+
|
70
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
71
|
+
|
72
|
+
## Contributing
|
73
|
+
|
74
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[sammyhenningsson]/flipside.
|
75
|
+
|
76
|
+
## License
|
77
|
+
|
78
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "models/flipside/entity"
|
4
|
+
|
5
|
+
module Flipside
|
6
|
+
module Entities
|
7
|
+
def self.included(base)
|
8
|
+
base.has_many :entities, foreign_key: :feature_id
|
9
|
+
|
10
|
+
base.add_check do |entity|
|
11
|
+
entity && entities.where(flippable: entity).exists?
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "models/flipside/checks"
|
4
|
+
require "models/flipside/entities"
|
5
|
+
require "models/flipside/roles"
|
6
|
+
|
7
|
+
module Flipside
|
8
|
+
class Feature < ::ActiveRecord::Base
|
9
|
+
extend Checks
|
10
|
+
include Entities
|
11
|
+
include Roles
|
12
|
+
|
13
|
+
self.table_name = "flipside_features"
|
14
|
+
|
15
|
+
def enabled?(object = nil)
|
16
|
+
return false unless active?
|
17
|
+
return true if enabled
|
18
|
+
|
19
|
+
self.class.checks.any? { |check| instance_exec(object, &check) }
|
20
|
+
end
|
21
|
+
|
22
|
+
def active?
|
23
|
+
activated? && !deactivated?
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def activated?
|
29
|
+
return true if activated_at.nil?
|
30
|
+
|
31
|
+
activated_at <= Time.current
|
32
|
+
end
|
33
|
+
|
34
|
+
def deactivated?
|
35
|
+
return false if deactivated_at.nil?
|
36
|
+
|
37
|
+
deactivated_at <= Time.current
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Flipside
|
4
|
+
# This class piggybacks on the Role class. They both need the same data, so
|
5
|
+
# it feels a bit unnecessary to have two similar db tables.
|
6
|
+
class Key < Role
|
7
|
+
KEY_CLASS = "_FlipsideKey_"
|
8
|
+
|
9
|
+
after_initialize { self.class_name = KEY_CLASS }
|
10
|
+
attr_readonly :class_name
|
11
|
+
|
12
|
+
alias_attribute :key, :method
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "models/flipside/key"
|
4
|
+
|
5
|
+
module Flipside
|
6
|
+
module Keys
|
7
|
+
def self.included(base)
|
8
|
+
validate_roles! base
|
9
|
+
base.add_check { |key| enabled_for? key.to_s }
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.validate_roles!(base)
|
13
|
+
return if base.ancestors.include? Roles
|
14
|
+
raise "Internal error in Flipside: Roles module has not been loaded"
|
15
|
+
end
|
16
|
+
|
17
|
+
def enabled_for?(key)
|
18
|
+
return false if key.blank?
|
19
|
+
|
20
|
+
roles.where(
|
21
|
+
class_name: Key::KEY_CLASS,
|
22
|
+
method: key
|
23
|
+
).exists?
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "models/flipside/role"
|
4
|
+
|
5
|
+
module Flipside
|
6
|
+
module Roles
|
7
|
+
def self.included(base)
|
8
|
+
base.has_many :roles, foreign_key: :feature_id
|
9
|
+
base.add_check { |object| has_role? object }
|
10
|
+
end
|
11
|
+
|
12
|
+
def has_role?(object)
|
13
|
+
return false unless object
|
14
|
+
|
15
|
+
methods = lookup_methods_for(object)
|
16
|
+
methods.any? { |method| object.public_send(method) }
|
17
|
+
end
|
18
|
+
|
19
|
+
def lookup_methods_for(object)
|
20
|
+
roles
|
21
|
+
.where(class_name: object.class.to_s)
|
22
|
+
.pluck(:method)
|
23
|
+
.select { |method| object.respond_to?(method) }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require "flipside/config/registered_entity"
|
2
|
+
|
3
|
+
module Flipside
|
4
|
+
module Config
|
5
|
+
module Entities
|
6
|
+
def register_entity(class_name:, search_by:, display_as:, identified_by: :id)
|
7
|
+
registered_entities[class_name.to_s] = RegisteredEntity.new(
|
8
|
+
class_name:,
|
9
|
+
search_by:,
|
10
|
+
display_as:,
|
11
|
+
identified_by:
|
12
|
+
)
|
13
|
+
end
|
14
|
+
|
15
|
+
def entity_classes
|
16
|
+
registered_entities.keys
|
17
|
+
end
|
18
|
+
|
19
|
+
def search_entity(class_name:, query:)
|
20
|
+
registered_entities.fetch(class_name.to_s).search(query)
|
21
|
+
end
|
22
|
+
|
23
|
+
def find_entity(class_name:, identifier:)
|
24
|
+
registered_entities.fetch(class_name.to_s).find(identifier)
|
25
|
+
end
|
26
|
+
|
27
|
+
def display_entity(entity)
|
28
|
+
registered_entities
|
29
|
+
.fetch(entity.class.to_s)
|
30
|
+
.display(entity)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def registered_entities
|
36
|
+
@registered_entities ||= {}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require "flipside/search_result"
|
2
|
+
|
3
|
+
module Flipside
|
4
|
+
class RegisteredEntity
|
5
|
+
attr_reader :class_name, :search_by, :display_as, :identified_by
|
6
|
+
|
7
|
+
def initialize(class_name:, identified_by: :id, search_by: nil, display_as: nil)
|
8
|
+
@class_name = class_name
|
9
|
+
@search_by = search_by
|
10
|
+
@display_as = display_as
|
11
|
+
@identified_by = identified_by
|
12
|
+
end
|
13
|
+
|
14
|
+
def search(query)
|
15
|
+
Array(lookup_proc.call(query)).map do |entity|
|
16
|
+
SearchResult.new(
|
17
|
+
entity,
|
18
|
+
display(entity),
|
19
|
+
entity.public_send(identified_by)
|
20
|
+
)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def find(identifier)
|
25
|
+
class_name.constantize.find_by!("#{identified_by}": identifier)
|
26
|
+
end
|
27
|
+
|
28
|
+
def display(entity)
|
29
|
+
display_proc.call(entity)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def lookup_proc
|
35
|
+
@lookup_proc ||=
|
36
|
+
case search_by
|
37
|
+
when Proc
|
38
|
+
search_by
|
39
|
+
when Symbol
|
40
|
+
->(query) { class_name.constantize.where("#{search_by}": query) }
|
41
|
+
else
|
42
|
+
->(query) { class_name.constantize.where("#{identified_by}": query) }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def display_proc
|
47
|
+
@display_proc ||=
|
48
|
+
case display_as
|
49
|
+
when Proc
|
50
|
+
display_as
|
51
|
+
when Symbol
|
52
|
+
->(entity) { entity.public_send(display_as) }
|
53
|
+
when String
|
54
|
+
->(entity) { display_as }
|
55
|
+
else
|
56
|
+
->(entity) { entity.public_send(identified_by) }
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require "flipside/search_result"
|
2
|
+
|
3
|
+
module Flipside
|
4
|
+
class RegisteredRole
|
5
|
+
attr_reader :class_name, :method_name, :display_as
|
6
|
+
|
7
|
+
def initialize(class_name:, method_name:, display_as: nil)
|
8
|
+
@class_name = class_name
|
9
|
+
@method_name = method_name
|
10
|
+
@display_as = display_as || method_name.to_s
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_result
|
14
|
+
SearchResult.new(
|
15
|
+
nil,
|
16
|
+
display,
|
17
|
+
method_name
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
def match?(query)
|
22
|
+
query = query.to_s.downcase
|
23
|
+
method_name.to_s.downcase.include?(query) ||
|
24
|
+
display.to_s.downcase.include?(query)
|
25
|
+
end
|
26
|
+
|
27
|
+
def display
|
28
|
+
display_proc.call
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def display_proc
|
34
|
+
@display_proc ||=
|
35
|
+
case display_as
|
36
|
+
when Proc
|
37
|
+
display_as
|
38
|
+
else
|
39
|
+
-> { display_as }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require "flipside/config/registered_role"
|
2
|
+
|
3
|
+
module Flipside
|
4
|
+
module Config
|
5
|
+
module Roles
|
6
|
+
def register_role(class_name:, method_name:, display_as: nil)
|
7
|
+
registered_roles[class_name.to_s] ||= []
|
8
|
+
registered_roles[class_name.to_s] << RegisteredRole.new(
|
9
|
+
class_name:,
|
10
|
+
method_name:,
|
11
|
+
display_as:
|
12
|
+
)
|
13
|
+
end
|
14
|
+
|
15
|
+
def role_classes
|
16
|
+
registered_roles.keys
|
17
|
+
end
|
18
|
+
|
19
|
+
def search_role(class_name:, query:)
|
20
|
+
registered_roles.fetch(class_name.to_s).filter_map do |registered_role|
|
21
|
+
next unless registered_role.match? query
|
22
|
+
registered_role.to_result
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def registered_roles
|
29
|
+
@registered_roles ||= {}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
class FeaturePresenter
|
4
|
+
extend Forwardable
|
5
|
+
attr_reader :feature, :base_path
|
6
|
+
|
7
|
+
def_delegators :@feature, :name, :description, :enabled, :entities, :roles
|
8
|
+
|
9
|
+
def initialize(feature, base_path)
|
10
|
+
@feature = feature
|
11
|
+
@base_path = base_path
|
12
|
+
end
|
13
|
+
|
14
|
+
def href
|
15
|
+
File.join(base_path, "feature", name)
|
16
|
+
end
|
17
|
+
|
18
|
+
def toggle_path
|
19
|
+
File.join(href, "toggle")
|
20
|
+
end
|
21
|
+
|
22
|
+
def back_path
|
23
|
+
base_path
|
24
|
+
end
|
25
|
+
|
26
|
+
def entities_path
|
27
|
+
File.join(href, "entities")
|
28
|
+
end
|
29
|
+
|
30
|
+
def add_entity_path
|
31
|
+
File.join(href, "add_entity")
|
32
|
+
end
|
33
|
+
|
34
|
+
def remove_entity_path
|
35
|
+
File.join(href, "remove_entity")
|
36
|
+
end
|
37
|
+
|
38
|
+
def roles_path
|
39
|
+
File.join(href, "roles")
|
40
|
+
end
|
41
|
+
|
42
|
+
def add_role_path
|
43
|
+
File.join(href, "add_role")
|
44
|
+
end
|
45
|
+
|
46
|
+
def remove_role_path
|
47
|
+
File.join(href, "remove_role")
|
48
|
+
end
|
49
|
+
|
50
|
+
def status
|
51
|
+
if feature.active?
|
52
|
+
"active"
|
53
|
+
else
|
54
|
+
"inactive"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def status_color
|
59
|
+
if feature.active?
|
60
|
+
deactivates_soon? ? "bg-orange-600" : "bg-green-600"
|
61
|
+
elsif activates_soon?
|
62
|
+
"bg-yellow-600"
|
63
|
+
else
|
64
|
+
"bg-zinc-600"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def activated_at
|
69
|
+
feature.activated_at&.strftime("%Y-%m-%d %H:%M")
|
70
|
+
end
|
71
|
+
|
72
|
+
def deactivated_at
|
73
|
+
feature.deactivated_at&.strftime("%Y-%m-%d %H:%M")
|
74
|
+
end
|
75
|
+
|
76
|
+
def activates_soon?(period = 60 * 60 * 24)
|
77
|
+
return false if feature.active?
|
78
|
+
return false if feature.activated_at.nil?
|
79
|
+
|
80
|
+
feature.activated_at <= Time.now + period
|
81
|
+
end
|
82
|
+
|
83
|
+
def deactivates_soon?(period = 60 * 60 * 24)
|
84
|
+
return false unless feature.active?
|
85
|
+
return false if feature.deactivated_at.nil?
|
86
|
+
|
87
|
+
feature.deactivated_at <= Time.now + period
|
88
|
+
end
|
89
|
+
|
90
|
+
def entity_count_str
|
91
|
+
count = feature.entities.count
|
92
|
+
if count == 1
|
93
|
+
"Enabled for one entity"
|
94
|
+
elsif count.positive?
|
95
|
+
"Enabled for #{count} entities"
|
96
|
+
else
|
97
|
+
""
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def role_count_str
|
102
|
+
count = feature.roles.count
|
103
|
+
if count == 1
|
104
|
+
"Enabled for one role"
|
105
|
+
elsif count.positive?
|
106
|
+
"Enabled for #{count} roles"
|
107
|
+
else
|
108
|
+
""
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
<div data-controller="modal">
|
2
|
+
<button
|
3
|
+
class="p-2 rounded-lg hover:bg-slate-600"
|
4
|
+
data-action="click->modal#open">
|
5
|
+
<%= feature.public_send(attribute) || "Not set" %>
|
6
|
+
</button>
|
7
|
+
|
8
|
+
<dialog data-modal-target="dialog" class="hidden fixed inset-0 flex items-center justify-center bg-black/50">
|
9
|
+
<div class="flex flex-col bg-slate-500 rounded-lg shadow-lg w-full max-w-xl p-6">
|
10
|
+
<div class="flex justify-end space-x-2">
|
11
|
+
<button
|
12
|
+
type="button"
|
13
|
+
class="px-2 bg-gray-300 text-gray-700 rounded hover:bg-gray-400"
|
14
|
+
data-action="modal#close">
|
15
|
+
X
|
16
|
+
</button>
|
17
|
+
</div>
|
18
|
+
<div class="mt-4">
|
19
|
+
<form action="<%= feature.href %>" method="post">
|
20
|
+
<input type="hidden" name="_method" value="put" />
|
21
|
+
<%# <input type="hidden" name="feature_name" value="<%= feature.name %1>" /> %>
|
22
|
+
<label class="font-bold text-slate-800">Update <%= attribute %></label>
|
23
|
+
<input
|
24
|
+
type="datetime-local"
|
25
|
+
class="block mt-1 p-2 rounded-lg bg-slate-300 text-slate-600 border border-slate-400"
|
26
|
+
name="<%= attribute %>"
|
27
|
+
value="<%= feature.public_send(attribute) %>" />
|
28
|
+
<button
|
29
|
+
type="submit"
|
30
|
+
class="block mt-4 mx-auto p-2 rounded-lg text-slate-300 bg-blue-900 border border-blue-950 hover:bg-blue-800">
|
31
|
+
Save
|
32
|
+
</button>
|
33
|
+
</form>
|
34
|
+
</div>
|
35
|
+
</div>
|
36
|
+
</dialog>
|
37
|
+
</div>
|
@@ -0,0 +1,10 @@
|
|
1
|
+
<div class="p-4 grid grid-cols-12 gap-4 cursor-pointer text-slate-300">
|
2
|
+
<div class="col-span-3 text-left text-xl"><%= feature.name %></div>
|
3
|
+
<div class="col-span-4 text-left text-lg"><%= feature.description&.capitalize %></div>
|
4
|
+
<div class="col-span-4 text-right text-lg font-normal">
|
5
|
+
<span class="inline-block py-1 px-2 min-w-20 text-center rounded-lg <%= feature.status_color %>"><%= feature.status %></span>
|
6
|
+
</div>
|
7
|
+
<div class="text-right">
|
8
|
+
<%= erb :_toggle_button, locals: {feature:} %>
|
9
|
+
</div>
|
10
|
+
</div>
|
@@ -0,0 +1,9 @@
|
|
1
|
+
<% if result.present? %>
|
2
|
+
<% result.map do |r| %>
|
3
|
+
<option value="<%= r.identifier %>" class="text-slate-800 p-2 rounded-md hover:bg-slate-300">
|
4
|
+
<%= r.display_as %>
|
5
|
+
</option>
|
6
|
+
<% end %>
|
7
|
+
<% else %>
|
8
|
+
<div class="text-slate-800 p-2">No <%= class_name %> matched "<%= query %>"</div>
|
9
|
+
<% end %>
|
@@ -0,0 +1,12 @@
|
|
1
|
+
<% color = feature.enabled ? "bg-blue-700" : "bg-gray-200" %>
|
2
|
+
<% translate = feature.enabled ? "translate-x-5" : "translate-x-0" %>
|
3
|
+
|
4
|
+
<button type="button"
|
5
|
+
class="relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent <%= color %>"
|
6
|
+
data-controller="toggle"
|
7
|
+
data-action="click->toggle#switch:prevent"
|
8
|
+
data-toggle-url-value="<%= feature.toggle_path %>"
|
9
|
+
data-toggle-enabled-value="<%= feature.enabled %>"
|
10
|
+
>
|
11
|
+
<span aria-hidden="true" class="pointer-events-none inline-block size-5 <%= translate %> transform rounded-full bg-white shadow"></span>
|
12
|
+
</button>
|
@@ -0,0 +1,68 @@
|
|
1
|
+
<div class="m-12">
|
2
|
+
<a href="<%= feature.href %>" class="p-2 rounded-lg text-xl text-slate-300 bg-blue-900 border border-blue-950 hover:bg-blue-800">Back</a>
|
3
|
+
<div class="mt-12 w-1/3 m-auto bg-gray-800 text-slate-400 p-8 rounded-lg shadow-lg">
|
4
|
+
<h1 class="text-2xl font-bold mb-6">Entities for <span class="font-extrabold text-slate-300"><%= feature.name %></span></h1>
|
5
|
+
<h2 class="text-xl font-bold">Add entity</h2>
|
6
|
+
<form action="<%= feature.add_entity_path %>" method="post">
|
7
|
+
<div data-controller="search" data-search-url-value="/flipside/search_entity" class="w-full flex p-0 gap-2 items-center">
|
8
|
+
<select
|
9
|
+
name="class_name"
|
10
|
+
data-search-target="param"
|
11
|
+
data-search-param="class_name"
|
12
|
+
data-action="search#clearAll"
|
13
|
+
class="border text-slate-600 border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
|
14
|
+
<% Flipside.entity_classes.each do |class_name| %>
|
15
|
+
<option value="<%= class_name %>"><%= class_name %></option>
|
16
|
+
<% end %>
|
17
|
+
</select>
|
18
|
+
|
19
|
+
<input
|
20
|
+
type="hidden"
|
21
|
+
data-search-target="value"
|
22
|
+
name="identifier" />
|
23
|
+
<div class="ml-2 relative flex-grow">
|
24
|
+
<input
|
25
|
+
type="text"
|
26
|
+
autocomplete="off"
|
27
|
+
data-action="search#search"
|
28
|
+
data-search-target="input"
|
29
|
+
placeholder="Search..."
|
30
|
+
class="w-full border text-slate-600 border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" />
|
31
|
+
<div
|
32
|
+
class="mt-2 rounded-md absolute left-0 top-full w-full bg-slate-400 bg-border shadow-lg z-10"
|
33
|
+
data-search-target="results"
|
34
|
+
data-action="click->search#select" >
|
35
|
+
</div>
|
36
|
+
</div>
|
37
|
+
<button
|
38
|
+
type="submit"
|
39
|
+
disabled="true"
|
40
|
+
data-search-target="addButton"
|
41
|
+
class="ml-2 p-2 bg-gray-600 text-gray-700 font-semibold rounded" >
|
42
|
+
Add
|
43
|
+
</button>
|
44
|
+
</div>
|
45
|
+
</form>
|
46
|
+
|
47
|
+
<div class="mt-8">
|
48
|
+
<h2 class="text-xl font-bold">Enabled entities</h2>
|
49
|
+
<ul>
|
50
|
+
<% feature.entities.each do |entity| %>
|
51
|
+
<li class="mt-2 p-2 bg-slate-700 text-slate-300 border border-slate-600 rounded-lg">
|
52
|
+
<div class="flex justify-between items-center">
|
53
|
+
<div>
|
54
|
+
<%= Flipside.display_entity(entity.flippable) %> (<%= entity.flippable_type %>)
|
55
|
+
</div>
|
56
|
+
<div>
|
57
|
+
<form action="<%= feature.remove_entity_path %>" method="post">
|
58
|
+
<input type="hidden" name="entity_id" value="<%= entity.id %>" />
|
59
|
+
<button type="submit" class="p-2 bg-gray-300 text-gray-700 font-semibold rounded hover:bg-gray-400">Remove</button>
|
60
|
+
</form>
|
61
|
+
</div>
|
62
|
+
</div>
|
63
|
+
</li>
|
64
|
+
<% end %>
|
65
|
+
</ul>
|
66
|
+
</div>
|
67
|
+
</div>
|
68
|
+
</div>
|
@@ -0,0 +1,68 @@
|
|
1
|
+
<div class="m-12">
|
2
|
+
<a href="<%= feature.href %>" class="p-2 rounded-lg text-xl text-slate-300 bg-blue-900 border border-blue-950 hover:bg-blue-800">Back</a>
|
3
|
+
<div class="mt-12 w-1/3 m-auto bg-gray-800 text-slate-400 p-8 rounded-lg shadow-lg">
|
4
|
+
<h1 class="text-2xl font-bold mb-6">Roles for <span class="font-extrabold text-slate-300"><%= feature.name %></span></h1>
|
5
|
+
<h2 class="text-xl font-bold">Add Role</h2>
|
6
|
+
<form action="<%= feature.add_role_path %>" method="post">
|
7
|
+
<div data-controller="search" data-search-url-value="/flipside/search_role" class="w-full flex p-0 gap-2 items-center">
|
8
|
+
<select
|
9
|
+
name="class_name"
|
10
|
+
data-search-target="param"
|
11
|
+
data-search-param="class_name"
|
12
|
+
data-action="search#clearAll"
|
13
|
+
class="border text-slate-600 border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
|
14
|
+
<% Flipside.role_classes.each do |class_name| %>
|
15
|
+
<option value="<%= class_name %>"><%= class_name %></option>
|
16
|
+
<% end %>
|
17
|
+
</select>
|
18
|
+
|
19
|
+
<input
|
20
|
+
type="hidden"
|
21
|
+
data-search-target="value"
|
22
|
+
name="identifier" />
|
23
|
+
<div class="ml-2 relative flex-grow">
|
24
|
+
<input
|
25
|
+
type="text"
|
26
|
+
autocomplete="off"
|
27
|
+
data-action="search#search"
|
28
|
+
data-search-target="input"
|
29
|
+
placeholder="Search..."
|
30
|
+
class="w-full border text-slate-600 border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" />
|
31
|
+
<div
|
32
|
+
class="mt-2 rounded-md absolute left-0 top-full w-full bg-slate-400 bg-border shadow-lg z-10"
|
33
|
+
data-search-target="results"
|
34
|
+
data-action="click->search#select" >
|
35
|
+
</div>
|
36
|
+
</div>
|
37
|
+
<button
|
38
|
+
type="submit"
|
39
|
+
disabled="true"
|
40
|
+
data-search-target="addButton"
|
41
|
+
class="ml-2 p-2 bg-gray-600 text-gray-700 font-semibold rounded" >
|
42
|
+
Add
|
43
|
+
</button>
|
44
|
+
</div>
|
45
|
+
</form>
|
46
|
+
|
47
|
+
<div class="mt-8">
|
48
|
+
<h2 class="text-xl font-bold">Enabled Roles</h2>
|
49
|
+
<ul>
|
50
|
+
<% feature.roles.each do |role| %>
|
51
|
+
<li class="mt-2 p-2 bg-slate-700 text-slate-300 border border-slate-600 rounded-lg">
|
52
|
+
<div class="flex justify-between items-center">
|
53
|
+
<div>
|
54
|
+
<%= "#{role.class_name}##{role.method}" %>
|
55
|
+
</div>
|
56
|
+
<div>
|
57
|
+
<form action="<%= feature.remove_role_path %>" method="post">
|
58
|
+
<input type="hidden" name="role_id" value="<%= role.id %>" />
|
59
|
+
<button type="submit" class="p-2 bg-gray-300 text-gray-700 font-semibold rounded hover:bg-gray-400">Remove</button>
|
60
|
+
</form>
|
61
|
+
</div>
|
62
|
+
</div>
|
63
|
+
</li>
|
64
|
+
<% end %>
|
65
|
+
</ul>
|
66
|
+
</div>
|
67
|
+
</div>
|
68
|
+
</div>
|
@@ -0,0 +1,15 @@
|
|
1
|
+
<div class="mt-12 w-3/4 m-auto bg-gray-800 text-slate-400 p-8 pb-24 rounded-lg shadow-lg">
|
2
|
+
<h1 class="text-3xl text-center font-bold mb-6">Flipside feature flags</h1>
|
3
|
+
|
4
|
+
<div class="mt-12 w-3/4 m-auto">
|
5
|
+
<ul>
|
6
|
+
<% features.each do |feature| %>
|
7
|
+
<li class="border-b border-slate-500 hover:font-semibold">
|
8
|
+
<a href=<%= feature.href %>>
|
9
|
+
<%= erb :_feature_item, locals: {feature:} %>
|
10
|
+
</a>
|
11
|
+
</li>
|
12
|
+
<% end %>
|
13
|
+
</ul>
|
14
|
+
</div>
|
15
|
+
</div>
|
@@ -0,0 +1,25 @@
|
|
1
|
+
<!doctype html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<meta charset="UTF-8">
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
7
|
+
<script type="importmap">
|
8
|
+
{
|
9
|
+
"imports": {
|
10
|
+
"@hotwired/stimulus": "https://cdn.jsdelivr.net/npm/@hotwired/stimulus/dist/stimulus.js",
|
11
|
+
"toggle_controller": "/flipside/toggle_controller.js",
|
12
|
+
"search_controller": "/flipside/search_controller.js",
|
13
|
+
"modal_controller": "/flipside/modal_controller.js"
|
14
|
+
}
|
15
|
+
}
|
16
|
+
</script>
|
17
|
+
</head>
|
18
|
+
<body class="w-full h-full bg-slate-600">
|
19
|
+
<div id="main">
|
20
|
+
<%= yield %>
|
21
|
+
</div>
|
22
|
+
|
23
|
+
<script src="/flipside/index.js" type="module" ></script>
|
24
|
+
</body>
|
25
|
+
</html>
|
@@ -0,0 +1 @@
|
|
1
|
+
<h1 class="mt-12 text-center text-slate-300 text-3xl font-semibold">This resource could not be found!</h1>
|
@@ -0,0 +1,55 @@
|
|
1
|
+
<div class="m-12">
|
2
|
+
<a href="<%= feature.back_path %>" class="p-2 rounded-lg text-xl text-slate-300 bg-blue-900 border border-blue-950 hover:bg-blue-800">Back</a>
|
3
|
+
<div class="mt-12 w-1/2 m-auto bg-gray-800 text-slate-400 p-8 rounded-lg shadow-lg">
|
4
|
+
<h1 class="text-2xl font-bold mb-6">Feature Details</h1>
|
5
|
+
<div class="mt-4 grid grid-cols-1 gap-4">
|
6
|
+
<div class="pb-6 border-b border-gray-600">
|
7
|
+
<div class="mt-2 flex justify-between items-center">
|
8
|
+
<span class="font-semibold">Name:</span>
|
9
|
+
<span class="font-bold text-lg text-slate-300"><%= feature.name %></span>
|
10
|
+
</div>
|
11
|
+
<div class="mt-2 flex justify-between items-center">
|
12
|
+
<span class="font-semibold">Description:</span>
|
13
|
+
<span><%= feature.description %></span>
|
14
|
+
</div>
|
15
|
+
</div>
|
16
|
+
<div class="mt-2 flex justify-between items-center">
|
17
|
+
<span class="font-semibold">Enabled for all:</span>
|
18
|
+
<span><%= erb :_toggle_button, locals: {feature:} %></span>
|
19
|
+
</div>
|
20
|
+
<div class="mt-2 flex justify-between items-center">
|
21
|
+
<span class="font-semibold">Active From:</span>
|
22
|
+
<span>
|
23
|
+
<%= erb :_datetime_modal, locals: {feature:, attribute: :activated_at} %>
|
24
|
+
</span>
|
25
|
+
</div>
|
26
|
+
<div class="mt-2 flex justify-between items-center">
|
27
|
+
<span class="font-semibold">Active Until:</span>
|
28
|
+
<span>
|
29
|
+
<%= erb :_datetime_modal, locals: {feature:, attribute: :deactivated_at} %>
|
30
|
+
</span>
|
31
|
+
</div>
|
32
|
+
<div class="mt-2 grid grid-cols-3 items-center">
|
33
|
+
<span class="font-semibold">Entities:</span>
|
34
|
+
<span class="text-center"><%= feature.entity_count_str %></span>
|
35
|
+
<span class="text-right text-xl text-slate-400">
|
36
|
+
<a
|
37
|
+
href="<%= feature.entities_path %>"
|
38
|
+
class="px-2 py-1 rounded-lg bg-blue-900 border border-blue-950 hover:bg-blue-800" >
|
39
|
+
Edit
|
40
|
+
</a>
|
41
|
+
</span>
|
42
|
+
</div><div class="mt-2 grid grid-cols-3 items-center">
|
43
|
+
<span class="font-semibold">Roles:</span>
|
44
|
+
<span class="text-center"><%= feature.role_count_str %></span>
|
45
|
+
<span class="text-right text-xl text-slate-400">
|
46
|
+
<a
|
47
|
+
href="<%= feature.roles_path %>"
|
48
|
+
class="px-2 py-1 rounded-lg bg-blue-900 border border-blue-950 hover:bg-blue-800" >
|
49
|
+
Edit
|
50
|
+
</a>
|
51
|
+
</span>
|
52
|
+
</div>
|
53
|
+
</div>
|
54
|
+
</div>
|
55
|
+
</div>
|
data/lib/flipside/web.rb
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
require "sinatra"
|
2
|
+
require "uri"
|
3
|
+
require "flipside/feature_presenter"
|
4
|
+
|
5
|
+
module Flipside
|
6
|
+
class Web < Sinatra::Base
|
7
|
+
not_found do
|
8
|
+
erb :not_found
|
9
|
+
end
|
10
|
+
|
11
|
+
get "/" do
|
12
|
+
features = Flipside::Feature.order(:name).map do |feature|
|
13
|
+
FeaturePresenter.new(feature, base_path)
|
14
|
+
end
|
15
|
+
|
16
|
+
erb :index, locals: {features:}
|
17
|
+
end
|
18
|
+
|
19
|
+
get "/feature/:name" do
|
20
|
+
erb :show, locals: {feature: FeaturePresenter.new(feature, base_path)}
|
21
|
+
end
|
22
|
+
|
23
|
+
put "/feature/:name/toggle" do
|
24
|
+
content_type :text
|
25
|
+
|
26
|
+
if feature.update(enabled: !feature.enabled)
|
27
|
+
204
|
28
|
+
else
|
29
|
+
[422, "Failed to update feature"]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
put "/feature/:name" do
|
34
|
+
kwargs = params.slice("activated_at", "deactivated_at")
|
35
|
+
feature.update(**kwargs)
|
36
|
+
redirect to("/feature/#{params["name"]}"), 303
|
37
|
+
end
|
38
|
+
|
39
|
+
get "/feature/:name/entities" do
|
40
|
+
erb :feature_entities, locals: {feature: FeaturePresenter.new(feature, base_path)}
|
41
|
+
end
|
42
|
+
|
43
|
+
get '/search_entity' do
|
44
|
+
class_name = params[:class_name]
|
45
|
+
query = URI.decode_www_form_component(params[:q])
|
46
|
+
result = Flipside.search_entity(class_name:, query:)
|
47
|
+
|
48
|
+
erb :_search_result, locals: {result:, class_name:, query:}
|
49
|
+
end
|
50
|
+
|
51
|
+
post "/feature/:name/add_entity" do
|
52
|
+
name, class_name, identifier = params.values_at("name", "class_name", "identifier")
|
53
|
+
|
54
|
+
entity = Flipside.find_entity(class_name:, identifier:)
|
55
|
+
Flipside.add_entity(name: , entity:)
|
56
|
+
redirect to("/feature/#{name}/entities"), 303
|
57
|
+
end
|
58
|
+
|
59
|
+
post "/feature/:name/remove_entity" do
|
60
|
+
Flipside.remove_entity(name: params["name"], entity_id: params["entity_id"])
|
61
|
+
redirect to("/feature/#{params["name"]}/entities"), 303
|
62
|
+
end
|
63
|
+
|
64
|
+
get "/feature/:name/roles" do
|
65
|
+
erb :feature_roles, locals: {feature: FeaturePresenter.new(feature, base_path)}
|
66
|
+
end
|
67
|
+
|
68
|
+
get '/search_role' do
|
69
|
+
class_name = params[:class_name]
|
70
|
+
query = URI.decode_www_form_component(params[:q])
|
71
|
+
result = Flipside.search_role(class_name:, query:)
|
72
|
+
|
73
|
+
erb :_search_result, locals: {result:, class_name:, query:}
|
74
|
+
end
|
75
|
+
|
76
|
+
post "/feature/:name/add_role" do
|
77
|
+
name, class_name, method_name = params.values_at("name", "class_name", "identifier")
|
78
|
+
Flipside.add_role(name:, class_name:, method_name:)
|
79
|
+
|
80
|
+
redirect to("/feature/#{name}/roles"), 303
|
81
|
+
end
|
82
|
+
|
83
|
+
post "/feature/:name/remove_role" do
|
84
|
+
Flipside.remove_role(name: params["name"], role_id: params["role_id"])
|
85
|
+
|
86
|
+
redirect to("/feature/#{params["name"]}/roles"), 303
|
87
|
+
end
|
88
|
+
|
89
|
+
def feature
|
90
|
+
@feature ||= Flipside::Feature.find_by!(name: params["name"])
|
91
|
+
rescue
|
92
|
+
halt 404, "This feature does not exist"
|
93
|
+
end
|
94
|
+
|
95
|
+
def base_path
|
96
|
+
@base_path ||= request.script_name
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
data/lib/flipside.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "flipside/version"
|
4
|
+
require "flipside/web"
|
5
|
+
require "flipside/config/entities"
|
6
|
+
require "flipside/config/roles"
|
7
|
+
require "models/flipside/feature"
|
8
|
+
|
9
|
+
module Flipside
|
10
|
+
extend Config::Entities
|
11
|
+
extend Config::Roles
|
12
|
+
|
13
|
+
class Error < StandardError; end
|
14
|
+
|
15
|
+
class NoSuchFeauture < Error
|
16
|
+
def initialize(name)
|
17
|
+
super("There's no feature named '#{name}'")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class << self
|
22
|
+
def enabled?(name, object = nil)
|
23
|
+
feature = find_by(name:)
|
24
|
+
return false unless feature
|
25
|
+
|
26
|
+
feature.enabled? object
|
27
|
+
end
|
28
|
+
|
29
|
+
def enable!(name)
|
30
|
+
feature = find_by!(name:)
|
31
|
+
feature.update(enabled: true)
|
32
|
+
end
|
33
|
+
|
34
|
+
def add_entity(name:, entity:)
|
35
|
+
feature = find_by!(name:)
|
36
|
+
Entity.find_or_create_by(feature:, flippable: entity)
|
37
|
+
end
|
38
|
+
|
39
|
+
def remove_entity(name:, entity_id:)
|
40
|
+
feature = find_by!(name:)
|
41
|
+
feature.entities.find_by(id: entity_id)&.destroy
|
42
|
+
end
|
43
|
+
|
44
|
+
def add_role(name:, class_name:, method_name:)
|
45
|
+
feature = find_by!(name:)
|
46
|
+
Role.find_or_create_by(feature:, class_name:, method: method_name)
|
47
|
+
end
|
48
|
+
|
49
|
+
def remove_role(name:, role_id:)
|
50
|
+
feature = find_by!(name:)
|
51
|
+
feature.roles.find_by(id: role_id)&.destroy
|
52
|
+
end
|
53
|
+
|
54
|
+
def find_by(name:)
|
55
|
+
Feature.find_by(name:)
|
56
|
+
end
|
57
|
+
|
58
|
+
def find_by!(name:)
|
59
|
+
find_by(name:) || raise(NoSuchFeauture.new(name))
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# require "rails/generators"
|
2
|
+
require "rails/generators/active_record"
|
3
|
+
|
4
|
+
module Flipside
|
5
|
+
module Generators
|
6
|
+
class InstallGenerator < Rails::Generators::Base
|
7
|
+
include ActiveRecord::Generators::Migration
|
8
|
+
|
9
|
+
# Makes this generator available as "rails generate feature_flags:install"
|
10
|
+
source_root File.expand_path("templates", __dir__)
|
11
|
+
|
12
|
+
def create_migration_file
|
13
|
+
# Check if the migration file already exists to avoid duplicates
|
14
|
+
unless ActiveRecord::Base.connection.table_exists?("flipside")
|
15
|
+
migration_template "20241122_create_flipside_migration.rb", "db/migrate/create_flipside_migration.rb"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
class CreateFlipsideMigration < ActiveRecord::Migration[6.1]
|
2
|
+
def change
|
3
|
+
create_table :flipside_features do |t|
|
4
|
+
t.string :name, null: false
|
5
|
+
t.string :description
|
6
|
+
t.boolean :enabled, default: false, null: false
|
7
|
+
t.datetime :activated_at
|
8
|
+
t.datetime :deactivated_at
|
9
|
+
t.timestamps
|
10
|
+
end
|
11
|
+
|
12
|
+
create_table :flipside_entities do |t|
|
13
|
+
t.belongs_to :feature, null: false
|
14
|
+
t.bigint :flippable_id, null: false
|
15
|
+
t.string :flippable_type, null: false
|
16
|
+
t.timestamps
|
17
|
+
end
|
18
|
+
|
19
|
+
create_table :flipside_roles do |t|
|
20
|
+
t.belongs_to :feature, null: false
|
21
|
+
t.string :class_name, null: false
|
22
|
+
t.string :method, null: false
|
23
|
+
t.timestamps
|
24
|
+
end
|
25
|
+
|
26
|
+
add_index :flipside_features, :name, unique: true
|
27
|
+
end
|
28
|
+
end
|
metadata
ADDED
@@ -0,0 +1,146 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: flipside
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Sammy Henningsson
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-01-25 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '6.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '6.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: sinatra
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: byebug
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: sqlite3
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '2.1'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '2.1'
|
83
|
+
description: Create simple feature toggles.
|
84
|
+
email:
|
85
|
+
- sammy.henningsson@apoex.se
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- CHANGELOG.md
|
91
|
+
- LICENSE.txt
|
92
|
+
- README.md
|
93
|
+
- app/models/flipside/checks.rb
|
94
|
+
- app/models/flipside/entities.rb
|
95
|
+
- app/models/flipside/entity.rb
|
96
|
+
- app/models/flipside/feature.rb
|
97
|
+
- app/models/flipside/key.rb
|
98
|
+
- app/models/flipside/keys.rb
|
99
|
+
- app/models/flipside/role.rb
|
100
|
+
- app/models/flipside/roles.rb
|
101
|
+
- lib/flipside.rb
|
102
|
+
- lib/flipside/config/entities.rb
|
103
|
+
- lib/flipside/config/registered_entity.rb
|
104
|
+
- lib/flipside/config/registered_role.rb
|
105
|
+
- lib/flipside/config/roles.rb
|
106
|
+
- lib/flipside/feature_presenter.rb
|
107
|
+
- lib/flipside/search_result.rb
|
108
|
+
- lib/flipside/version.rb
|
109
|
+
- lib/flipside/views/_datetime_modal.erb
|
110
|
+
- lib/flipside/views/_feature_item.erb
|
111
|
+
- lib/flipside/views/_search_result.erb
|
112
|
+
- lib/flipside/views/_toggle_button.erb
|
113
|
+
- lib/flipside/views/feature_entities.erb
|
114
|
+
- lib/flipside/views/feature_roles.erb
|
115
|
+
- lib/flipside/views/index.erb
|
116
|
+
- lib/flipside/views/layout.erb
|
117
|
+
- lib/flipside/views/not_found.erb
|
118
|
+
- lib/flipside/views/show.erb
|
119
|
+
- lib/flipside/web.rb
|
120
|
+
- lib/generators/flipside/install/install_generator.rb
|
121
|
+
- lib/generators/flipside/install/templates/20241122_create_flipside_migration.rb
|
122
|
+
homepage:
|
123
|
+
licenses:
|
124
|
+
- MIT
|
125
|
+
metadata: {}
|
126
|
+
post_install_message:
|
127
|
+
rdoc_options: []
|
128
|
+
require_paths:
|
129
|
+
- app
|
130
|
+
- lib
|
131
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
132
|
+
requirements:
|
133
|
+
- - ">="
|
134
|
+
- !ruby/object:Gem::Version
|
135
|
+
version: 3.0.0
|
136
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
137
|
+
requirements:
|
138
|
+
- - ">="
|
139
|
+
- !ruby/object:Gem::Version
|
140
|
+
version: '0'
|
141
|
+
requirements: []
|
142
|
+
rubygems_version: 3.5.11
|
143
|
+
signing_key:
|
144
|
+
specification_version: 4
|
145
|
+
summary: Feature flags.
|
146
|
+
test_files: []
|