Ruby MIT

madmin

A robust Admin Interface for Ruby on Rails apps

E

excid3

Dernière activité 27 août 2026
excid3/madmin

761

étoiles

102

forks

10

issues ouvertes

adminadmin-dashboardhacktoberfestrailsrubyruby-on-rails

Ce README est souvent en anglais.

Madmin

🛠 A robust Admin Interface for Ruby on Rails apps

Build Status Gem Version

Why another Ruby on Rails admin? We wanted an admin that was:

  • Familiar and customizable like Rails scaffolds (less DSL)
  • Supports Rails features out of the box (Action Text, Active Storage, Action Mailbox, has_secure_password, etc)
  • Built with Hotwire: Turbo, Stimulus and import maps, with no Node or build step
  • Search, filters and scopes on index pages, and dark mode
  • Works with Propshaft and Sprockets

Madmin index page with filters

Madmin edit form in dark mode, searching for a user

Installation

Add madmin to your application's Gemfile:

bundle add madmin

Then run the madmin generator:

rails g madmin:install

This will install Madmin and generate resources for each of the models it finds.

Resources

Madmin uses Resource classes to add models to the admin area.

Generate a Resource

To generate a resource for a model, you can run:

rails g madmin:resource ActionText::RichText

Avoid N+1 queries

In case of N+1 queries, you can preload the association by overriding the scoped_resource method in the controller:

module Madmin
  class PostsController < Madmin::ResourceController
    private

    def scoped_resources
      super.includes(:user)
    end
  end
end

Read-only Resources

To expose a resource in the admin without allowing writes, override readonly? in the resource:

class AuditLogResource < Madmin::Resource
  def self.readonly?
    true
  end
end

The new, edit, and delete links are hidden for read-only resources, and any write actions redirect back to the index.

Scopes

Declare scopes on a resource to render filter buttons on the index page. Each scope must be a scope or class method on the model:

class PostResource < Madmin::Resource
  scope :published
  scope :draft
end

Button labels default to the humanized scope name. To customize or translate them, define an I18n key — madmin.scopes.<param_key>.<scope> for one resource, or madmin.scopes.<scope> shared across resources:

en:
  madmin:
    scopes:
      post:
        published: Live

Filters

Index pages have a Filters button for narrowing records by their columns, like "Admin is true" or "Created at after Aug 15". Filters combine with search, scopes and sorting, and they're kept in the URL so filtered lists can be bookmarked and shared.

String, text, number, date, datetime and boolean columns can be filtered. Encrypted columns and attributes that aren't database columns are left out. Turn filtering off for an attribute with filter: false:

class UserResource < Madmin::Resource
  attribute :api_token, filter: false
end

Custom fields can offer a filter by returning :string, :number, :date, :datetime or :boolean from self.filter_type.

Resource Names

Resource names in the menu, headings and buttons come from the model's translation when it has one, so they follow Rails' own activerecord.models keys:

en:
  activerecord:
    models:
      post:
        one: Article
        other: Articles

Models without a translation keep their default names, like Posts and Active Storage / Blobs.

For labels that need logic (e.g. looking up a record name), override scope_label:

class PostResource < Madmin::Resource
  def self.scope_label(name)
    name.to_s.titleize
  end
end

Configuring Views

The views packaged within the gem are a great starting point, but inevitably people will need to be able to customize those views.

You can use the included generator to create the appropriate view files, which can then be customized.

For example, running the following will copy over all of the views into your application that will be used for every resource:

rails generate madmin:views

The view files that are copied over in this case includes all of the standard Rails action views (index, new, edit, show, and _form), as well as:

  • application.html.erb (layout file)
  • _javascript.html.erb (default JavaScript setup)
  • _navigation.html.erb (renders the navigation/sidebar menu)

As with the other views, you can specifically run the views generator for only the navigation or application layout views:

rails g madmin:views:navigation
 # -> app/views/madmin/_navigation.html.erb

rails g madmin:views:layout  # Note the layout generator includes the layout, javascript, and navigation files.
 # -> app/views/madmin/application.html.erb
 # -> app/views/madmin/_javascript.html.erb
 # -> app/views/madmin/_navigation.html.erb

If you only need to customize specific views, you can restrict which views are copied by the generator:

rails g madmin:views:index
 # -> app/views/madmin/application/index.html.erb

You might want to make some of your model's attributes visible in some views but invisible in others. The attribute method in model_resource.rb gives you that flexibility.

 # -> app/madmin/resources/book_resource.rb
class BookResource < Madmin::Resource
  attribute :id, form: false
  attribute :title
  attribute :subtitle, index: false
  attribute :author
  attribute :genre
  attribute :pages, show: false
end

You can also scope the copied view(s) to a specific Resource/Model:

rails generate madmin:views:index Book
 # -> app/views/madmin/books/index.html.erb

Specifying Field Types

You can set a field type as the second argument. Field types may have additional options to render the field UI.

For example, we can use a select for the genre attribute and specify the collection of options to choose from.

class BookResource < Madmin::Resource
  attribute :genre, :select, collection: ["Fiction", "Mystery", "Thriller"]
end

Nested Forms

Use :nested_has_many or :nested_has_one to edit associated records inside the parent's form. The model needs accepts_nested_attributes_for, with allow_destroy: true for the Remove link to work, and the associated model needs a resource. Use skip to leave out attributes of the associated resource:

class User < ApplicationRecord
  has_many :posts
  has_one :profile
  accepts_nested_attributes_for :posts, :profile, allow_destroy: true
end

class UserResource < Madmin::Resource
  attribute :posts, :nested_has_many, skip: %I[user]
  attribute :profile, :nested_has_one, skip: %I[user]
end

A nested has one shows its fields when the record exists, or an "Add" link when it doesn't, so saving the parent doesn't create an empty record.

Direct Uploads

File fields upload through the form by default. Add direct_upload: true to upload straight from the browser to your Active Storage service instead, which keeps large files off your app servers and avoids request timeouts:

class PostResource < Madmin::Resource
  attribute :image, direct_upload: true
  attribute :attachments, direct_upload: true
end

Cloud services like S3 need CORS configured to accept uploads from the browser. See the Active Storage guide.

Custom Fields

You can generate a custom field with:

rails g madmin:field Custom

This will create a CustomField class in app/madmin/fields/custom_field.rb And the related views:

# -> app/views/madmin/fields/custom_field/_form.html.erb
# -> app/views/madmin/fields/custom_field/_index.html.erb
# -> app/views/madmin/fields/custom_field/_show.html.erb

You can then use this field on our resource:

class PostResource < Madmin::Resource
  attribute :title, field: CustomField
end

Actions

Member Actions

member_action lets you add custom buttons to a resource's show page. Each block receives the current record and is rendered in the header's .actions div:

class PostResource < Madmin::Resource
  member_action do |record|
    link_to "Publish", publish_admin_post_path(record), class: "btn btn-secondary"
  end
end

Pass collection: true to also render the action on the index page, in each row next to the built-in "View" and "Edit" links:

class PostResource < Madmin::Resource
  member_action collection: true do |record|
    link_to "Publish", publish_admin_post_path(record), class: "btn btn-secondary"
  end
end

Collection Actions

collection_action is the index-page counterpart to member_action. Blocks are rendered in the index header's .actions div, immediately before the built-in "New <Resource>" link.

Unlike member_action, blocks receive no arguments. Each block is instance_exec-ed on the index view context, so link_to, route helpers, policy, current_user, params, resource, and @records are all available. Multiple blocks render in registration order.

Each block is responsible for its own authorization gating — if a button should be hidden for some users, guard it inside the block.

class PostResource < Madmin::Resource
  collection_action do
    link_to "Bulk Import", bulk_import_admin_posts_path, class: "btn btn-secondary"
  end

  collection_action do
    link_to "Export CSV", export_admin_posts_path(format: :csv), class: "btn btn-secondary"
  end
end

Extending Madmin

Madmin runs ActiveSupport load hooks on its core classes so you can extend them from an initializer without reopening or monkey patching them. This is the recommended way for engines and gems to add behavior to Madmin.

Hook Base Use it for
:madmin_resource Madmin::Resource Adding to the resource DSL
:madmin_field Madmin::Field Shared behavior across every field type
:madmin_search Madmin::Search Customizing how the search query is built
:madmin_base_controller Madmin::BaseController Layouts, helpers, rescue_from, authentication
:madmin_resource_controller Madmin::ResourceController CRUD callbacks, scoping records, audit logging

Blocks are class_eval-ed on the base class, so self is the class itself:

# config/initializers/madmin.rb
ActiveSupport.on_load(:madmin_resource) do
  # self == Madmin::Resource, so `extend` adds class-level DSL to every resource
  extend MyAdmin::ResourceExtensions
end

ActiveSupport.on_load(:madmin_base_controller) do
  # self == Madmin::BaseController
  rescue_from Pundit::NotAuthorizedError, with: :admin_not_authorized
end

Hooks registered after a class has already loaded run immediately, so ordering in your initializers doesn't matter. The controllers live in the engine's app/ directory and are reloadable, which means their hooks re-run on every reload in development — keep the blocks idempotent.

Authentication

You can use a couple of strategies to authenticate users who are trying to access your madmin panel: Authentication Docs

Dark Mode

Madmin follows the system's light or dark setting. To always use one, add a stylesheet with Madmin.stylesheets << "madmin_overrides" (see Assets) and set color-scheme:

:root {
  color-scheme: light;
}

Madmin's colors are CSS variables on :root, defined with light-dark() so each has a light and a dark value. Custom fields and views can use them to match both modes, and you can override them to change the theme:

:root {
  --primary-color: rgb(79 70 229); /* Button backgrounds */
  --link-color: light-dark(rgb(79 70 229), rgb(129 140 248));
}

The variables are --primary-color, --link-color, --text-color, --light-text-color, --surface-color (page and input backgrounds), --background-color (table headers), --hover-color, --border-color, --input-border-color and --danger-color.

Assets

You can customize the JavaScript and CSS assets used by Madmin for your application. To learn how see the Assets Doc

🙏 Contributing

This project uses Standard for formatting Ruby code. Please make sure to run standardrb before submitting pull requests.

📝 License

The gem is available as open source under the terms of the MIT License.

Projets similaires

The administration framework for Ruby on Rails applications.

Rubyactiveadminadmin-uiarbre
Aactiveadmin
9,7 k étoiles3,3 k

A Rails engine that helps you put together a super-flexible admin dashboard.

JavaScriptadmin-dashboardrailsruby
Tthoughtbot
6 k étoiles1,1 k

RailsAdmin is a Rails engine that provides an easy-to-use interface for managing your data

Ruby
Rrailsadminteam
8 k étoiles2,2 k