Skip to content

Custom tools

You may use custom tools to create custom sections or pages to add to your app.

Custom tools vs. resource tools

A custom tool is a standalone page — its own route, controller action, and sidebar item. If instead you want to embed a custom partial inside a resource's Show or Edit view, reach for a resource tool.

Generate tools

bin/rails generate avo:tool dashboard will generate the necessary files to show the new custom tool.

bash
 bin/rails generate avo:tool dashboard
      create  app/views/avo/sidebar/items/_dashboard.html.erb
      create  app/controllers/avo/tools_controller.rb
      insert  app/controllers/avo/tools_controller.rb
      create  app/views/avo/tools/dashboard.html.erb
      insert  config/routes.rb

The generator restarts the server at the end so the new route takes effect.

Controller

If this is your first custom tool, a new ToolsController is generated for you. The generator inserts an action into it, pre-filled with a page title and a breadcrumb.

ruby
# app/controllers/avo/tools_controller.rb
class Avo::ToolsController < Avo::ApplicationController
  def dashboard
    @page_title = "Dashboard"
    add_breadcrumb title: "Dashboard"
  end
end

You can keep this action here or move it to another controller and organize it differently.

Route

The route is injected inside your mount_avo block in config/routes.rb, so it inherits whatever wraps that call:

ruby
# config/routes.rb
authenticate :user, ->(user) { user.is_admin? } do
  mount_avo do
    get "dashboard", to: "tools#dashboard", as: :dashboard
  end
end

If mount_avo is a one-liner (mount_avo or mount_avo at: "avo"), the generator converts it to block form and drops the route inside. If it's already a block, the route is injected into it. Existing indentation is preserved.

INFO

Because the route lives inside mount_avo, it's protected by the same authentication as the rest of Avo — the authenticate block above covers it too.

The one exception is the fallback: if your config/routes.rb has no mount_avo call at all, the generator appends a standalone Avo::Engine.routes.draw block instead, and that route is not behind any auth. Secure it yourself if needed. See Routing and Rails engines & paths for the details of routing inside the engine.

The _dashboard.html.erb partial will be added to the app/views/avo/sidebar/items directory. All the files in this directory will be loaded by Avo and displayed in the sidebar. They are displayed alphabetically, so you may change their names to reorder the items.

Customize the sidebar

If you want to customize the sidebar partial further, you can eject and update it to your liking. We're planning on creating a better sidebar customization experience later this year.

What's available in your tool

A custom tool isn't an isolated component — it's a plain Rails view (app/views/avo/tools/dashboard.html.erb) rendered inside Avo's layout by an action on a controller that inherits from Avo::ApplicationController. That means your view runs with the full Avo view context: anything Avo exposes to its own screens is available to yours.

The pieces you'll reach for most:

Variable / helperWhat it is
uiAvo's UI component builder — ui.panel, panel.with_card, ui.description_list, and the rest of the components Avo renders everywhere. Using these keeps your tool visually consistent and dark-mode-ready.
@page_titleThe page title, pre-set by the generator. Assign any instance variable in the action and it's available in the view, just like a normal Rails controller.
_current_userThe currently signed-in user, as resolved by Avo's authentication.
Avo::Current.contextYour app-wide context object.
paramsThe request params, as in any Rails view.
avo. / main_app. path helpersRoute helpers for the engine and your main app — see Using path helpers below.

Everything is wired through the controller action, so the usual Rails rules apply: set instance variables in the action, read them in the view, and pull in your own helpers with the helper method.

Set the page title

The generator already assigns a @page_title for you. To change it, edit that instance variable in the controller action.

ruby
# app/controllers/avo/tools_controller.rb
class Avo::ToolsController < Avo::ApplicationController
  def dashboard
    @page_title = "Custom tool page title"
  end
end

Avo uses the meta-tags gem to compile and render the page title.

Add assets

You might want to import assets (javascript and stylesheets files) when creating custom tools or fields. Please follow the Asset handling guide to bring your assets with your asset pipeline.

Using helpers from your app

You'll probably want to use some of your helpers in your custom tools. To have them available inside your custom controllers inherited from Avo's ApplicationController, you need to include them using the helper method.

ruby
# app/helpers/home_helper.rb
module HomeHelper
  def custom_helper
    'hey from custom helper'
  end
end

# app/controllers/avo/tools_controller.rb
class Avo::ToolsController < Avo::ApplicationController
  helper HomeHelper

  def dashboard
    @page_title = "Dashboard"
  end
end
erb
<%# app/views/avo/tools/dashboard.html.erb %>
<div class="flex flex-col">
  <%= render ui.panel(title: 'Dashboard') do |panel| %>
    <% panel.with_controls do %>
      <div class="text-sm italic">This is the panels tools section.</div>
    <% end %>

    <% panel.with_card(title: "New tool", padded: true) do %>
      <div class="flex flex-col justify-between min-h-24 space-y-4">
        <h3>What a nice new tool 👋</h3>

        <%= custom_helper %>
      </div>
    <% end %>
  <% end %>
</div>

Reuse Avo's UI components

ui.panel and panel.with_card are the same building blocks Avo renders everywhere else — the Avo::UI::PanelComponent and the Avo::UI::CardComponent it wraps content in. Reaching for them (instead of hand-rolling <div>s) keeps your custom tool visually consistent with the rest of the app and gives you dark mode for free. Prefer their built-in options over patching the markup with utility classes — padded: true gives the card body Avo's standard padding instead of adding your own px-*/py-*, and title:/description: build the header for you. When your content is a list of fields, wrap it in ui.description_list so they get full width and dividers. For colors, spacing, and dark-mode tokens, follow the theming guide.

Using path helpers

Because you're in a Rails engine, you will have to prepend the engine object to the path.

For Avo paths

Instead of writing resources_posts_path(1) you have to write avo.resources_posts_path(1).

For the main app paths

When you want to reference paths from your main app, instead of writing posts_path(1), you have to write main_app.posts_path.