Coder Social home page Coder Social logo

vinistock / sail Goto Github PK

View Code? Open in Web Editor NEW
506.0 8.0 32.0 42.83 MB

Sail is a lightweight Rails engine that brings an admin panel for managing configuration settings on a live Rails app

License: Other

Ruby 73.12% JavaScript 5.45% CSS 7.08% HTML 14.34%
rails settings ruby ruby-on-rails utilities feature-flags dashboard admin

sail's Introduction

dashboard

Build Status Gem Version Mentioned in Awesome Ruby

Sail

This Rails engine brings a setting model into your app to be used as feature flags, gauges, knobs and other live controls you may need.

It saves configurations to the database so that they can be changed while the application is running, without requiring a deploy.

Having this ability enables live experiments and tuning to find an application's best setup.

Enable/Disable a new feature, turn ON/OFF ab testing for new functionality, change jobs' parameters to tune performance, you name it.

It comes with a lightweight responsive admin dashboard for searching and changing configurations on the fly.

Sail assigns to each setting a relevancy score. This metric is calculated while the application is running and is based on the relative number of times a setting is invoked and on the total number of settings. The goal is to have an indicator of how critical changing a setting's value can be based on its frequency of usage.

Contents

  1. Installation
  2. Configuration
  3. Populating the database
  4. Searching
  5. Manipulating settings
  6. Localization
  7. Contributing

Installation

Add this line to your application's Gemfile:

gem "sail"

And then execute:

$ bundle

Adding the following line to your routes file will make the dashboard available at <base_url>/sail

mount Sail::Engine => "/sail"

Running the install generator will create necessary migrations for having the settings in your database.

$ bin/rails g sail:install

When going through a major version upgrade, be sure to check the changelog and run the update generator. It will create whatever migrations are needed to move from any other major version to the latest.

$ bin/rails g sail:update

If you wish to customize the settings' card, the views can be copied to the main app by using the view generator.

$ bin/rails g sail:views

Configuration

Available configurations and their defaults are listed below

Sail.configure do |config|
  config.cache_life_span = 6.hours        # How long to cache the Sail.get response for (note that cache is deleted after a set)
  config.array_separator = ";"            # Default separator for array settings
  config.dashboard_auth_lambda = nil      # Defines an authorization lambda to access the dashboard as a before action. Rendering or redirecting is included here if desired.
  config.back_link_path = "root_path"     # Path method as string for the "Main app" button in the dashboard. Any non-existent path will make the button disappear
  config.enable_search_auto_submit = true # Enables search auto submit after 2 seconds without typing
  config.days_until_stale = 60            # Days with no updates until a setting is considered stale and is a candidate to be removed from code (leave nil to disable checks)
  config.enable_logging = true            # Enable logging for update and reset actions. Logs include timestamp, setting name, new value and author_user_id (if current_user is defined)
  config.failures_until_reset = 50        # Number of times Sail.get can fail with unexpected errors until resetting the setting value
end

A possible authorization lambda is defined below.

Sail.configure do |config|
  config.dashboard_auth_lambda = -> { redirect_to("/") unless session[:current_user].admin? }
end

Populating the database

In order to create settings, use the config/sail.yml file (or create your own data migrations).

If the sail.yml file was not created, it can be generated with the current state of the database using the following rake task.

$ rake sail:create_config_file

After settings have been created a first time, they will not be updated with the values in the yaml file (otherwise it would defeat the purpose of being able to configure the application without requiring a deploy).

Removing the entries from this file will cause settings to be deleted from the database.

Settings can be aggregated by using groups. Searching by a group name will return all settings for that group.

# Rails.root/config/sail.yml
# Setting name with it's information contained inside
# These values are used for the reset functionality as well

first_setting:
    description: My very first setting
    value: some_important_string
    cast_type: string
    group: setting_group_1
second_setting:
    description: My second setting, this time a boolean
    value: false
    cast_type: boolean
    group: feature_flags

To clear the database and reload the contents of your sail.yml file, invoke this rake task.

$ rake sail:load_defaults

Searching

Searching for settings in the dashboard can be done in the following ways:

  • By name: matches a substring of the setting's name
  • By group: matches all settings with the same group (exact match)
  • By cast type: matches all settings with the same cast type (exact match)
  • By stale: type 'stale' and get all settings that haven't been updated in X days (X is defined in the configuration)
  • By recent: type 'recent X' where X is the number of hours and get all settings that have been updated since X hours ago

Manipulating settings in the code

Settings can be read or set via their interface. Notice that when reading a setting's value, it will be cast to the appropriate type using the "cast_type" field.

All possible cast types as well as detailed examples of usage can be found in the wiki.

# Get setting value with appropriate cast type
#
# Returns setting value with cast or yields it if passed a block
Sail.get(:name)

# This usage will return the result of the block
Sail.get(:name) do |setting_value|
  my_code(setting_value)
end

# When the optional argument expected_errors is passed,
# Sail will count the number of unexpected errors raised inside a given block.
# After the number of unexpected errors reaches the amount configured in
# failures_until_reset, it will automatically trigger a Sail.reset for that setting.

# For example, this will ignore ExampleError, but any other error raised will increase
# the count until the setting "name" is reset.
Sail.get(:name, expected_errors: [ExampleError]) do |value|
  code_that_can_raise_example_error(value)
end

# Set setting value
Sail.set(:name, "value")

# Reset setting value (requires the sail.yml file!)
Sail.reset(:name)

# Switcher
# This method will take three setting names as parameters
# positive: This is the name of the setting that will be returned if the throttle setting returns true
# negative: This is the name of the setting that will be returned if the throttle setting returns false
# throttle: A setting of cast_type throttle that will switch between positive and negative
#
# return: Value with cast of either positive or negative, depending on the randomized value of throttle
# Settings positive and negative do not have to be of the same type. However, throttle must be a throttle type setting

Sail.switcher(
  positive: :setting_name_for_true,
  negative: :setting_name_for_false,
  throttle: :throttle_setting_name
)

Sail also comes with a JSON API for manipulating settings. It is simply an interface for the methods described above.

GET sail/settings/:name

Response
{
  "value": true
}

PUT sail/settings/:name?value=something

Response
200 OK

GET sail/settings/switcher/:positive/:negative/:throttled_by

Response
{
  "value": "Some value that depends on the setting combination passed"
}

Switching to a specific settings profile

PUT sail/profiles/:name/switch

Response
200 OK

GraphQL

For GraphQL APIs, types and mutations are defined for convenience. Include Sail's Graphql modules to get the appropriate fields.

# app/graphql/types/query_type.rb

module Types
  class QueryType < Types::BaseObject
    include Sail::Graphql::Types
  end
end

# app/graphql/types/mutation_type.rb

module Types
  class MutationType < Types::BaseObject
    include Sail::Graphql::Mutations
  end
end

To query settings via GraphQL, use the following pattern.

query {
    sailGet(name: "my_setting")
    sailSwitcher(
        positive: "positive_case_setting"
        negative: "negative_case_setting"
        throttledBy: "throttle_setting"
    )
}

mutation {
    sailSet(name: "my_setting", value: "value") {
        success
    }

    sailProfileSwitch(name: "my_profile") {
        success
    }
}

Localization

Sail's few strings are all localized for English in en.yml. Using the same YAML keys for other languages should work for localizing the dashboard.

Make sure to pass in the desired locale as a parameter.

Credits

The awesome icons used by Sail's admin panel are all made by Font Awesome.

Contributing

Contributions are very welcome! Don't hesitate to ask if you wish to contribute, but don't yet know how.

Please refer to this simple guideline.

sail's People

Contributors

andyrosenberg avatar asko-ohmann avatar chaadow avatar denny avatar dependabot-preview[bot] avatar dependabot[bot] avatar dersnek avatar johnthethird avatar oleg-kiviljov avatar rohandaxini avatar vikasnautiyal avatar vinistock avatar zvlex avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

sail's Issues

Implement profiles

Context

Eventually, it can be useful to save the entire state of the configuration for an application. For instance, if testing the effects of combinations of numerous settings, switching each individual setting can be a time consuming manual task.

Profiles will allow saving the current state of all settings. Switching between profiles will also be available. This can also benefit automated test suites to explore different scenarios by saving profiles with many different setting combinations.

Target version is 3.0.0 (will require extra migrations and therefore will include breaking changes).

Feature

By clicking a cog button in the dashboard, I want a menu to show up allowing me to save, delete, edit and switch between profiles easily.

As a test engineer, I want be able to switch profiles via JSON API.

Feedback and suggestions

Please feel free to provide feedback or suggestions as these are very helpful when designing new features (thumbs up or down included).

Create A/B test cast type

A/B test flags are useful for validating features live. Sail should support A/B testing out of the box.

Expected behavior

When an ab_test setting has its value as 'true' in the database, the get method should randomly return true or false. This can easily be achieve using

[true, false].sample

When an ab_test setting has its value as 'false' in the database, the get method should always return false (Boolean).

In summary, they are basically a Boolean setting, but when the value is 'true', it will randomize between true or false.

Steps

  • Add ab_test to cast types in Setting class
  • In Setting.get, add logic to randomize true or false or always return false depending on setting value
  • Make sure ab_test settings are validated with value_is_true_or_false
  • In the _setting.html.erb partial, make sure ab_test types are displayed exactly like Booleans

Reducing visual complexity: Disable save buttons until setting is changed? Hide type label?

Hiya :) I've been thinking about ways to make the settings page less busy - once you fill it up with cards there's a lot of detail on there to take in at a glance.

One thing that I thought might help was if the settings card SAVE buttons were all in a disabled greyed-out state initially, only changing to active eye-catching tangerine when that particular setting has been changed.

Relatedly, I'd be interested in having the option to show/hide different parts of the card UI, maybe as a set of config options... or even a way to override the card template? For instance, I don't think the end-users of my CMS will care about whether a setting is a 'boolean', a 'float', etc, so that label is one detail I would hide by default - again to reduce the overall complexity of the settings page.

Unable to use Sail v1.5.0

Hello @vinistock

After installing the latest version of sail gem 1.5.0, I am facing lot of problems now on my Rails 4 application.

  1. From rails console I am unable to create any setting. Following is the error 👇
2.2.2 :001 > Sail::Setting.create(name: :my_setting, cast_type: :integer, description: 'A very important setting', value: '15')
SyntaxError: /Users/rohan/.rvm/gems/ruby-2.2.2/gems/sail-1.5.0/app/models/sail/setting.rb:62: syntax error, unexpected '.'
...ere(name: throttled_by).first&.throttle?
...                               ^
	from /Users/rohan/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.4/lib/active_support/dependencies.rb:457:in `load'
	from /Users/rohan/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.4/lib/active_support/dependencies.rb:457:in `block in load_file'
	from /Users/rohan/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.4/lib/active_support/dependencies.rb:647:in `new_constants_in'
	from /Users/rohan/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.4/lib/active_support/dependencies.rb:456:in `load_file'
	from /Users/rohan/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.4/lib/active_support/dependencies.rb:354:in `require_or_load'
	from /Users/rohan/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.4/lib/active_support/dependencies.rb:494:in `load_missing_constant'
	from /Users/rohan/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.4/lib/active_support/dependencies.rb:184:in `const_missing'
	from (irb):1
	from /Users/rohan/.rvm/gems/ruby-2.2.2/gems/railties-4.2.4/lib/rails/commands/console.rb:110:in `start'
	from /Users/rohan/.rvm/gems/ruby-2.2.2/gems/railties-4.2.4/lib/rails/commands/console.rb:9:in `start'
	from /Users/rohan/.rvm/gems/ruby-2.2.2/gems/railties-4.2.4/lib/rails/commands/commands_tasks.rb:68:in `console'
	from /Users/rohan/.rvm/gems/ruby-2.2.2/gems/railties-4.2.4/lib/rails/commands/commands_tasks.rb:39:in `run_command!'
	from /Users/rohan/.rvm/gems/ruby-2.2.2/gems/railties-4.2.4/lib/rails/commands.rb:17:in `<top (required)>'
	from script/rails:6:in `require'
	from script/rails:6:in `<main>'
  1. I recreated migrations and installed sail from scratch on my application for v1.5.0 and now when I navigate to http://localhost:3000/sail then I face following errors 👇

screen shot 2018-12-09 at 6 27 37 pm

Can you guide me if I am missing any step?

Propose new setting types

Description

The idea of Sail is to be a centralized source of live configurations for Rails applications. More setting types can make the configurations simpler and require less application code to be written.

Request

Propose new types of settings that could be useful having in Sail. Your proposal can either be a written idea as a comment in this issue or as a pull request implementing the new type.

Existing types can be found in the readme.

Issues with sessions when using `redis_store` and custom session key names

Describe the bug
Hey guys 👋 . Recently we've migrated from using ActiveRecord Session Store (https://github.com/rails/activerecord-session_store) to redis_store (https://github.com/redis-store/redis-actionpack). After this change our Sail Dashboard stopped working:

Zrzut ekranu 2022-04-28 o 10 00 48

When debugging from within Sails:

[1] pry(#<Sail::SettingsController>)> session.options
=> #<ActionDispatch::Request::Session::Options:0x00007fa5b36098b0
 @by=
  #<ActionDispatch::Session::RedisStore:0x00007fa593577a48
   @app=#<ActionDispatch::Routing::RouteSet:0x00007fa571addf18>,
   @conn=
    #<Redis::Rack::Connection:0x00007fa5935771b0
     @options=
      {:path=>"/", :domain=>nil, :expire_after=>nil, :secure=>false, :httponly=>true, :defer=>false, :renew=>false, :redis_server=>nil},
     @pool=nil,
     @pooled=false,
     @store=#<Redis client v4.6.0 for redis://0.0.0.0:6379/0>>,
   @cookie_only=true,
   @default_options=
    {:path=>"/", :domain=>nil, :expire_after=>nil, :secure=>false, :httponly=>true, :defer=>false, :renew=>false, :redis_server=>nil},
   @key="_session_id",
   @mutex=#<Thread::Mutex:0x00007fa5935771d8>,
   @same_site=nil>,
 @delegate=
  {:path=>"/", :domain=>nil, :expire_after=>nil, :secure=>false, :httponly=>true, :defer=>false, :renew=>false, :redis_server=>nil}>

What I've noticed - the session key ("_session_id") looks like kind of a default key - in our configuration we are using different key name, so when we would try to fetch sesssion.id we'll get nil, because parent app stores session under different key.

When I've updated this line

config.middleware.use Rails.application.config.session_store || ActionDispatch::Session::CookieStore
to

if Rails.application.config.session_store.nil?
 config.middleware.use ActionDispatch::Session::CookieStore
end

Everything works as expected - dashboard loads, and when debugging and checking whats inside session object:

=> #<ActionDispatch::Request::Session::Options:0x00007fb952e3a720
 @by=
  #<ActionDispatch::Session::RedisStore:0x00007fb9e2a5fa98
   @app=
    #<ActionDispatch::ContentSecurityPolicy::Middleware:0x00007fb9e2a5fcc8
     @app=
      #<Rack::Head:0x00007fb9e2a5fed0
       @app=
        #<Rack::ConditionalGet:0x00007fb9e2a5ff48
         @app=
          #<Rack::ETag:0x00007fb9e2a54008
           @app=
            #<Rack::TempfileReaper:0x00007fb9e2a54058
             @app=
              #<Warden::Manager:0x00007fb9e2a54170
               @app=
                #<ApolloUploadServer::Middleware:0x00007fb9e2a541c0
                 @app=
                  #<Warden::JWTAuth::Middleware:0x00007fb9e2a54210
                   @app=#<Bullet::Rack:0x00007fb9e2a54260 @app=#<ActionDispatch::Routing::RouteSet:0x00007fb984297210>>>>,
               @config=
                {:default_scope=>:account,
                 :scope_defaults=>{},
                 :default_strategies=>
                  {:account=>[:jwt, :database_authenticatable],
                   ...},
                 :intercept_401=>false,
                 :failure_app=>#<Devise::Delegator:0x00007fb98468e3f0>}>>,
           @cache_control="max-age=0, private, must-revalidate",
           @no_cache_control="no-cache">>>>,
   @conn=
    #<Redis::Rack::Connection:0x00007fb9e2a5f368
     @options=
      {:path=>"/",
       :domain=>nil,
       :expire_after=>1 minute,
       :secure=>false,
       :httponly=>true,
       :defer=>false,
       :renew=>false,
       :redis_server=>["redis://0.0.0.0:6379/0/session"],
       :servers=>["redis://0.0.0.0:6379/0/session"],
       :threadsafe=>true},
     @pool=nil,
     @pooled=false,
     @store=#<Redis client v4.6.0 for redis://0.0.0.0:6379/0>>,
   @cookie_only=true,
   @default_options=
    {:path=>"/",
     :domain=>nil,
     :expire_after=>1 minute,
     :secure=>false,
     :httponly=>true,
     :defer=>false,
     :renew=>false,
     :redis_server=>["redis://0.0.0.0:6379/0/session"],
     :servers=>["redis://0.0.0.0:6379/0/session"],
     :threadsafe=>true},
   @key="_hub_session",
   @mutex=#<Thread::Mutex:0x00007fb9e2a5f390>,
   @same_site=nil>,
 @delegate=
  {:path=>"/",
   :domain=>nil,
   :expire_after=>1 minute,
   :secure=>false,
   :httponly=>true,
   :defer=>false,
   :renew=>false,
   :redis_server=>["redis://0.0.0.0:6379/0/session"],
   :servers=>["redis://0.0.0.0:6379/0/session"],
   :threadsafe=>true,
   :id=>"7f9d96ee441b7b418f5f2573263fa03c"}>
[7] pry(#<Sail::SettingsController>)>

So here I can see way more details and options, also the key matches the one we've configured ("_hub_session"). Looks like when we are adding a middleware in after_initialize block it somehow overrides some of the configs from the base/parent app 🤔 .

Environment
Gemfile

source 'https://rubygems.org'

ruby '2.7.6'
gem 'rails', '~> 6.0.0'
gem 'redis-actionpack'
gem 'sail'
...

To Reproduce
Steps to reproduce the behavior:
Install 'redis-actionpack' gem,
create app/initializers/session_store.rb

opts = {
    key: '_hub_session',
    servers: ['redis_url'],
    expire_after: 1.hour,
    threadsafe: true,
    secure: !(Rails.env.development? || Rails.env.test?)
  }
  Rails.application.config.session_store :redis_store, opts

routes:

...
authenticate :user, ->(u) { u.admin? } do
    mount Sail::Engine => '/sail'
...

Login and visit localhost:3000/sail

Expected behavior
User should see a Sail Dashboard

Actual behavior
User sees an error page.

Create cron cast type

It would be interesting to have a cron setting type. The definitions for this new type are as follows

Expected behavior

Will work very similar to the string cast type, but must only keep crons (validations required for before saving to the database).

When returning the setting value, if DateTime.now.utc matches the cron, return true. Otherwise, return false.

Steps

  • Add cast type cron to the Setting class
  • In the Setting.get method, use the fugit gem to check if the saved cron matches DateTime.now.utc
  • Add before_save validation to check if string is a valid cron (validation should only run for cron cast_type settings)

Add a notification mechanism for setting updates

Context

Depending on how critical changing a setting in a live application may be, other admins or groups might want to be notified of such changes.

Therefore, it can be useful to have a notification system that will push messages to tools such as Slack, Hipchat or Microsoft teams to warn specific channels or people about changes made in their production settings.

Request

Investigate what is needed to create an abstract notification system that can push messages to multiple services (somewhat similar to what active_job does for using different queues). This abstraction can be made into a new gem for generic purposes.

Ideally, a Sail user would only need the following to receive the messages:

  • Pick which service they want in their Sail configure block (e.g.: config.notifier = { service: :slack, groups: %i[feature_flags] })
  • Add the gem responsible for the notifier service to their gemfile (e.g.: gem "slack-ruby-client"
  • Make the needed credentials to publish messages accessible to Sail

This will probably require writing wrapper classes for each service. Additionally, define which services Sail will support on the first release of this new feature.

Deprecated syntax for implementing auth lambda fails in Ruby 3.x

The to_prepare in engine.rb method uses an implicit block to set the auth lambda, which isn't allowed in Ruby 3.x (and is flagged by rubocop in 2.7.2, I think).

To Reproduce

Set config.dashboard_auth_lambda in a Ruby 3.x application and try to run it; it exits with the following error:

/home/denny/code/sail/lib/sail/engine.rb:54:in `new': tried to create Proc object without a block (ArgumentError)
  from /home/denny/code/sail/lib/sail/engine.rb:54:in `to_prepare'
  from /home/denny/code/sail/lib/sail/engine.rb:33:in `block in <class:Engine>'
  [...]

Migration issue

Hi @vinistock Seems we have an issue with class name generated due to migration step for sail configuration settings.

When we run the step rails g sail my_desired_migration_name and let's say we name our migration as for_sail i.e. rails g sail for_sail then it generates following migration file.

class CreateSailSettings < ActiveRecord::Migration
  def change
    create_table :sail_settings do |t|
      t.string :name, null: false
      t.text :description
      t.string :value, null: false
      t.integer :cast_type, null: false, limit: 1
      t.timestamps
      t.index ["name"], name: "index_settings_on_name", unique: true
    end
  end
end

But when we run rake db:migrate then this throws an error as

NameError: uninitialized constant ForSail

Here the class name is CreateSailSettings which should be ideally ForSail in this case. I had to manually modify the class name i.e. top line in the migration to be class ForSail < ActiveRecord::Migration and then the migrations started working.

Make Values Nullable

We are planning to use Sail to allow support/QA team make some configurable changes easily. Some of those configs need to be set for specific case/time and then reset/unset it. The problem is, in order to unset cofig value, we need to remove the config and the team need to remember the exact config name to set it again.

We want to make values nullable, so that we can keep the attributes without any values. They will return nil if accessed. Add Unset & Reset to default to revert the values.

We are making the above mentioned changes inorder to support our use cases, let me know if they seems like good addition to Sail.

Redis cache store is unsupported

Describe the bug
ActionController::Base#expire_fragment allow to pass regex but it doesn't work with redis cache story

ActionController::Base.new.expire_fragment(/name: "#{setting_name}"/)

To Reproduce
Steps to reproduce the behavior:

  1. Add flag
  2. Use redis as rails cache store
  3. Read flag more than USAGES_UNTIL_CACHE_EXPIRE by using
Sail.get(flag_name)

Expected behavior
Exception shouldn't raise

Actual behavior
Exception ArgumentError: Only Redis glob strings are supported: /name: "flag_name"/ is raised

Monitor mode button misplaced for Safari

The "Monitor mode" button in the navbar is supposed to be aligned with the button "Guide". However, in Safari "Monitor mode" is below "Guide" by a few pixels.

Both buttons should be vertically centered within the navbar.

Invalid SQL generated in index

  ↳ /Users/plbelt/.rvm/gems/ruby-2.6.0@learning_office/bundler/gems/sail-f88cbb636bcd/app/controllers/sail/settings_controller.rb:18```

The issue is an ambiguous `updated_at` column created by the nested-selected when using `fresh_when(@settings)` ... when `@settings = Setting.by_query...` already includes that column.

Add a monitor mode for the dashboard

Context

A lot of information in the dashboard is really only useful for editing settings. If someone desires to simply put the dashboard on a big screen and keep it as a monitor, the view can be much simpler with bigger fonts to make it extra simple to spot the values of each setting. Also, more settings can fit the same page.

Request

Add a button to the nav bar to enter monitor mode. Monitor mode will only include the name and value of each setting in their cards, use bigger fonts and increase the number of settings per page. No setting management mechanisms, like inputs, buttons or labels, should be visible in the monitor mode.

Feedback

As always, feedback and thoughts on the new feature are much appreciated.

Avoid using SettingsController#value

Here:

def value
if params[:cast_type] == Sail::ConstantCollection::BOOLEAN
params[:value] == Sail::ConstantCollection::ON
else
params[:value]
end
end

The controller should not be manipulating values for the model to save.
This makes writing test difficult, moreover the validation in model layer is enough to only store value as 'true' or 'false'.

This method could be avoided or moved to some better place

Fix build stability issues

Some specs flaky specs are breaking way too often in travis. They need to be adapted so that they don't break so regularly.

  1. spec/features/managing_profiles_feature_spec.rb:71 can't find profiles modal
  2. spec/features/quick_guide_feature_spec.rb:8 can't find quick guide
  3. spec/features/sorting_settings_feature_spec.rb:37 can't find sorting menu

Problem with sessions

Hi; I've got this in routes.rb, and I haven't overridden any settings so far:
mount Sail::Engine, at: '/admin/settings'

When I try to load http://localhost:3000/admin/settings from a test I get this backtrace:

     TypeError:
       can't cast Rack::Session::SessionId
     # /home/denny/.rvm/gems/ruby-3.0.0/gems/rack-2.2.3/lib/rack/session/abstract/id.rb:388:in `commit_session'
     # /home/denny/.rvm/gems/ruby-3.0.0/gems/rack-2.2.3/lib/rack/session/abstract/id.rb:268:in `context'
     # /home/denny/.rvm/gems/ruby-3.0.0/gems/rack-2.2.3/lib/rack/session/abstract/id.rb:260:in `call'
     # /home/denny/.rvm/gems/ruby-3.0.0/gems/railties-6.1.0/lib/rails/rack/logger.rb:37:in `call_app'
     # /home/denny/.rvm/gems/ruby-3.0.0/gems/railties-6.1.0/lib/rails/rack/logger.rb:26:in `block in call'
     # /home/denny/.rvm/gems/ruby-3.0.0/gems/railties-6.1.0/lib/rails/rack/logger.rb:26:in `call'
     # /home/denny/.rvm/gems/ruby-3.0.0/gems/ahoy_matey-3.1.0/lib/ahoy/engine.rb:22:in `call_with_quiet_ahoy'
     # /home/denny/.rvm/gems/ruby-3.0.0/gems/request_store-1.5.0/lib/request_store/middleware.rb:19:in `call'
     # /home/denny/.rvm/gems/ruby-3.0.0/gems/rack-2.2.3/lib/rack/method_override.rb:24:in `call'
     # /home/denny/.rvm/gems/ruby-3.0.0/gems/rack-2.2.3/lib/rack/runtime.rb:22:in `call'
     # /home/denny/.rvm/gems/ruby-3.0.0/gems/rack-2.2.3/lib/rack/sendfile.rb:110:in `call'
     # /home/denny/.rvm/gems/ruby-3.0.0/gems/railties-6.1.0/lib/rails/engine.rb:539:in `call'
     # /home/denny/.rvm/gems/ruby-3.0.0/gems/rack-test-1.1.0/lib/rack/mock_session.rb:29:in `request'
     # /home/denny/.rvm/gems/ruby-3.0.0/gems/rack-test-1.1.0/lib/rack/test.rb:266:in `process_request'
     # /home/denny/.rvm/gems/ruby-3.0.0/gems/rack-test-1.1.0/lib/rack/test.rb:119:in `request'
     # ./spec/requests/admin/sail_settings_spec.rb:20:in `block (3 levels) in <top (required)>'

I'm a bit out of my depth here, but I was wondering if it was because I'm using ActiveRecord::SessionStore and you're using ActionDispatch::Session::CookieStore ?

If I hit the URL with a browser, I get a longer backtrace in the logs that mentions both...

Started GET "/admin/settings" for ::1 at 2021-01-04 20:39:22 +0000
   (0.5ms)  SELECT "schema_migrations"."version" FROM "schema_migrations" ORDER BY "schema_migrations"."version" ASC
  
TypeError (can't cast Rack::Session::SessionId):
  
activerecord (6.1.0) lib/active_record/connection_adapters/abstract/quoting.rb:246:in `_type_cast'
activerecord (6.1.0) lib/active_record/connection_adapters/postgresql/quoting.rb:162:in `_type_cast'
activerecord (6.1.0) lib/active_record/connection_adapters/abstract/quoting.rb:43:in `type_cast'
activerecord (6.1.0) lib/active_record/connection_adapters/abstract/quoting.rb:205:in `block in type_casted_binds'
activerecord (6.1.0) lib/active_record/connection_adapters/abstract/quoting.rb:203:in `map'
activerecord (6.1.0) lib/active_record/connection_adapters/abstract/quoting.rb:203:in `type_casted_binds'
activerecord (6.1.0) lib/active_record/connection_adapters/postgresql_adapter.rb:689:in `exec_cache'
activerecord (6.1.0) lib/active_record/connection_adapters/postgresql_adapter.rb:657:in `execute_and_clear'
activerecord (6.1.0) lib/active_record/connection_adapters/postgresql/database_statements.rb:53:in `exec_query'
activerecord (6.1.0) lib/active_record/connection_adapters/abstract/database_statements.rb:536:in `select_prepared'
activerecord (6.1.0) lib/active_record/connection_adapters/abstract/database_statements.rb:67:in `select_all'
activerecord (6.1.0) lib/active_record/connection_adapters/abstract/query_cache.rb:101:in `block in select_all'
activerecord (6.1.0) lib/active_record/connection_adapters/abstract/query_cache.rb:118:in `block in cache_sql'
activesupport (6.1.0) lib/active_support/concurrency/load_interlock_aware_monitor.rb:26:in `block (2 levels) in synchronize'
activesupport (6.1.0) lib/active_support/concurrency/load_interlock_aware_monitor.rb:25:in `handle_interrupt'
activesupport (6.1.0) lib/active_support/concurrency/load_interlock_aware_monitor.rb:25:in `block in synchronize'
activesupport (6.1.0) lib/active_support/concurrency/load_interlock_aware_monitor.rb:21:in `handle_interrupt'
activesupport (6.1.0) lib/active_support/concurrency/load_interlock_aware_monitor.rb:21:in `synchronize'
activerecord (6.1.0) lib/active_record/connection_adapters/abstract/query_cache.rb:109:in `cache_sql'
activerecord (6.1.0) lib/active_record/connection_adapters/abstract/query_cache.rb:101:in `select_all'
activerecord (6.1.0) lib/active_record/querying.rb:47:in `find_by_sql'
activerecord (6.1.0) lib/active_record/relation.rb:850:in `block in exec_queries'
activerecord (6.1.0) lib/active_record/relation.rb:868:in `skip_query_cache_if_necessary'
activerecord (6.1.0) lib/active_record/relation.rb:835:in `exec_queries'
activerecord (6.1.0) lib/active_record/relation.rb:638:in `load'
activerecord (6.1.0) lib/active_record/relation.rb:249:in `records'
activerecord (6.1.0) lib/active_record/relation.rb:244:in `to_ary'
activerecord (6.1.0) lib/active_record/relation/finder_methods.rb:553:in `find_nth_with_limit'
activerecord (6.1.0) lib/active_record/relation/finder_methods.rb:538:in `find_nth'
activerecord (6.1.0) lib/active_record/relation/finder_methods.rb:122:in `first'
activerecord-session_store (f188efbc49a5) lib/active_record/session_store/session.rb:51:in `find_by_session_id'
activerecord-session_store (f188efbc49a5) lib/active_record/session_store/session.rb:28:in `find_by_session_id'
activerecord-session_store (f188efbc49a5) lib/action_dispatch/session/active_record_store.rb:120:in `block in get_session_model'
activesupport (6.1.0) lib/active_support/logger_silence.rb:18:in `block in silence'
activesupport (6.1.0) lib/active_support/logger_thread_safe_level.rb:52:in `log_at'
activesupport (6.1.0) lib/active_support/logger_silence.rb:18:in `silence'
activesupport (6.1.0) lib/active_support/logger.rb:64:in `block (3 levels) in broadcast'
activesupport (6.1.0) lib/active_support/logger_silence.rb:18:in `block in silence'
activesupport (6.1.0) lib/active_support/logger_thread_safe_level.rb:52:in `log_at'
activesupport (6.1.0) lib/active_support/logger_silence.rb:18:in `silence'
activesupport (6.1.0) lib/active_support/logger.rb:62:in `block (2 levels) in broadcast'
activerecord-session_store (f188efbc49a5) lib/action_dispatch/session/active_record_store.rb:119:in `get_session_model'
activerecord-session_store (f188efbc49a5) lib/action_dispatch/session/active_record_store.rb:136:in `find_session'
rack (2.2.3) lib/rack/session/abstract/id.rb:314:in `load_session'
actionpack (6.1.0) lib/action_dispatch/middleware/session/abstract_store.rb:45:in `block in load_session'
actionpack (6.1.0) lib/action_dispatch/middleware/session/abstract_store.rb:53:in `stale_session_check!'
actionpack (6.1.0) lib/action_dispatch/middleware/session/abstract_store.rb:45:in `load_session'
actionpack (6.1.0) lib/action_dispatch/request/session.rb:234:in `load!'
actionpack (6.1.0) lib/action_dispatch/request/session.rb:226:in `load_for_read!'
actionpack (6.1.0) lib/action_dispatch/request/session.rb:144:in `to_hash'
actionpack (6.1.0) lib/action_dispatch/request/session.rb:217:in `merge!'
actionpack (6.1.0) lib/action_dispatch/request/session.rb:217:in `merge!'
actionpack (6.1.0) lib/action_dispatch/request/session.rb:19:in `create'
actionpack (6.1.0) lib/action_dispatch/middleware/session/abstract_store.rb:71:in `prepare_session'
rack (2.2.3) lib/rack/session/abstract/id.rb:265:in `context'
rack (2.2.3) lib/rack/session/abstract/id.rb:260:in `call'
actionpack (6.1.0) lib/action_dispatch/middleware/cookies.rb:689:in `call'
railties (6.1.0) lib/rails/engine.rb:539:in `call'
railties (6.1.0) lib/rails/railtie.rb:207:in `public_send'
railties (6.1.0) lib/rails/railtie.rb:207:in `method_missing'
actionpack (6.1.0) lib/action_dispatch/routing/mapper.rb:20:in `block in <class:Constraints>'
actionpack (6.1.0) lib/action_dispatch/routing/mapper.rb:49:in `serve'
actionpack (6.1.0) lib/action_dispatch/journey/router.rb:50:in `block in serve'
actionpack (6.1.0) lib/action_dispatch/journey/router.rb:32:in `each'
actionpack (6.1.0) lib/action_dispatch/journey/router.rb:32:in `serve'
actionpack (6.1.0) lib/action_dispatch/routing/route_set.rb:842:in `call'
warden (1.2.9) lib/warden/manager.rb:36:in `block in call'
warden (1.2.9) lib/warden/manager.rb:34:in `catch'
warden (1.2.9) lib/warden/manager.rb:34:in `call'
rack (2.2.3) lib/rack/tempfile_reaper.rb:15:in `call'
rack (2.2.3) lib/rack/etag.rb:27:in `call'
rack (2.2.3) lib/rack/conditional_get.rb:27:in `call'
rack (2.2.3) lib/rack/head.rb:12:in `call'
actionpack (6.1.0) lib/action_dispatch/http/permissions_policy.rb:22:in `call'
actionpack (6.1.0) lib/action_dispatch/http/content_security_policy.rb:18:in `call'
rack (2.2.3) lib/rack/session/abstract/id.rb:266:in `context'
rack (2.2.3) lib/rack/session/abstract/id.rb:260:in `call'
actionpack (6.1.0) lib/action_dispatch/middleware/cookies.rb:689:in `call'
activerecord (6.1.0) lib/active_record/migration.rb:601:in `call'
actionpack (6.1.0) lib/action_dispatch/middleware/callbacks.rb:27:in `block in call'
activesupport (6.1.0) lib/active_support/callbacks.rb:98:in `run_callbacks'
actionpack (6.1.0) lib/action_dispatch/middleware/callbacks.rb:26:in `call'
actionpack (6.1.0) lib/action_dispatch/middleware/executor.rb:14:in `call'
actionpack (6.1.0) lib/action_dispatch/middleware/actionable_exceptions.rb:18:in `call'
actionpack (6.1.0) lib/action_dispatch/middleware/debug_exceptions.rb:29:in `call'
actionpack (6.1.0) lib/action_dispatch/middleware/show_exceptions.rb:33:in `call'
railties (6.1.0) lib/rails/rack/logger.rb:37:in `call_app'
railties (6.1.0) lib/rails/rack/logger.rb:26:in `block in call'
activesupport (6.1.0) lib/active_support/tagged_logging.rb:99:in `block in tagged'
activesupport (6.1.0) lib/active_support/tagged_logging.rb:37:in `tagged'
activesupport (6.1.0) lib/active_support/tagged_logging.rb:99:in `tagged'
railties (6.1.0) lib/rails/rack/logger.rb:26:in `call'
ahoy_matey (3.1.0) lib/ahoy/engine.rb:22:in `call_with_quiet_ahoy'
sprockets-rails (3.2.2) lib/sprockets/rails/quiet_assets.rb:13:in `call'
actionpack (6.1.0) lib/action_dispatch/middleware/remote_ip.rb:81:in `call'
request_store (1.5.0) lib/request_store/middleware.rb:19:in `call'
actionpack (6.1.0) lib/action_dispatch/middleware/request_id.rb:26:in `call'
rack (2.2.3) lib/rack/method_override.rb:24:in `call'
rack (2.2.3) lib/rack/runtime.rb:22:in `call'
activesupport (6.1.0) lib/active_support/cache/strategy/local_cache_middleware.rb:29:in `call'
actionpack (6.1.0) lib/action_dispatch/middleware/executor.rb:14:in `call'
actionpack (6.1.0) lib/action_dispatch/middleware/static.rb:24:in `call'
rack (2.2.3) lib/rack/sendfile.rb:110:in `call'
actionpack (6.1.0) lib/action_dispatch/middleware/host_authorization.rb:98:in `call'
webpacker (5.2.1) lib/webpacker/dev_server_proxy.rb:25:in `perform_request'
rack-proxy (0.6.5) lib/rack/proxy.rb:57:in `call'
railties (6.1.0) lib/rails/engine.rb:539:in `call'
puma (5.1.1) lib/puma/configuration.rb:246:in `call'
puma (5.1.1) lib/puma/request.rb:76:in `block in handle_request'
puma (5.1.1) lib/puma/thread_pool.rb:337:in `with_force_shutdown'
puma (5.1.1) lib/puma/request.rb:75:in `handle_request'
puma (5.1.1) lib/puma/server.rb:431:in `process_client'
puma (5.1.1) lib/puma/thread_pool.rb:145:in `block in spawn_thread'

Let me know if I'm doing something stupid :) or if there's any useful info I can provide otherwise.

Option to make pagination more compact

Currently if you have lot of settings the pagination takes too much space.

Here is an example of the issue:

U9RTAEvA

Probably would be better to have the pagination in format < 1,2,3,4,5 ... > if the page number exceeds certain threshold.

Make Sail work with Rails API mode

At the moment, Sail does not work with an application created using the --api option. This is due to missing middleware and libraries that are not included in the API mode.

It would be great if Sail could resolve the missing requirements on its own and work out of the box for API only applications since they can also benefit from configurations.

NameError: uninitialized constant Sail::Types when visiting the dashboard

Hello! I have created 1 test setting in sail.yml and loaded it.

  description: Name of the portal
  value: monestro
  cast_type: string
  group: portal_settings

But now when I try to open the dashboard I get the error mentioned in header.

the gem version is 3.0.1, mounted like this:

constraints subdomain: ['backoffice', 'staging-backoffice'] do
    … 
    constraints(Clearance::Constraints::SignedIn.new { |user| user.staff? }) do
     …
        namespace :admin do
  	      mount Sail::Engine => '/sail'
	end
     end
end

Really seems I am not doing anything fancy, so I don't understand why constant resolution for "Sail::Types" does not work

Unpermitted params

When I try to complete any action in the settings dashboard, the action fails and I get a strong params error in my logs:

ActionController::UnpermittedParameters (found unpermitted parameters: :_method, :locale):

I'll push up a PR for this in a few minutes, assuming it's as simple as it sounds 🙂 🤞🏼

Create show action for settings

Sail needs to provide the ability of reading settings' values via javascript. Therefore, we need a show action in settings controller to return settings information as JSON.

Steps

  • Add show to the settings resources in routes.rb
  • Create show action in settings controller. It should use the name parameter to find the setting and return its value in a JSON like { "value": true }. It should only accept JSON format as well.
  • Don't forget to write the specs in settings_controller_spec.rb

Add a quick reference guide to the dashboard

Context

Often, the people responsible for managing configurations in production like environments are not technical or do not engage directly with coding.

Therefore, it is unlikely that any non-technical admin will look for the gem's README or Wiki for support on its usage. Having a quick reference guide can make sure users will make the most out of Sail.

Request

Add a "Guide" link to the nav bar on the right that will display a modal containing a quick reference guide on how to use the dashboard. The guide should include all the following information in a concise and clear manner:

  • The list of all groups
  • The list of all cast types (not all available, but all the ones used in settings)
  • Information on what the labels mean
  • Information on how to use the search
  • Information on how to use profiles
  • Information on what the relevancy score is

Feedback

As always, feedback and thoughts on the new feature are much appreciated.

Rails 6.0 compatibility issues in migrations

Hi @vinistock

While installing sail in my newer Rails 6.0 application, I faced migration issues after step rails g sail:install. The errors while running rails db:migrate is

StandardError: An error has occurred, this and all later migrations canceled:

Directly inheriting from ActiveRecord::Migration is not supported. Please specify the Rails release the migration was written for:

  class CreateSailSettings < ActiveRecord::Migration[4.2]
...
...
...
...

I fixed it on my local by just inheriting it using ActiveRecord::Migration[6.0] instead of ActiveRecord::Migration. i.e. I edited both the migration files for CreateSailSettings class and CreateSailProfiles class and inherited them from ActiveRecord::Migration[6.0]. This now allows me to run the migrations successfully.

Should we start working on to make it Rails 6.0 compatible? 😄

BTW, last year we both collaborated on your sail gem for one of the features and few fixes.

Why cache if the db is still being hit?

Hello, could you please explain what purpose does the caching serve if in order to retrieve the value of the setting the query is still being made?

Talking about get method in Sail::Setting model

    def self.get(name)
      Sail.instrumenter.increment_usage_of(name)
      setting = Setting.for_value_by_name(name).first
      return if setting.nil?

      if setting.should_not_cache?
        setting.safe_cast
      else
        Rails.cache.fetch("setting_get_#{name}", expires_in: Sail.configuration.cache_life_span) do
          setting.safe_cast
        end
      end
    end

Doesn't this defeat purpose of caching?

Thank you for an awesome gem and sorry if this is a stupid question and I just miss something :)

Caching a setting view fragment doesn't work because pagination doesn't return setting id

The caching in setting view results in multiple copies of the same settging

<% cache setting, expires_in: Sail.configuration.cache_life_span do %>

I think the reason is in Setting.paginated method

scope :paginated, lambda { |page, per_page|
select(:name, :description, :group, :value, :cast_type, :updated_at)
.offset(page.to_i * per_page)
.limit(per_page)
}

The cache must rely on the entity id to generate the cache key but the id is not present in paginated result.

Add support for graphql

It would be nice if Sail provided the appropriate types to be used by applications that have a GraphQL based backend. I'm not sure if defining the types inside the engine is enough or if any code would be required in the main app.

Some possibilities that can be explored:

  1. Figure out a way of defining all that is needed from within the engine
  2. Provide the types and add documentation on how to add them to the GraphQL endpoints so that developers know how to use it
  3. Create a generator that will install Sail types in the main application

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.