flipflop 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +5 -0
  3. data/.travis.yml +51 -0
  4. data/Gemfile +20 -0
  5. data/LICENSE +22 -0
  6. data/README.md +261 -0
  7. data/Rakefile +16 -0
  8. data/app/assets/stylesheets/flipflop.scss +109 -0
  9. data/app/controllers/concerns/flipflop/environment_filters.rb +5 -0
  10. data/app/controllers/flipflop/features_controller.rb +59 -0
  11. data/app/controllers/flipflop/strategies_controller.rb +30 -0
  12. data/app/models/flipflop/feature.rb +3 -0
  13. data/app/views/flipflop/features/index.html.erb +60 -0
  14. data/app/views/layouts/flipflop.html.erb +1 -0
  15. data/config/routes.rb +5 -0
  16. data/flipflop.gemspec +23 -0
  17. data/lib/flipflop/configurable.rb +27 -0
  18. data/lib/flipflop/engine.rb +58 -0
  19. data/lib/flipflop/facade.rb +23 -0
  20. data/lib/flipflop/feature_cache.rb +64 -0
  21. data/lib/flipflop/feature_definition.rb +15 -0
  22. data/lib/flipflop/feature_set.rb +99 -0
  23. data/lib/flipflop/strategies/abstract_strategy.rb +103 -0
  24. data/lib/flipflop/strategies/active_record_strategy.rb +43 -0
  25. data/lib/flipflop/strategies/cookie_strategy.rb +44 -0
  26. data/lib/flipflop/strategies/default_strategy.rb +15 -0
  27. data/lib/flipflop/strategies/lambda_strategy.rb +25 -0
  28. data/lib/flipflop/strategies/query_string_strategy.rb +17 -0
  29. data/lib/flipflop/strategies/session_strategy.rb +29 -0
  30. data/lib/flipflop/strategies/test_strategy.rb +40 -0
  31. data/lib/flipflop/version.rb +3 -0
  32. data/lib/flipflop.rb +26 -0
  33. data/lib/generators/flipflop/features/USAGE +8 -0
  34. data/lib/generators/flipflop/features/features_generator.rb +7 -0
  35. data/lib/generators/flipflop/features/templates/features.rb +21 -0
  36. data/lib/generators/flipflop/install/install_generator.rb +21 -0
  37. data/lib/generators/flipflop/migration/USAGE +5 -0
  38. data/lib/generators/flipflop/migration/migration_generator.rb +23 -0
  39. data/lib/generators/flipflop/migration/templates/create_features.rb +10 -0
  40. data/lib/generators/flipflop/routes/USAGE +7 -0
  41. data/lib/generators/flipflop/routes/routes_generator.rb +5 -0
  42. data/test/integration/app_test.rb +32 -0
  43. data/test/integration/dashboard_test.rb +162 -0
  44. data/test/test_helper.rb +96 -0
  45. data/test/unit/configurable_test.rb +104 -0
  46. data/test/unit/feature_cache_test.rb +142 -0
  47. data/test/unit/feature_definition_test.rb +42 -0
  48. data/test/unit/feature_set_test.rb +136 -0
  49. data/test/unit/flipflop_test.rb +99 -0
  50. data/test/unit/strategies/abstract_strategy_request_test.rb +42 -0
  51. data/test/unit/strategies/abstract_strategy_test.rb +124 -0
  52. data/test/unit/strategies/active_record_strategy_test.rb +157 -0
  53. data/test/unit/strategies/cookie_strategy_test.rb +126 -0
  54. data/test/unit/strategies/default_strategy_test.rb +44 -0
  55. data/test/unit/strategies/lambda_strategy_test.rb +137 -0
  56. data/test/unit/strategies/query_string_strategy_test.rb +70 -0
  57. data/test/unit/strategies/session_strategy_test.rb +101 -0
  58. data/test/unit/strategies/test_strategy_test.rb +76 -0
  59. metadata +134 -0
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ Mjk5MWNkOGUyOGViMGIxOTJmZmY0NTc5NjU3Yjk3NjM0NWIyNDE1ZA==
5
+ data.tar.gz: !binary |-
6
+ NjRiOGU3YTg3ZDAyYmMxM2RmZmEzZDcyOTBlZTNjZmRlMWI4YTdiNA==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ YTRkOTdlMjAyMzVjYjdjOWJlMDkxOWYxZmYwMmUzMDMxYmE3YjcyNmI5ZWZk
10
+ MWMxYjY1ZTU2MTBiOWYxMDNlOGI0MDE2Y2RmYWQ3ZTZiMTUzNjE5ZDA2NmEw
11
+ YTViYjdmNzNkNTljZWFiMTUwOWZmNDhhMDgzYThiNGJmMTdlM2I=
12
+ data.tar.gz: !binary |-
13
+ MTllZjMwYjA5YWZmMzVjNGM3YjRmMGE0Mjk0NTg3MGY4NmY2MTgwNjcxOWVk
14
+ NzE3NzllOWQwZDgzN2FkYjg2YThiMThlMDI4ZGMzODk5ZjFiYTIzYzQ3MGU5
15
+ NDU0OTcxYjdjYzJkZWIwNTI5OTdkOWIxMGRjYmJkZDVmYmM4OTI=
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ tmp/*
data/.travis.yml ADDED
@@ -0,0 +1,51 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0
4
+ - 2.1
5
+ - 2.2
6
+ - 2.3.0
7
+ - jruby-9.0.5.0
8
+ - rbx-head
9
+ - ruby-head
10
+ - jruby-head
11
+ env:
12
+ - RAILS_VERSION=4.0
13
+ - RAILS_VERSION=4.1
14
+ - RAILS_VERSION=4.2
15
+ - RAILS_VERSION=5.0.0.beta3
16
+ - RAILS_VERSION=master
17
+ matrix:
18
+ exclude:
19
+ - rvm: 2.0
20
+ env: RAILS_VERSION=5.0.0.beta3
21
+ - rvm: 2.0
22
+ env: RAILS_VERSION=master
23
+ - rvm: 2.1
24
+ env: RAILS_VERSION=5.0.0.beta3
25
+ - rvm: 2.1
26
+ env: RAILS_VERSION=master
27
+ - rvm: 2.2
28
+ env: RAILS_VERSION=5.0.0.beta3
29
+ - rvm: 2.2
30
+ env: RAILS_VERSION=master
31
+ allow_failures:
32
+ - rvm: jruby-9.0.5.0
33
+ env: RAILS_VERSION=5.0.0.beta3
34
+ - rvm: jruby-9.0.5.0
35
+ env: RAILS_VERSION=master
36
+ - rvm: rbx-head
37
+ - rvm: ruby-head
38
+ - rvm: jruby-head
39
+ notifications:
40
+ email: false
41
+ slack:
42
+ secure: crOO1QGnFn9T1DpVgxkukTSiN8lQq09X8WF8oi1Eoa7Liex4gzWq7f8wlIXPrRFAsNjU47fSrz+L1C6Eg068Vd2df9pkqjW0pLNeqUViJ35TzpYISTtzJrX+x3nvCQLlvS4leP6lkijlsvlu1IN0xnXadW5cmcMoEcPo4Yma1RUklwlreSRIEmJiutYKVFRw3gIZA9vsnXNEcD408mvSY/8Kuw+hmRQupODUalXDpZo1q3HH+ZPQq+/rGuJ7XRf9sBtxjpUF0G4FJZQhVP4CrLNYVBE/83rHJ6HSf6u3SlYVIMiautq0nWpVLPHUrkOPJVeVh6EPtoFeI/cehH1NyoAVvL5a39wFRBlJ4jVPWUrrnihJT/6+P6GM9PSnYogxtIoTsdrYES2FgtWGgwG5uLyw8U6bW7G7rCzQwBP7enVHWVCbDgdSSjE1Mg1I9qhRuL6pHs5des4VKk6pfD3p+BRqLmOZR2jx4v8MFwakSFqQWOMxaD0U1lfxecqSx9OkwWEhCFSnHeXeHInEhY6qKCdZZzT+beYn0xppUMPJGMTqe5+po8gL+5MxQwI8Xs/5hSve5frfmuS7UQf5BnFMOzwoThQrXCFRz58wXvcZTD9eTdVBV44Hsi5OLdYn9K58sNUhgcxGfRgdE7Gy9P7DD4fHPGakD/Tz++HuCmCUXNs=
43
+ deploy:
44
+ provider: rubygems
45
+ api_key:
46
+ secure: ObuFc5QWnSgraKzYXLT5EhnlGm/+BQ2IN46q3ykXdk8m80ajpWD0/rqtCmu5SiBd6Z56CVZe2zSsEpGhN7/pC5kdG0hYzdpvbgX9IxJNMjYb7rNh2onXdIHVd3yk3qdNgI8hmrgtocJPdtPXqbZSY6KOSH6eb5rPDmA/bWwyREdicW5KN8ZWsXiLzKXoFZ6xvkNFbAjY8AicIfG+dHBFbx+q1xBOvbS44M+VIt/Qsknt9m5CYaWJ0AK/RZoyRhs0F0yYQSLu0mVt7JbHOL05SqQ3uCNlwiEqX+fIiN2mPb24Xxy6SVXr1kD0+H4EZHpgDeUlylO2myYJBFBnjzKXXr0qh3YTxaEKlWO7HnUdnJDRQ+bjyT0USBv7gXO2fEIBN864AGZf+3cL0aKRW3n3cyqrhiUxim7gro7eQSh6t6x5o+LGYw6dDdhR53XIFZoh0s/azYBbgl0bfM3juBMrD3e4w/ieaE9iI+NVj4Z4DYDRtbGIqxsuLwRmSkg3epvBc4Pa7vTDuCyVtd9lpFK55V1TysxhcPu0lqHf+SNnW3+DDwp+CQusQhFIsOcIQdr6PBNSTP5ZtEEqyEMfmt2QK2BEOYUuL6TxIUa9xuu8zMUahSaLOvekM5R+EwW04C3T5hWbPs7W1Ks2jcVzkL2iNd3I18pW9VvkXNAqveuoT28=
47
+ gem: flipflop
48
+ on:
49
+ tags: true
50
+ repo: voormedia/flipflop
51
+ ruby: 2.3.0
data/Gemfile ADDED
@@ -0,0 +1,20 @@
1
+ source "https://rubygems.org"
2
+ gemspec
3
+
4
+ group :test do
5
+ version = ENV["RAILS_VERSION"] || "master"
6
+ if version == "master"
7
+ gem "rails", github: "rails/rails"
8
+ else
9
+ gem "rails", "~> #{version}.0"
10
+ end
11
+
12
+ gem "sqlite3", ">= 1.3", platform: :ruby
13
+ gem "activerecord-jdbcsqlite3-adapter", platform: :jruby
14
+ gem "minitest", ">= 4.2"
15
+ gem "capybara", ">= 2.6"
16
+
17
+ if ENV["RAILS_VERSION"] == "4.0"
18
+ gem "minitest-rails", platform: :jruby
19
+ end
20
+ end
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License
2
+
3
+ Copyright (c) 2011-2013 Learnable Pty Ltd
4
+ Copyright (c) 2016 Voormedia
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in
14
+ all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,261 @@
1
+ [<img src="https://travis-ci.org/voormedia/flipflop.svg?branch=master" alt="Build Status">](https://travis-ci.org/voormedia/flipflop)
2
+
3
+ # Flipflop your features
4
+
5
+ **Flipflop** provides a declarative, layered way of enabling and disabling
6
+ application functionality at run-time. It is originally based on
7
+ [Flip](https://github.com/pda/flip). Compared to the original gem **Flipflop** has:
8
+ * an improved dashboard
9
+ * thread safety
10
+ * better database performance due to per-request caching, enabled by default
11
+ * more strategies (query strings, sessions, custom code)
12
+ * more strategy options (cookie options, strategy names and descriptions, custom database models)
13
+ * the ability to use the same strategy twice, with different options
14
+ * configuration in a fixed location (`config/features.rb`) that is usable even if you don't use the database strategy
15
+ * dashboard is inaccessible in production by default, for safety in case of misconfiguration
16
+ * removes controller filters and view helpers, to promote uniform semantics to check for features (facilitates project-wide searching)
17
+
18
+ You can configure strategy layers that will evaluate if a feature is currently
19
+ enabled or disabled. Available strategies are:
20
+ * a per-feature default setting
21
+ * database (with Active Record), to flipflop features site-wide for all users
22
+ * cookie or session, to flipflop features for single users
23
+ * query string parameters, to flipflop features occasionally (in development mode for example)
24
+ * custom strategy code
25
+
26
+ Flipflop has a dashboard interface that's easy to understand and use.
27
+
28
+ [<img src="https://raw.githubusercontent.com/voormedia/flipflop/screenshots/dashboard.png" alt="Dashboard">](https://raw.githubusercontent.com/voormedia/flipflop/screenshots/dashboard.png)
29
+
30
+ ## Installation
31
+
32
+ Add the gem to your `Gemfile`:
33
+
34
+ ```ruby
35
+ gem "flipflop"
36
+ ```
37
+
38
+ Generate routes, feature settings and database migration:
39
+
40
+ ```
41
+ rails g flipflop:install
42
+ ```
43
+
44
+ Run the migration to store feature settings in your database:
45
+
46
+ ```
47
+ rake db:migrate
48
+ ```
49
+
50
+ ## Declaring features
51
+
52
+ Features and strategies are declared in `config/features.rb`:
53
+
54
+ ```ruby
55
+ Flipflop.configure do
56
+ # Strategies will be used in the order listed here.
57
+ strategy :cookie
58
+ strategy :active_record
59
+ strategy :default
60
+
61
+ # Basic feature declaration:
62
+ feature :shiny_things
63
+
64
+ # Enable features by default:
65
+ feature :world_domination, default: true
66
+ end
67
+ ```
68
+
69
+ This file is automatically reloaded in development mode. No need to restart
70
+ your server after making changes.
71
+
72
+ ## Strategies
73
+
74
+ The following strategies are provided:
75
+ * `:active_record` – Save feature settings in the database.
76
+ * `:class` – Provide the feature model. `Flipflop::Feature` by default (which uses the table `features`). Honors `default_scope` when features are resolved or switched on/off.
77
+ * `:cookie` – Save feature settings in browser cookies for the current user.
78
+ * `:path` – The path for which the cookies apply. Defaults to the root of the application.
79
+ * `:domain` – Cookie domain. Is `nil` by default (no specific domain). Can be `:all` to use the topmost domain. Can be an array of domains.
80
+ * `:secure` – Only set cookies if the connection is secured with TLS. Default is `false`.
81
+ * `:httponly` – Whether the cookies are accessible via scripting or only HTTP. Default is `false`.
82
+ * `:query_string` – Interpret query string parameters as features. This strategy is only used for resolving. It does not allow switching features on/off.
83
+ * `:session` – Save feature settings in the current user's application session.
84
+ * `:default` – Not strictly needed, all feature defaults will be applied if no strategies match a feature. Include this strategy to determine the order of using the default value, and to make it appear in the dashboard.
85
+
86
+ All strategies support these options, to change the appearance of the dashboard:
87
+ * `:name` – The name of the strategy. Defaults to the name of the selected strategy.
88
+ * `:description` – The description of the strategy. Every strategy has a default description.
89
+ * `:hidden` – Optionally hides the strategy from the dashboard. Default is `false`.
90
+
91
+ ## Checking if a feature is enabled
92
+
93
+ `Flipflop.enabled?` or the dynamic predicate methods can be used to check
94
+ feature state:
95
+
96
+ ```ruby
97
+ Flipflop.enabled?(:world_domination) # true
98
+ Flipflop.world_domination? # true
99
+
100
+ Flipflop.enabled?(:shiny_things) # false
101
+ Flipflop.shiny_things? # false
102
+ ```
103
+
104
+ This works everywhere. In your views:
105
+
106
+ ```erb
107
+ <div>
108
+ <% if Flipflop.world_domination? %>
109
+ <%= link_to "Dominate World", world_dominations_path %>
110
+ <% end %>
111
+ </div>
112
+ ```
113
+
114
+ In your controllers:
115
+
116
+ ```ruby
117
+ class ShinyThingsController < ApplicationController
118
+ def index
119
+ return head :forbidden unless Flipflop.shiny_things?
120
+ # Proceed with shiny things...
121
+ end
122
+ end
123
+ ```
124
+
125
+ In your models:
126
+
127
+ ```ruby
128
+ class ShinyThing < ActiveRecord::Base
129
+ after_initialize do
130
+ if !Flipflop.shiny_things?
131
+ raise ActiveRecord::RecordNotFound
132
+ end
133
+ end
134
+ end
135
+ ```
136
+
137
+ ## Custom strategies
138
+
139
+ Custom light-weight strategies can be defined with a block:
140
+
141
+ ```ruby
142
+ Flipflop.configure do
143
+ strategy :random do |feature|
144
+ rand(2).zero?
145
+ end
146
+ # ...
147
+ end
148
+ ```
149
+
150
+ You can define your own custom strategies by inheriting from `Flipflop::Strategies::AbstractStrategy`:
151
+
152
+ ```ruby
153
+ class UserPreferenceStrategy < Flipflop::Strategies::AbstractStrategy
154
+ class << self
155
+ def default_description
156
+ "Allows configuration of features per user."
157
+ end
158
+ end
159
+
160
+ def switchable?
161
+ # Can only switch features on/off if we have the user's session.
162
+ # The `request` method is provided by AbstractStrategy.
163
+ request?
164
+ end
165
+
166
+ def enabled?(feature)
167
+ # Can only check features if we have the user's session.
168
+ return unless request?
169
+ find_current_user.enabled_features[feature]
170
+ end
171
+
172
+ def switch!(feature, enabled)
173
+ user = find_current_user
174
+ user.enabled_features[feature] = enabled
175
+ user.save!
176
+ end
177
+
178
+ def clear!(feature)
179
+ user = find_current_user
180
+ user.enabled_features.delete(feature)
181
+ user.save!
182
+ end
183
+
184
+ private
185
+
186
+ def find_current_user
187
+ # The `request` method is provided by AbstractStrategy.
188
+ User.find_by_id(request.session[:user_id])
189
+ end
190
+ end
191
+ ```
192
+
193
+ Use it in `config/features.rb`:
194
+
195
+ ```ruby
196
+ Flipflop.configure do
197
+ strategy UserPreferenceStrategy # name: "my strategy", description: "..."
198
+ end
199
+ ```
200
+
201
+ ## Dashboard access control
202
+
203
+ The dashboard provides visibility and control over the features.
204
+
205
+ You don't want the dashboard to be public. For that reason it is only available
206
+ in the development and test environments by default. Here's one way of
207
+ implementing access control.
208
+
209
+ In `app/config/application.rb`:
210
+
211
+ ```ruby
212
+ config.flipflop.dashboard_access_filter = :require_authenticated_user
213
+ ```
214
+
215
+ In `app/controllers/application_controller.rb`:
216
+
217
+ ```ruby
218
+ class ApplicationController < ActionController::Base
219
+ def require_authenticated_user
220
+ head :forbidden unless User.logged_in?
221
+ end
222
+ end
223
+ ```
224
+
225
+ Or directly in `app/config/application.rb`:
226
+
227
+ ```ruby
228
+ config.flipflop.dashboard_access_filter = -> {
229
+ head :forbidden unless User.logged_in?
230
+ }
231
+ ```
232
+
233
+ ## Testing
234
+
235
+ In your test environment, you typically want to keep your features. But to make
236
+ testing easier, you may not want to use any of the strategies you use in
237
+ development and production. You can replace all strategies with a single
238
+ `:test` strategy by calling `Flipflop::FeatureSet.current.test!`. The test
239
+ strategy will be returned. You can use this strategy to enable and disable
240
+ features.
241
+
242
+ ```ruby
243
+ describe WorldDomination do
244
+ before do
245
+ test_strategy = Flipflop::FeatureSet.current.test!
246
+ test_strategy.switch!(:world_domination, true)
247
+ end
248
+
249
+ it "should dominate the world" do
250
+ # ...
251
+ end
252
+ end
253
+ ```
254
+
255
+ If you are not happy with the default test strategy (which is essentially a
256
+ simple thread-safe hash object), you can provide your own implementation as
257
+ argument to the `test!` method.
258
+
259
+ ## License
260
+
261
+ This software is licensed under the MIT License. [View the license](LICENSE).
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new("test") do |test|
5
+ test.pattern = "test/**/*_test.rb"
6
+ end
7
+
8
+ Rake::TestTask.new("test:unit") do |test|
9
+ test.pattern = "test/unit/**/*_test.rb"
10
+ end
11
+
12
+ Rake::TestTask.new("test:integration") do |test|
13
+ test.pattern = "test/integration/**/*_test.rb"
14
+ end
15
+
16
+ task default: :test
@@ -0,0 +1,109 @@
1
+ $enable-transitions: true;
2
+ @import "bootstrap";
3
+
4
+ section.flipflop {
5
+ @extend .container-fluid;
6
+ margin: 5rem 0 0;
7
+
8
+ table {
9
+ @extend .table;
10
+ @extend .table-striped;
11
+
12
+ thead {
13
+ @extend .thead-inverse;
14
+
15
+ th {
16
+ position: relative;
17
+ cursor: default;
18
+
19
+ &[data-tooltip]:before, &[data-tooltip]:after {
20
+ @include transition(all 0.2s ease-out);
21
+ transform: translateY(0.2rem) translateZ(0);
22
+ opacity: 0;
23
+
24
+ display: block;
25
+ position: absolute;
26
+ }
27
+
28
+ &[data-tooltip]:before {
29
+ content: attr(data-tooltip);
30
+ width: 98%;
31
+ left: 0;
32
+ bottom: 3.75rem;
33
+ margin: 0;
34
+ padding: 0.5rem 0.75rem;
35
+ background: $gray;
36
+ border-radius: 0.2rem;
37
+ font-size: 0.875rem;
38
+ font-weight: normal;
39
+ pointer-events: none;
40
+ }
41
+
42
+ &[data-tooltip]:after {
43
+ content: " ";
44
+ width: 0;
45
+ height: 0;
46
+ left: 1rem;
47
+ bottom: 3.25rem;
48
+ border-left: solid transparent 0.5rem;
49
+ border-right: solid transparent 0.5rem;
50
+ border-top: solid $gray 0.5rem;
51
+ }
52
+
53
+ &:hover {
54
+ &[data-tooltip]:before, &[data-tooltip]:after {
55
+ transform: translateY(0) translateZ(0);
56
+ opacity: 1;
57
+ }
58
+ }
59
+ }
60
+ }
61
+
62
+ tbody {
63
+ td.status {
64
+ width: 2rem;
65
+ font-size: 1.1rem;
66
+
67
+ span {
68
+ width: 2rem;
69
+ @extend .label;
70
+ @extend .label-pill;
71
+ &.on { @extend .label-success; }
72
+ &.off { @extend .label-default; }
73
+ }
74
+ }
75
+
76
+ td.name {
77
+ min-width: 11rem;
78
+ padding-top: 0.9rem;
79
+ font-weight: bold;
80
+ }
81
+
82
+ td.description {
83
+ min-width: 11rem;
84
+ padding-top: 0.9rem;
85
+ }
86
+
87
+ td.toggle {
88
+ min-width: 11rem;
89
+
90
+ div.toolbar {
91
+ @extend .btn-toolbar;
92
+ margin-left: 0;
93
+
94
+ div.group {
95
+ @extend .btn-group;
96
+ @extend .btn-group-sm;
97
+
98
+ input[type=submit] {
99
+ @extend .btn;
100
+ @extend .btn-sm;
101
+ &.active { @extend .btn-primary; }
102
+ &:not(.active) { @extend .btn-secondary; }
103
+ }
104
+ }
105
+ }
106
+ }
107
+ }
108
+ }
109
+ }
@@ -0,0 +1,5 @@
1
+ module Flipflop::EnvironmentFilters
2
+ def require_development
3
+ head :forbidden unless Rails.env.development? or Rails.env.test?
4
+ end
5
+ end
@@ -0,0 +1,59 @@
1
+ require "bootstrap"
2
+
3
+ module Flipflop
4
+ class FeaturesController < ApplicationController
5
+ include EnvironmentFilters
6
+
7
+ layout "flipflop"
8
+
9
+ def index
10
+ @feature_set = FeaturesPresenter.new(FeatureSet.current)
11
+ end
12
+
13
+ class FeaturesPresenter
14
+ include Flipflop::Engine.routes.url_helpers
15
+
16
+ def initialize(feature_set)
17
+ @cache = {}
18
+ @feature_set = feature_set
19
+ end
20
+
21
+ def strategies
22
+ @feature_set.strategies.reject(&:hidden?)
23
+ end
24
+
25
+ def features
26
+ @feature_set.features
27
+ end
28
+
29
+ def status(feature)
30
+ cache(nil, feature) do
31
+ status_to_s(@feature_set.enabled?(feature.key))
32
+ end
33
+ end
34
+
35
+ def strategy_status(strategy, feature)
36
+ cache(strategy, feature) do
37
+ status_to_s(strategy.enabled?(feature.key))
38
+ end
39
+ end
40
+
41
+ def switch_url(strategy, feature)
42
+ feature_strategy_path(feature.key, strategy.key)
43
+ end
44
+
45
+ private
46
+
47
+ def cache(strategy, feature)
48
+ key = feature.key.to_s + (strategy ? "-" + strategy.key.to_s : "")
49
+ return @cache[key] if @cache.has_key?(key)
50
+ @cache[key] = yield
51
+ end
52
+
53
+ def status_to_s(status)
54
+ return "on" if status == true
55
+ return "off" if status == false
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,30 @@
1
+ module Flipflop
2
+ class StrategiesController < ApplicationController
3
+ include EnvironmentFilters
4
+ include Engine.routes.url_helpers
5
+
6
+ def update
7
+ strategy.switch!(feature_key, enable?)
8
+ redirect_to(flipflop.features_url)
9
+ end
10
+
11
+ def destroy
12
+ strategy.clear!(feature_key)
13
+ redirect_to(flipflop.features_url)
14
+ end
15
+
16
+ private
17
+
18
+ def enable?
19
+ params[:commit].to_s.downcase.include?("on")
20
+ end
21
+
22
+ def feature_key
23
+ params[:feature_id].to_sym
24
+ end
25
+
26
+ def strategy
27
+ FeatureSet.current.strategy(params[:id])
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,3 @@
1
+ class Flipflop::Feature < ActiveRecord::Base
2
+ self.table_name = "features"
3
+ end
@@ -0,0 +1,60 @@
1
+ <section class="flipflop">
2
+ <h1>Feature Flipflop</h1>
3
+ <table>
4
+ <thead>
5
+ <tr>
6
+ <th></th>
7
+ <th class="name">Feature</th>
8
+ <th class="description">Description</th>
9
+ <% @feature_set.strategies.each do |strategy| -%>
10
+ <th data-tooltip="<%= strategy.description -%>">
11
+ <%= strategy.name.humanize -%>
12
+ </th>
13
+ <% end -%>
14
+ </tr>
15
+ </thead>
16
+ <tbody>
17
+ <% @feature_set.features.each do |feature| -%>
18
+ <tr data-feature="<%= feature.name.dasherize.parameterize %>">
19
+ <td class="status">
20
+ <span class="<%= @feature_set.status(feature) -%>"><%= @feature_set.status(feature) -%></span>
21
+ </td>
22
+ <td class="name"><%= feature.name.humanize -%></td>
23
+ <td class="description"><%= feature.description -%></td>
24
+
25
+ <% @feature_set.strategies.each do |strategy| -%>
26
+ <td class="toggle" data-strategy="<%= strategy.name.dasherize.parameterize %>">
27
+ <div class="toolbar">
28
+ <%= form_tag(@feature_set.switch_url(strategy, feature), method: :put) do -%>
29
+ <div class="group">
30
+ <%= submit_tag "on",
31
+ type: "submit",
32
+ class: @feature_set.strategy_status(strategy, feature) == "on" ? "active" : nil,
33
+ disabled: strategy.switchable? ? false : true
34
+ -%>
35
+
36
+ <%= submit_tag "off",
37
+ type: "submit",
38
+ class: @feature_set.strategy_status(strategy, feature) == "off" ? "active" : nil,
39
+ disabled: strategy.switchable? ? false : true
40
+ -%>
41
+ </div>
42
+ <% end -%>
43
+
44
+ <% if strategy.switchable? -%>
45
+ <div class="group">
46
+ <% unless @feature_set.strategy_status(strategy, feature).blank? -%>
47
+ <%= form_tag(@feature_set.switch_url(strategy, feature), method: :delete) do -%>
48
+ <%= submit_tag "clear", type: "submit" -%>
49
+ <% end -%>
50
+ <% end -%>
51
+ </div>
52
+ <% end -%>
53
+ </div>
54
+ </td>
55
+ <% end -%>
56
+ </tr>
57
+ <% end -%>
58
+ </tbody>
59
+ </table>
60
+ </div>
@@ -0,0 +1 @@
1
+ <!doctype html><head><title></title><%= stylesheet_link_tag :flipflop -%></head><body><%= yield %></body>
data/config/routes.rb ADDED
@@ -0,0 +1,5 @@
1
+ Flipflop::Engine.routes.draw do
2
+ resources :features, path: "", only: [:index] do
3
+ resources :strategies, only: [:update, :destroy]
4
+ end
5
+ end