flipflop 2.0.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.
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