Skip to content

Commit

Permalink
Merge pull request #12297 from dmarcoux/opt-out-of-feature-toggles
Browse files Browse the repository at this point in the history
Allow users to manage beta features
  • Loading branch information
Dany Marcoux committed Mar 16, 2022
2 parents 001a19b + ec39d84 commit 86fa5e3
Show file tree
Hide file tree
Showing 21 changed files with 329 additions and 61 deletions.
1 change: 0 additions & 1 deletion src/api/app/assets/javascripts/webui/application.js
Expand Up @@ -35,7 +35,6 @@
//= require webui/popover.js
//= require webui/repositories.js
//= require webui/distributions.js
//= require webui/users.js
// FIXME refactor these files
//= require webui/autocomplete.js
//= require webui/comment.js
Expand Down
5 changes: 0 additions & 5 deletions src/api/app/assets/javascripts/webui/users.js

This file was deleted.

37 changes: 37 additions & 0 deletions src/api/app/controllers/webui/users/beta_features_controller.rb
@@ -0,0 +1,37 @@
class Webui::Users::BetaFeaturesController < Webui::WebuiController
after_action :verify_policy_scoped

def index
@disabled_beta_features = policy_scope(DisabledBetaFeature).pluck(:name)
@user = User.session!
end

def update
feature_name = feature_params.keys.first
feature_action = feature_params.values.first

if feature_action == 'disable'
begin
policy_scope(DisabledBetaFeature).create!(name: feature_name)
flash[:success] = "You disabled the beta feature '#{feature_name.humanize}'."
rescue ActiveRecord::RecordInvalid
flash[:error] = "You already disabled the beta feature '#{feature_name.humanize}'."
end
else
disabled_beta_feature = policy_scope(DisabledBetaFeature).find_by(name: feature_name)
if disabled_beta_feature && disabled_beta_feature.destroy
flash[:success] = "You enabled the beta feature '#{feature_name.humanize}'."
else
flash[:error] = "You already enabled the beta feature '#{feature_name.humanize}'."
end
end

redirect_to my_beta_features_path
end

private

def feature_params
params.require(:feature).permit(ENABLED_FEATURE_TOGGLES.pluck(:name))
end
end
25 changes: 25 additions & 0 deletions src/api/app/models/disabled_beta_feature.rb
@@ -0,0 +1,25 @@
class DisabledBetaFeature < ApplicationRecord
validates :name, presence: true
validates :user_id, uniqueness: { scope: :name }

belongs_to :user
# rubocop:disable Rails/InverseOf
# This is an internal model from the flipper-active_record gem, so we cannot set inverse_of
belongs_to :feature, foreign_key: :name, class_name: 'Flipper::Adapters::ActiveRecord::Feature', primary_key: :key
# rubocop:enable Rails/InverseOf
end

# == Schema Information
#
# Table name: disabled_beta_features
#
# id :integer not null, primary key
# name :string(255) not null, indexed => [user_id]
# created_at :datetime not null
# updated_at :datetime not null
# user_id :integer indexed => [name]
#
# Indexes
#
# index_disabled_beta_features_on_user_id_and_name (user_id,name) UNIQUE
#
2 changes: 2 additions & 0 deletions src/api/app/models/user.rb
Expand Up @@ -53,6 +53,8 @@ class User < ApplicationRecord
has_many :status_message_acknowledgements, dependent: :destroy
has_many :acknowledged_status_messages, through: :status_message_acknowledgements, class_name: 'StatusMessage', source: 'status_message'

has_many :disabled_beta_features, dependent: :destroy

scope :confirmed, -> { where(state: 'confirmed') }
scope :all_without_nobody, -> { where.not(login: NOBODY_LOGIN) }
scope :not_deleted, -> { where.not(state: 'deleted') }
Expand Down
13 changes: 13 additions & 0 deletions src/api/app/policies/disabled_beta_feature_policy.rb
@@ -0,0 +1,13 @@
class DisabledBetaFeaturePolicy < ApplicationPolicy
class Scope < Scope
def initialize(user, scope)
raise Pundit::NotAuthorizedError, reason: ApplicationPolicy::ANONYMOUS_USER if user.nil? || user.is_nobody?

super(user, scope)
end

def resolve
scope.where(user: user)
end
end
end
1 change: 1 addition & 0 deletions src/api/app/views/webui/user/_index_actions.html.haml
Expand Up @@ -4,3 +4,4 @@
= render partial: 'webui/user/index_actions/change_notifications'
- if feature_enabled?('trigger_workflow')
= render partial: 'webui/user/index_actions/manage_tokens'
= render partial: 'webui/user/index_actions/manage_beta_features'
15 changes: 0 additions & 15 deletions src/api/app/views/webui/user/_info.html.haml
Expand Up @@ -21,18 +21,3 @@
= link_to(group.title, group_path(group))
%span.badge.badge-primary
#{group.tasks} #{'task'.pluralize(group.tasks)}

- if is_user
.mt-4
= form_tag(user_path, id: 'beta-form', method: :patch) do
.custom-control.custom-switch
= hidden_field_tag 'user[in_beta]', false
= check_box_tag('user[in_beta]', !user.in_beta, user.in_beta, class: 'custom-control-input', id: 'beta-switch')
= label_tag 'Public Beta Program', nil, class: 'custom-control-label', for: 'beta-switch'
= hidden_field_tag('user[login]', user.login)
%i.fa.fa-question-circle.text-info{ data: { placement: 'top', toggle: 'popover', html: 'true',
content: 'Join the <strong>beta program</strong> to try the latest ' + |
'features we develop and give us feedback on them before they go live.' } } |

:javascript
switchBeta();
@@ -0,0 +1,4 @@
%li.nav-item
= link_to(my_beta_features_path, class: 'nav-link', title: 'Manage Beta Features') do
%i.fas.fa-lg.mr-2.fa-flask
%span.nav-item-name Manage Beta Features
@@ -0,0 +1,4 @@
%li.breadcrumb-item
= link_to('Your Profile', user_path(@user))
%li.breadcrumb-item.active{ 'aria-current' => 'page' }
Manage Beta Features
43 changes: 43 additions & 0 deletions src/api/app/views/webui/users/beta_features/index.html.haml
@@ -0,0 +1,43 @@
- @pagetitle = 'Manage Beta Features'

.card
.card-body
%h2= @pagetitle

.my-4
- if @user.in_beta?
%p
Thank you for joining the beta program! Give us
= link_to('feedback', 'https://openbuildservice.org/2018/10/04/the-beta-program/#how-to-provide-feedback')
on the features before they go live.
- else
%p
Join the beta program to try the latest features we develop and give us feedback on them before they go live.
Read our
= link_to('blog post', 'https://openbuildservice.org/2018/10/04/the-beta-program/')
on the beta program for more details.

= form_with(url: user_path(@user), method: :patch) do |_form|
.custom-control.custom-switch
= hidden_field_tag('user[in_beta]', false, id: nil)
= check_box_tag('user[in_beta]', !@user.in_beta, @user.in_beta, class: 'custom-control-input', onChange: 'this.form.submit()')
= label_tag('user[in_beta]', 'Beta program', class: 'custom-control-label')

- if @user.in_beta?
%hr

%p By default, all beta features are enabled as soon as you join the beta program.
%p
You can disable a specific beta feature if you want.
Enabling back a beta feature is always possible if you change your mind later.

- ENABLED_FEATURE_TOGGLES.each do |feature_toggle|
- feature_toggle_name, feature_toggle_description = feature_toggle.values_at(:name, :description).map(&:to_s)
.mb-2
%h3= feature_toggle_name.humanize
= form_with(url: my_beta_feature_path, method: :put) do |_form|
.custom-control.custom-switch
= hidden_field_tag("feature[#{feature_toggle_name}]", 'disable', id: nil)
= check_box_tag("feature[#{feature_toggle_name}]", 'enable', @disabled_beta_features.exclude?(feature_toggle_name),
class: 'custom-control-input', onChange: 'this.form.submit()')
= label_tag("feature[#{feature_toggle_name}]", feature_toggle_description, class: 'custom-control-label')
17 changes: 17 additions & 0 deletions src/api/config/initializers/flipper.rb
@@ -1,3 +1,9 @@
ENABLED_FEATURE_TOGGLES = [
{ name: :notifications_redesign, description: 'Introducing notifications page' },
{ name: :trigger_workflow, description: 'Better SCM and CI integration with OBS workflows' },
{ name: :new_watchlist, description: 'New implementation of watchlist including projects, packages and requests' }
].freeze

Flipper.configure do
# Register beta and rollout groups by default.
# We need to add it when initializing because Flipper.register doesn't
Expand All @@ -14,4 +20,15 @@
Flipper.register(:rollout) do |user|
user.respond_to?(:in_rollout?) && user.in_rollout?
end

ENABLED_FEATURE_TOGGLES.each do |feature_toggle|
feature_toggle_name = feature_toggle[:name]
# Register a group for this feature toggle
Flipper.register(feature_toggle_name) do |user|
# The user has to be in beta for this group to be active
user.respond_to?(:in_beta?) && user.in_beta? &&
# If a user didn't disable the feature, the feature will be active
user.respond_to?(:disabled_beta_features) && !user.disabled_beta_features.exists?(name: feature_toggle_name)
end
end
end
3 changes: 3 additions & 0 deletions src/api/config/routes/webui_routes.rb
Expand Up @@ -331,6 +331,9 @@
end
end

resources :beta_features, only: [:index], controller: 'webui/users/beta_features', as: :my_beta_features
resource :beta_feature, only: [:update], controller: 'webui/users/beta_features', as: :my_beta_feature

resource :notification, only: [:update], controller: 'webui/users/notifications', as: :my_notification

resources :subscriptions, only: [:index], controller: 'webui/users/subscriptions', as: :my_subscriptions do
Expand Down
12 changes: 12 additions & 0 deletions src/api/db/migrate/20220309122242_create_disabled_beta_features.rb
@@ -0,0 +1,12 @@
class CreateDisabledBetaFeatures < ActiveRecord::Migration[6.1]
def change
create_table(:disabled_beta_features, id: :integer) do |t|
t.string :name, null: false
t.references :user, type: :integer, index: false

t.timestamps

t.index [:user_id, :name], unique: true
end
end
end
10 changes: 9 additions & 1 deletion src/api/db/schema.rb
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 2022_02_10_154407) do
ActiveRecord::Schema.define(version: 2022_03_09_122242) do

create_table "architectures", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_ci", options: "ENGINE=InnoDB ROW_FORMAT=DYNAMIC", force: :cascade do |t|
t.string "name", null: false, collation: "utf8_general_ci"
Expand Down Expand Up @@ -363,6 +363,14 @@
t.index ["queue"], name: "index_delayed_jobs_on_queue"
end

create_table "disabled_beta_features", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
t.string "name", null: false
t.integer "user_id"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["user_id", "name"], name: "index_disabled_beta_features_on_user_id_and_name", unique: true
end

create_table "distribution_icons", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_ci", options: "ENGINE=InnoDB ROW_FORMAT=DYNAMIC", force: :cascade do |t|
t.string "url", null: false, collation: "utf8_unicode_ci"
t.integer "width"
Expand Down
18 changes: 5 additions & 13 deletions src/api/lib/tasks/dev.rake
Expand Up @@ -3,8 +3,6 @@
require 'fileutils'
require 'yaml'

ENABLED_FEATURE_FLAGS = [:notifications_redesign, :trigger_workflow, :new_watchlist].freeze

namespace :dev do
task :prepare do
puts 'Setting up the database configuration...'
Expand Down Expand Up @@ -54,14 +52,10 @@ namespace :dev do
puts 'Configure default signing'
Rake::Task['assets:clobber'].invoke
::Configuration.update(enforce_project_keys: true)
# Enable all feature flags for all beta users in the development environment to easily join the beta and test changes
# related to feature flags while also being able to test changes for non-beta users, so without any feature flag enabled
ENABLED_FEATURE_FLAGS.each do |feature_flag|
puts "Enabling feature flag #{feature_flag} for all beta users"
Flipper.disable(feature_flag) # making sure we are starting from a clean state since the database is not overwritten if already present
Flipper.enable(feature_flag, :beta)
end
end

puts 'Enable feature toggles for their group'
Rake::Task['flipper:enable_features_for_group'].invoke
end

desc 'Run all linters we use'
Expand Down Expand Up @@ -303,10 +297,8 @@ namespace :dev do
Rails.cache.clear
Rake::Task['db:reset'].invoke

# Enable all the feature flags for all logged-in and not-logged-in users in development env.
ENABLED_FEATURE_FLAGS.each do |feature_flag|
Flipper[feature_flag].enable
end
puts 'Enable feature toggles for their group'
Rake::Task['flipper:enable_features_for_group'].invoke

iggy = create(:confirmed_user, login: 'Iggy')
admin = User.get_default_admin
Expand Down
10 changes: 10 additions & 0 deletions src/api/lib/tasks/flipper.rake
@@ -0,0 +1,10 @@
namespace :flipper do
desc 'Enable feature toggles from ENABLED_FEATURE_TOGGLES for their group'
task enable_features_for_group: :environment do
ENABLED_FEATURE_TOGGLES.each do |feature_toggle|
feature_toggle_name = feature_toggle[:name]
# Enable the feature toggle for group with the same name
Flipper.enable(feature_toggle_name, feature_toggle_name)
end
end
end
@@ -0,0 +1,12 @@
require 'rails_helper'

RSpec.describe Webui::Users::BetaFeaturesController do
describe 'when the user is anonymous' do
before do
get :index
end

it { expect(response).to have_http_status(:found) }
it { expect(response).to redirect_to(new_session_path) }
end
end
6 changes: 6 additions & 0 deletions src/api/spec/factories/disabled_beta_feature.rb
@@ -0,0 +1,6 @@
FactoryBot.define do
factory :disabled_beta_feature do
name { 'something' } # It needs to be enabled: Flipper[name].enable
user
end
end

0 comments on commit 86fa5e3

Please sign in to comment.