Skip to content

Commit

Permalink
Implement feature grouping and translations. Closes #17.
Browse files Browse the repository at this point in the history
  • Loading branch information
rolftimmermans committed Feb 28, 2017
1 parent 590f2ae commit 51cc858
Show file tree
Hide file tree
Showing 31 changed files with 816 additions and 266 deletions.
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## 2.3.0

* Support for Rails API only apps.
* Features can now be grouped in the dashboard.
* Features can now be added to Rails engines and loaded with an initializer.
* The dashboard (including features and strategies) can be translated.
* Removed Rails asset pipeline dependency.
11 changes: 6 additions & 5 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,21 @@ group :test do
end

gem "bootstrap", "= 4.0.0.alpha6", require: false

gem "fakeredis", require: false
gem "sqlite3", ">= 1.3", platform: :ruby
gem "fakeredis"
gem "activerecord-jdbcsqlite3-adapter", platform: :jruby,
github: "jruby/activerecord-jdbc-adapter", branch: "rails-5"

gem "minitest", ">= 4.2"
gem "capybara", ">= 2.6"

if Gem::Version.new(RUBY_VERSION) > Gem::Version.new("2.2.4")
gem "listen", ">= 3.0", require: false
end

if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("2.1.0")
# Nokogiri 1.7+ requires Ruby 2.1+.
gem "nokogiri", "< 1.7"
end

if ENV["RAILS_VERSION"] == "4.0"
gem "minitest-rails", platform: :jruby
end
end
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ application functionality at run-time. It is originally based on
* removes controller filters and view helpers, to promote uniform semantics to check for features (facilitates project-wide searching)
* support for API only Rails apps
* support for loading features from Rails engines
* support for feature groups

You can configure strategy layers that will evaluate if a feature is currently
enabled or disabled. Available strategies are:
Expand Down
15 changes: 15 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,19 @@ namespace :assets do
environment.css_compressor = :scss
File.write(stylesheet_dst_path, environment[stylesheet_file])
end

task :watch do
require "listen"

listener = Listen.to(stylesheets_src_path, only: /\.scss$/) do
Rake::Task["assets:compile"].execute
end

$stderr.puts("Watching #{stylesheets_src_path} for changes...")

listener.start

Rake::Task["assets:compile"].execute
sleep
end
end
23 changes: 13 additions & 10 deletions app/controllers/flipflop/features_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,31 @@ def index
class FeaturesPresenter
include Flipflop::Engine.routes.url_helpers

attr_reader :strategies, :grouped_features, :application_name

def initialize(feature_set)
@cache = {}
@feature_set = feature_set
end

def strategies
@feature_set.strategies.reject(&:hidden?)
@strategies = @feature_set.strategies.reject(&:hidden?)
@grouped_features = @feature_set.features.group_by(&:group)

@application_name = Rails.application.class.parent_name.underscore.titleize
end

def features
@feature_set.features
def grouped?
grouped_features.keys != [nil]
end

def status(feature)
cache(nil, feature) do
status_to_s(@feature_set.enabled?(feature.key))
status_to_sym(@feature_set.enabled?(feature.key))
end
end

def strategy_status(strategy, feature)
cache(strategy, feature) do
status_to_s(strategy.enabled?(feature.key))
status_to_sym(strategy.enabled?(feature.key))
end
end

Expand All @@ -53,9 +56,9 @@ def cache(strategy, feature)
@cache[key] = yield
end

def status_to_s(status)
return "on" if status == true
return "off" if status == false
def status_to_sym(status)
return :enabled if status == true
return :disabled if status == false
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/flipflop/strategies_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def destroy
private

def enable?
params[:commit].to_s.downcase.include?("on")
params[:commit].to_s == "1"
end

def feature_key
Expand Down
104 changes: 63 additions & 41 deletions app/views/flipflop/features/index.html.erb
Original file line number Diff line number Diff line change
@@ -1,66 +1,88 @@
<!DOCTYPE html>
<html>
<head>
<title><%= app_name = Rails.application.class.parent_name.underscore.titleize %> Features</title>
<title><%= t(:title, scope: :flipflop, application: @feature_set.application_name) -%></title>
<style><%= render partial: "flipflop/stylesheets/flipflop.css" %></style>
</head>
<body>
<section class="flipflop">
<h1><%= app_name %> Features</h1>
<h1><%= t(:title, scope: :flipflop, application: @feature_set.application_name) -%></h1>
<table>
<thead>
<tr>
<th></th>
<th class="name">Feature</th>
<th class="description">Description</th>
<th class="name"><%= t(:feature, scope: :flipflop) -%></th>
<th class="description"><%= t(:description, scope: :flipflop) -%></th>
<% @feature_set.strategies.each do |strategy| -%>
<th data-tooltip="<%= strategy.description -%>">
<%= strategy.name.humanize -%>
<%= t(strategy.name, scope: [:flipflop, :strategies], default: strategy.title) -%>
</th>
<% end -%>
</tr>
</thead>
<tbody>
<% @feature_set.features.each do |feature| -%>
<tr data-feature="<%= feature.name.dasherize.parameterize %>">
<td class="status">
<span class="<%= @feature_set.status(feature) -%>"><%= @feature_set.status(feature) -%></span>
</td>
<td class="name"><%= feature.name.humanize -%></td>
<td class="description"><%= feature.description -%></td>
<% @feature_set.grouped_features.each do |group, features| -%>
<% if @feature_set.grouped? -%>
<tr class="group">
<td></td>
<td class="name" colspan="<%= 2 + @feature_set.strategies.size -%>">
<h2>
<%= t(group ? group.name : :default, scope: [:flipflop, :groups], default: group ? group.title : nil) -%>
</h2>
</td>
</tr>
<% end -%>
<% features.each do |feature| -%>
<tr data-feature="<%= feature.name.dasherize.parameterize -%>">
<td class="status">
<span class="<%= @feature_set.status(feature) -%>">
<span><%= t(@feature_set.status(feature), scope: [:flipflop, :feature_states]) -%></span>
</span>
</td>
<td class="name">
<%= title = t(feature.name, scope: [:flipflop, :features], default: feature.title) -%>
</td>
<td class="description">
<%= t(:"#{feature.name}_description", scope: [:flipflop, :features], default: feature.description || title + ".") -%>
</td>

<% @feature_set.strategies.each do |strategy| -%>
<td class="toggle" data-strategy="<%= strategy.name.dasherize.parameterize %>">
<div class="toolbar">
<%= form_tag(@feature_set.switch_url(strategy, feature), method: :put) do -%>
<div class="group">
<%= submit_tag "on",
type: "submit",
class: @feature_set.strategy_status(strategy, feature) == "on" ? "active" : nil,
disabled: !strategy.switchable?
-%>
<% @feature_set.strategies.each do |strategy| -%>
<td class="toggle" data-strategy="<%= strategy.name.dasherize.parameterize %>">
<div class="toolbar">
<%= form_tag(@feature_set.switch_url(strategy, feature), method: :put) do -%>
<div class="group">
<%= button_tag t(:enabled, scope: [:flipflop, :feature_states]),
type: "submit",
name: "commit",
value: "1",
class: @feature_set.strategy_status(strategy, feature) == :enabled ? "active" : nil,
disabled: !strategy.switchable?
-%>
<%= submit_tag "off",
type: "submit",
class: @feature_set.strategy_status(strategy, feature) == "off" ? "active" : nil,
disabled: !strategy.switchable?
-%>
</div>
<% end -%>
<%= button_tag t(:disabled, scope: [:flipflop, :feature_states]),
type: "submit",
name: "commit",
value: "0",
class: @feature_set.strategy_status(strategy, feature) == :disabled ? "active" : nil,
disabled: !strategy.switchable?
-%>
</div>
<% end -%>
<% if strategy.switchable? -%>
<div class="group">
<% unless @feature_set.strategy_status(strategy, feature).blank? -%>
<%= form_tag(@feature_set.switch_url(strategy, feature), method: :delete) do -%>
<%= submit_tag "clear", type: "submit" -%>
<% if strategy.switchable? -%>
<div class="group">
<% unless @feature_set.strategy_status(strategy, feature).blank? -%>
<%= form_tag(@feature_set.switch_url(strategy, feature), method: :delete) do -%>
<%= button_tag t(:clear, scope: :flipflop), type: "submit" -%>
<% end -%>
<% end -%>
<% end -%>
</div>
<% end -%>
</div>
</td>
<% end -%>
</tr>
</div>
<% end -%>
</div>
</td>
<% end -%>
</tr>
<% end -%>
<% end -%>
</tbody>
</table>
Expand Down
2 changes: 1 addition & 1 deletion app/views/flipflop/stylesheets/_flipflop.css

Large diffs are not rendered by default.

34 changes: 34 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
en:
flipflop:
title: "%{application} Features"
feature: "Feature"
description: "Description"

feature_states:
enabled: "on"
disabled: "off"

clear: "clear"

# You can optionally translate the names of the chosen features, feature
# groups and strategies:

groups:
default: "Other features"

# groups:
# design_improvements: "Design improvements"

# features:
# world_domination: "World domination"
# world_domination_description: "Take over the world!"

# strategies:
# cookie: "Cookie"
# active_record: "Active record"
# default: "Default"
# lambda: "Lambda"
# query_string: "Query string"
# redis: "Redis"
# session: "Session"
# test: "Test"
1 change: 1 addition & 0 deletions lib/flipflop.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
require "flipflop/feature_definition"
require "flipflop/feature_loader"
require "flipflop/feature_set"
require "flipflop/group_definition"

require "flipflop/strategies/abstract_strategy"
require "flipflop/strategies/options_hasher"
Expand Down
10 changes: 10 additions & 0 deletions lib/flipflop/configurable.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
module Flipflop
module Configurable
attr_accessor :current_group

def group(group)
self.current_group = GroupDefinition.new(group)
yield
ensure
self.current_group = nil
end

def feature(feature, **options)
options = options.merge(group: current_group)
feature = FeatureDefinition.new(feature, **options)
FeatureSet.current.add(feature)
end
Expand Down
11 changes: 5 additions & 6 deletions lib/flipflop/feature_definition.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
module Flipflop
class FeatureDefinition
attr_reader :key, :default, :description
attr_reader :key, :name, :title, :description, :default, :group

def initialize(key, **options)
@key = key
@name = @key.to_s.freeze
@title = @name.humanize.freeze
@description = options.delete(:description).freeze
@default = !!options.delete(:default) || false
@description = options.delete(:description) || key.to_s.humanize + "."
end

def name
key.to_s
@group = options.delete(:group).freeze
end
end
end
2 changes: 1 addition & 1 deletion lib/flipflop/feature_loader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def checker

def reload!
@@lock.synchronize do
Flipflop.feature_set.replace do
Flipflop::FeatureSet.current.replace do
@paths.each { |path| Kernel.load(path) }
end
end
Expand Down
11 changes: 11 additions & 0 deletions lib/flipflop/group_definition.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module Flipflop
class GroupDefinition
attr_reader :key, :name, :title

def initialize(key)
@key = key
@name = @key.to_s.freeze
@title = @name.humanize.freeze
end
end
end
3 changes: 2 additions & 1 deletion lib/flipflop/strategies/abstract_strategy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,15 @@ def default_description
end
end

attr_reader :key, :name, :description
attr_reader :key, :name, :title, :description

def initialize(**options)
# Generate key before setting instance that should be excluded from
# unique key generation.
@key = OptionsHasher.new(self).generate

@name = (options.delete(:name) || self.class.default_name).freeze
@title = @name.humanize.freeze
@description = (options.delete(:description) || self.class.default_description).freeze
@hidden = !!options.delete(:hidden) || false

Expand Down

0 comments on commit 51cc858

Please sign in to comment.