flipflop 2.2.1 → 2.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -13
- data/.gitignore +0 -1
- data/.travis.yml +23 -27
- data/CHANGES.md +7 -0
- data/Gemfile +14 -8
- data/README.md +27 -1
- data/Rakefile +20 -8
- data/app/controllers/flipflop/features_controller.rb +20 -10
- data/app/controllers/flipflop/strategies_controller.rb +4 -4
- data/app/views/flipflop/features/index.html.erb +64 -41
- data/app/views/flipflop/stylesheets/_flipflop.css +1 -0
- data/app/views/layouts/flipflop.html.erb +8 -1
- data/config/features.rb +7 -0
- data/config/locales/en.yml +34 -0
- data/flipflop.gemspec +0 -5
- data/lib/flipflop/configurable.rb +10 -0
- data/lib/flipflop/engine.rb +10 -15
- data/lib/flipflop/feature_definition.rb +5 -6
- data/lib/flipflop/feature_loader.rb +40 -0
- data/lib/flipflop/feature_set.rb +7 -9
- data/lib/flipflop/group_definition.rb +11 -0
- data/lib/flipflop/strategies/abstract_strategy.rb +2 -1
- data/lib/flipflop/version.rb +1 -1
- data/lib/flipflop.rb +2 -0
- data/lib/test_engine/config/features.rb +3 -0
- data/lib/test_engine/test_engine.rb +7 -0
- data/src/stylesheets/_flipflop.scss +160 -0
- data/test/integration/app_test.rb +50 -21
- data/test/integration/dashboard_test.rb +395 -52
- data/test/templates/nl.yml +30 -0
- data/test/templates/test_app_features.rb +7 -0
- data/test/templates/test_engine.rb +7 -0
- data/test/templates/test_engine_features.rb +3 -0
- data/test/test_helper.rb +83 -10
- data/test/unit/configurable_test.rb +1 -1
- data/test/unit/feature_definition_test.rb +27 -2
- data/test/unit/feature_set_test.rb +7 -19
- data/test/unit/flipflop_test.rb +16 -11
- data/test/unit/group_definition_test.rb +21 -0
- data/test/unit/strategies/abstract_strategy_test.rb +14 -25
- data/test/unit/strategies/active_record_strategy_test.rb +4 -0
- data/test/unit/strategies/cookie_strategy_test.rb +4 -0
- data/test/unit/strategies/default_strategy_test.rb +10 -4
- data/test/unit/strategies/lambda_strategy_test.rb +11 -27
- data/test/unit/strategies/query_string_strategy_test.rb +4 -0
- data/test/unit/strategies/redis_strategy_test.rb +4 -0
- data/test/unit/strategies/session_strategy_test.rb +4 -0
- data/test/unit/strategies/test_strategy_test.rb +4 -0
- metadata +22 -9
- data/app/assets/stylesheets/flipflop.css +0 -1
@@ -0,0 +1,34 @@
|
|
1
|
+
en:
|
2
|
+
flipflop:
|
3
|
+
title: "%{application} Features"
|
4
|
+
feature: "Feature"
|
5
|
+
description: "Description"
|
6
|
+
|
7
|
+
feature_states:
|
8
|
+
enabled: "on"
|
9
|
+
disabled: "off"
|
10
|
+
|
11
|
+
clear: "clear"
|
12
|
+
|
13
|
+
# You can optionally translate the names of the chosen features, feature
|
14
|
+
# groups and strategies:
|
15
|
+
|
16
|
+
groups:
|
17
|
+
default: "Other features"
|
18
|
+
|
19
|
+
# groups:
|
20
|
+
# design_improvements: "Design improvements"
|
21
|
+
|
22
|
+
# features:
|
23
|
+
# world_domination: "World domination"
|
24
|
+
# world_domination_description: "Take over the world!"
|
25
|
+
|
26
|
+
# strategies:
|
27
|
+
# cookie: "Cookie"
|
28
|
+
# active_record: "Active record"
|
29
|
+
# default: "Default"
|
30
|
+
# lambda: "Lambda"
|
31
|
+
# query_string: "Query string"
|
32
|
+
# redis: "Redis"
|
33
|
+
# session: "Session"
|
34
|
+
# test: "Test"
|
data/flipflop.gemspec
CHANGED
@@ -14,12 +14,7 @@ Gem::Specification.new do |s|
|
|
14
14
|
s.license = "MIT"
|
15
15
|
|
16
16
|
s.add_dependency("activesupport", ">= 4.0")
|
17
|
-
if ENV["CONTINUOUS_INTEGRATION"]
|
18
|
-
s.files = `git ls-files`.split("\n").map { |f| f.sub(".scss", ".css") }
|
19
|
-
else
|
20
|
-
s.add_dependency("bootstrap", "= 4.0.0.alpha3")
|
21
17
|
s.files = `git ls-files`.split("\n")
|
22
|
-
end
|
23
18
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
24
19
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
25
20
|
s.require_paths = ["lib"]
|
@@ -1,6 +1,16 @@
|
|
1
1
|
module Flipflop
|
2
2
|
module Configurable
|
3
|
+
attr_accessor :current_group
|
4
|
+
|
5
|
+
def group(group)
|
6
|
+
self.current_group = GroupDefinition.new(group)
|
7
|
+
yield
|
8
|
+
ensure
|
9
|
+
self.current_group = nil
|
10
|
+
end
|
11
|
+
|
3
12
|
def feature(feature, **options)
|
13
|
+
options = options.merge(group: current_group)
|
4
14
|
feature = FeatureDefinition.new(feature, **options)
|
5
15
|
FeatureSet.current.add(feature)
|
6
16
|
end
|
data/lib/flipflop/engine.rb
CHANGED
@@ -4,23 +4,24 @@ module Flipflop
|
|
4
4
|
|
5
5
|
isolate_namespace Flipflop
|
6
6
|
|
7
|
+
# The following middleware needs to be inserted for this engine, because it
|
8
|
+
# may not be available in Rails API only apps.
|
9
|
+
middleware.use Rack::MethodOverride
|
10
|
+
middleware.use ActionDispatch::Cookies
|
11
|
+
|
7
12
|
config.app_middleware.insert_after ActionDispatch::Callbacks,
|
8
13
|
FeatureCache::Middleware
|
9
14
|
|
10
15
|
config.flipflop = ActiveSupport::OrderedOptions.new
|
11
16
|
|
12
|
-
initializer "flipflop.assets" do |app|
|
13
|
-
config.assets.precompile += ["flipflop.css"]
|
14
|
-
end
|
15
|
-
|
16
17
|
initializer "flipflop.features_path" do |app|
|
17
|
-
|
18
|
+
FeatureLoader.current.append(app)
|
18
19
|
end
|
19
20
|
|
20
|
-
initializer "flipflop.
|
21
|
-
app.reloaders.push(
|
21
|
+
initializer "flipflop.features_loader" do |app|
|
22
|
+
app.reloaders.push(FeatureLoader.current)
|
22
23
|
to_prepare do
|
23
|
-
|
24
|
+
FeatureLoader.current.execute
|
24
25
|
end
|
25
26
|
end
|
26
27
|
|
@@ -37,6 +38,7 @@ module Flipflop
|
|
37
38
|
initializer "flipflop.request_interceptor" do |app|
|
38
39
|
interceptor = Strategies::AbstractStrategy::RequestInterceptor
|
39
40
|
ActionController::Base.send(:include, interceptor)
|
41
|
+
ActionController::API.send(:include, interceptor) if defined?(ActionController::API)
|
40
42
|
end
|
41
43
|
|
42
44
|
def run_tasks_blocks(app)
|
@@ -47,13 +49,6 @@ module Flipflop
|
|
47
49
|
|
48
50
|
private
|
49
51
|
|
50
|
-
def feature_reloader(app)
|
51
|
-
features = app.paths["config/features.rb"].existent
|
52
|
-
ActiveSupport::FileUpdateChecker.new(features) do
|
53
|
-
features.each { |path| load(path) }
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
52
|
def to_prepare
|
58
53
|
klass = defined?(ActiveSupport::Reloader) ? ActiveSupport::Reloader : ActionDispatch::Reloader
|
59
54
|
klass.to_prepare(&Proc.new)
|
@@ -1,15 +1,14 @@
|
|
1
1
|
module Flipflop
|
2
2
|
class FeatureDefinition
|
3
|
-
attr_reader :key, :default, :
|
3
|
+
attr_reader :key, :name, :title, :description, :default, :group
|
4
4
|
|
5
5
|
def initialize(key, **options)
|
6
6
|
@key = key
|
7
|
+
@name = @key.to_s.freeze
|
8
|
+
@title = @name.humanize.freeze
|
9
|
+
@description = options.delete(:description).freeze
|
7
10
|
@default = !!options.delete(:default) || false
|
8
|
-
@
|
9
|
-
end
|
10
|
-
|
11
|
-
def name
|
12
|
-
key.to_s
|
11
|
+
@group = options.delete(:group).freeze
|
13
12
|
end
|
14
13
|
end
|
15
14
|
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Flipflop
|
2
|
+
class FeatureLoader
|
3
|
+
@@lock = Monitor.new
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def current
|
7
|
+
@current or @@lock.synchronize { @current ||= new }
|
8
|
+
end
|
9
|
+
|
10
|
+
private :new
|
11
|
+
end
|
12
|
+
|
13
|
+
extend Forwardable
|
14
|
+
delegate [:execute, :updated?] => :checker
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
@paths = []
|
18
|
+
end
|
19
|
+
|
20
|
+
def append(engine)
|
21
|
+
@paths.concat(engine.paths.add("config/features.rb".freeze).existent)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def checker
|
27
|
+
@checker or @@lock.synchronize do
|
28
|
+
@checker ||= ActiveSupport::FileUpdateChecker.new(@paths) { reload! }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def reload!
|
33
|
+
@@lock.synchronize do
|
34
|
+
Flipflop::FeatureSet.current.replace do
|
35
|
+
@paths.each { |path| Kernel.load(path) }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/flipflop/feature_set.rb
CHANGED
@@ -28,21 +28,19 @@ module Flipflop
|
|
28
28
|
end
|
29
29
|
|
30
30
|
def configure
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
extend Configurable
|
35
|
-
instance_exec(&Proc.new)
|
36
|
-
end
|
37
|
-
@features.freeze
|
38
|
-
@strategies.freeze
|
31
|
+
Module.new do
|
32
|
+
extend Configurable
|
33
|
+
instance_exec(&Proc.new)
|
39
34
|
end
|
40
35
|
self
|
41
36
|
end
|
42
37
|
|
43
|
-
def
|
38
|
+
def replace
|
44
39
|
@@lock.synchronize do
|
45
40
|
initialize
|
41
|
+
yield if block_given?
|
42
|
+
@features.freeze
|
43
|
+
@strategies.freeze
|
46
44
|
end
|
47
45
|
self
|
48
46
|
end
|
@@ -35,7 +35,7 @@ module Flipflop
|
|
35
35
|
end
|
36
36
|
end
|
37
37
|
|
38
|
-
attr_reader :key, :name, :description
|
38
|
+
attr_reader :key, :name, :title, :description
|
39
39
|
|
40
40
|
def initialize(**options)
|
41
41
|
# Generate key before setting instance that should be excluded from
|
@@ -43,6 +43,7 @@ module Flipflop
|
|
43
43
|
@key = OptionsHasher.new(self).generate
|
44
44
|
|
45
45
|
@name = (options.delete(:name) || self.class.default_name).freeze
|
46
|
+
@title = @name.humanize.freeze
|
46
47
|
@description = (options.delete(:description) || self.class.default_description).freeze
|
47
48
|
@hidden = !!options.delete(:hidden) || false
|
48
49
|
|
data/lib/flipflop/version.rb
CHANGED
data/lib/flipflop.rb
CHANGED
@@ -8,7 +8,9 @@ require "flipflop/configurable"
|
|
8
8
|
require "flipflop/facade"
|
9
9
|
require "flipflop/feature_cache"
|
10
10
|
require "flipflop/feature_definition"
|
11
|
+
require "flipflop/feature_loader"
|
11
12
|
require "flipflop/feature_set"
|
13
|
+
require "flipflop/group_definition"
|
12
14
|
|
13
15
|
require "flipflop/strategies/abstract_strategy"
|
14
16
|
require "flipflop/strategies/options_hasher"
|
@@ -0,0 +1,160 @@
|
|
1
|
+
$enable-transitions: true;
|
2
|
+
$grid-gutter-width-base: 0;
|
3
|
+
|
4
|
+
@import "bootstrap/variables";
|
5
|
+
@import "bootstrap/mixins";
|
6
|
+
@import "bootstrap/normalize";
|
7
|
+
@import "bootstrap/print";
|
8
|
+
|
9
|
+
@import "bootstrap/reboot";
|
10
|
+
@import "bootstrap/type";
|
11
|
+
@import "bootstrap/tables";
|
12
|
+
@import "bootstrap/buttons";
|
13
|
+
|
14
|
+
@import "bootstrap/button-group";
|
15
|
+
@import "bootstrap/badge";
|
16
|
+
|
17
|
+
@import "bootstrap/utilities/spacing";
|
18
|
+
|
19
|
+
section.flipflop {
|
20
|
+
@include make-container(); /* @extend .container-fluid; */
|
21
|
+
margin: 5rem 0 0;
|
22
|
+
|
23
|
+
h1 {
|
24
|
+
padding: 0 1.4rem;
|
25
|
+
}
|
26
|
+
|
27
|
+
table {
|
28
|
+
@extend .table;
|
29
|
+
|
30
|
+
td:first-child {
|
31
|
+
padding-left: 1.5rem;
|
32
|
+
}
|
33
|
+
|
34
|
+
td:last-child {
|
35
|
+
padding-right: 1.5rem;
|
36
|
+
}
|
37
|
+
|
38
|
+
thead {
|
39
|
+
@extend .thead-inverse;
|
40
|
+
|
41
|
+
tr {
|
42
|
+
th {
|
43
|
+
position: relative;
|
44
|
+
cursor: default;
|
45
|
+
|
46
|
+
&[data-tooltip]:before, &[data-tooltip]:after {
|
47
|
+
@include transition(all 0.2s ease-out);
|
48
|
+
transform: translateY(0.2rem) translateZ(0);
|
49
|
+
opacity: 0;
|
50
|
+
|
51
|
+
display: block;
|
52
|
+
position: absolute;
|
53
|
+
}
|
54
|
+
|
55
|
+
&[data-tooltip]:before {
|
56
|
+
content: attr(data-tooltip);
|
57
|
+
width: 98%;
|
58
|
+
left: 0;
|
59
|
+
bottom: 3.75rem;
|
60
|
+
margin: 0;
|
61
|
+
padding: 0.5rem 0.75rem;
|
62
|
+
background: $gray;
|
63
|
+
border-radius: 0.2rem;
|
64
|
+
font-size: 0.875rem;
|
65
|
+
font-weight: normal;
|
66
|
+
pointer-events: none;
|
67
|
+
}
|
68
|
+
|
69
|
+
&[data-tooltip]:after {
|
70
|
+
content: " ";
|
71
|
+
width: 0;
|
72
|
+
height: 0;
|
73
|
+
left: 1rem;
|
74
|
+
bottom: 3.25rem;
|
75
|
+
border-left: solid transparent 0.5rem;
|
76
|
+
border-right: solid transparent 0.5rem;
|
77
|
+
border-top: solid $gray 0.5rem;
|
78
|
+
}
|
79
|
+
|
80
|
+
&:hover {
|
81
|
+
&[data-tooltip]:before, &[data-tooltip]:after {
|
82
|
+
transform: translateY(0) translateZ(0);
|
83
|
+
opacity: 1;
|
84
|
+
}
|
85
|
+
}
|
86
|
+
}
|
87
|
+
}
|
88
|
+
}
|
89
|
+
|
90
|
+
tbody {
|
91
|
+
tr {
|
92
|
+
td.status {
|
93
|
+
width: 2.2rem;
|
94
|
+
font-size: 1.1rem;
|
95
|
+
|
96
|
+
span.enabled, span.disabled {
|
97
|
+
width: 2rem;
|
98
|
+
@extend .badge;
|
99
|
+
@extend .badge-pill;
|
100
|
+
span {
|
101
|
+
width: auto;
|
102
|
+
text-align: center;
|
103
|
+
margin: 0 -2rem;
|
104
|
+
}
|
105
|
+
}
|
106
|
+
|
107
|
+
span.enabled { @extend .badge-success; }
|
108
|
+
span.disabled { @extend .badge-default; }
|
109
|
+
}
|
110
|
+
|
111
|
+
td.name {
|
112
|
+
min-width: 12rem;
|
113
|
+
padding-top: 0.9rem;
|
114
|
+
font-weight: bold;
|
115
|
+
}
|
116
|
+
|
117
|
+
td.description {
|
118
|
+
min-width: 12rem;
|
119
|
+
padding-top: 0.9rem;
|
120
|
+
}
|
121
|
+
|
122
|
+
td.toggle {
|
123
|
+
min-width: 10rem;
|
124
|
+
|
125
|
+
div.toolbar {
|
126
|
+
@extend .btn-toolbar;
|
127
|
+
margin-left: 0;
|
128
|
+
|
129
|
+
div.group {
|
130
|
+
@extend .mr-2;
|
131
|
+
@extend .btn-group;
|
132
|
+
@extend .btn-group-sm;
|
133
|
+
|
134
|
+
button {
|
135
|
+
@extend .btn;
|
136
|
+
@extend .btn-sm;
|
137
|
+
cursor: pointer;
|
138
|
+
&.active { @extend .btn-primary; }
|
139
|
+
&:not(.active) { @extend .btn-secondary; }
|
140
|
+
}
|
141
|
+
}
|
142
|
+
}
|
143
|
+
}
|
144
|
+
|
145
|
+
&.group {
|
146
|
+
background-color: $table-bg-accent;
|
147
|
+
|
148
|
+
td {
|
149
|
+
h2 {
|
150
|
+
margin: 0;
|
151
|
+
line-height: inherit;
|
152
|
+
font-size: 1.4rem;
|
153
|
+
font-weight: bold;
|
154
|
+
}
|
155
|
+
}
|
156
|
+
}
|
157
|
+
}
|
158
|
+
}
|
159
|
+
}
|
160
|
+
}
|
@@ -1,36 +1,65 @@
|
|
1
1
|
require File.expand_path("../../test_helper", __FILE__)
|
2
2
|
|
3
3
|
describe Flipflop do
|
4
|
-
|
5
|
-
|
6
|
-
|
4
|
+
describe "without engine" do
|
5
|
+
before do
|
6
|
+
@app = TestApp.new
|
7
|
+
end
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
|
9
|
+
after do
|
10
|
+
@app.unload!
|
11
|
+
end
|
11
12
|
|
12
|
-
|
13
|
-
|
14
|
-
|
13
|
+
subject do
|
14
|
+
@app
|
15
|
+
end
|
15
16
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
17
|
+
describe "middleware" do
|
18
|
+
it "should include cache middleware" do
|
19
|
+
middlewares = Rails.application.middleware.map(&:klass)
|
20
|
+
assert_includes middlewares, Flipflop::FeatureCache::Middleware
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "module" do
|
25
|
+
before do
|
26
|
+
Flipflop::FeatureSet.current.instance_variable_set(:@features, {})
|
27
|
+
Module.new do
|
28
|
+
extend Flipflop::Configurable
|
29
|
+
feature :world_domination
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should allow querying for app features" do
|
34
|
+
assert_equal false, Flipflop.world_domination?
|
35
|
+
end
|
20
36
|
end
|
21
37
|
end
|
22
38
|
|
23
|
-
describe "
|
39
|
+
describe "with engine" do
|
24
40
|
before do
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
41
|
+
@app = TestApp.new([
|
42
|
+
TestFeaturesGenerator,
|
43
|
+
TestEngineGenerator,
|
44
|
+
])
|
45
|
+
end
|
46
|
+
|
47
|
+
after do
|
48
|
+
@app.unload!
|
30
49
|
end
|
31
50
|
|
32
|
-
|
33
|
-
|
51
|
+
subject do
|
52
|
+
@app
|
53
|
+
end
|
54
|
+
|
55
|
+
describe "module" do
|
56
|
+
it "should allow querying for app features" do
|
57
|
+
assert_equal false, Flipflop.application_feature?
|
58
|
+
end
|
59
|
+
|
60
|
+
it "should allow querying for engine features" do
|
61
|
+
assert_equal false, Flipflop.engine_feature?
|
62
|
+
end
|
34
63
|
end
|
35
64
|
end
|
36
65
|
end
|