flipside 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|