Controllers
Trek::BaseController— the base for all admin controllers: authentication, authorization, localization, theadminlayout and Trek's form builder.Trek::ResourceController— extendsBaseControllerwith the full CRUD (index,show,new,create,edit,update,destroy), wired to ActionPolicy (authorized_scope,authorize!) and ordering. Scaffolded controllers inherit from it and only need to declare theirmodel:
module Admin
class BooksController < Trek::ResourceController
private
def model
Book
end
end
end- Panel controllers (
Trek::Panels::*) — support endpoints used by the admin UI: image uploads (Uppy), link insertion and content editor prompts.
Mixins
include Trek::Pagination— paginates the index with Kaminariinclude Trek::Search— filters the index byparams[:search]when the model is Searchableinclude Trek::Filters— filters the index by attribute equality (where(column => value)) fromparams[:filters]include Trek::Scopes— filters the index by named scopes fromparams[:scopes]
Filters
Trek::Filters lets users narrow an index by attribute values. Call apply_filters! in your index action — it rewrites @objects from params[:filters], applying a where(column => value) per non-blank filter (a value of "nil" matches NULL):
module Admin
class CitiesController < Trek::ResourceController
include Trek::Filters
helper_method :countries
def index
super
apply_filters!
end
private
def countries
City.distinct.pluck(:country_code).index_with { |code| I18n.t("countries.#{code}") }
end
end
endRender the choices with a ButtonGroupComponent, each button linking back to the index with the chosen filter:
= render Trek::ButtonGroupComponent.new do |g|
- g.with_button text: t("admin.actions.all"), \
href: [:admin, model], active: params.dig(:filters, :country_code).nil?
- countries.each do |code, name|
- g.with_button text: name, \
href: polymorphic_path([:admin, model], filters: { country_code: code }), \
active: code == params.dig(:filters, :country_code)If a controller includes
Trek::Filtersbut itsindexnever callsapply_filters!, anafter_actionraisesTrek::Filters::NotAppliedError(outside production) to flag the missing wiring.
Scopes
Trek::Scopes filters by named model scopes rather than raw attributes. Declare the allowed scopes via available_scopes, then call apply_scopes! in your index. It also exposes scope_counts and unscoped_objects helpers (the per-scope match counts and the pre-scope collection) for building scope tabs:
module Admin
class ArticlesController < Trek::ResourceController
include Trek::Scopes
def index
super
apply_scopes!
end
private
def available_scopes
%i[published draft]
end
end
endLike Trek::Filters, forgetting to call apply_scopes! raises Trek::Scopes::NotAppliedError outside production.
N+1 detection
trek:install:prosopite (part of trek:install) sets up Prosopite in development and test: config/initializers/prosopite.rb mounts its Rack middleware, so every request — including Trek's own engine controllers — is scanned. A detected N+1 raises Prosopite::NPlusOneQueriesError in test (so request specs fail) and is logged in development.
The generator also adds spec/requests/admin/n_plus_one_spec.rb, a smoke request spec that renders the dashboard, the pages index and the fragments index as an admin — extend its route table with the app's other admin pages (scaffolded indexes and edit pages in particular), with a few records created beforehand, since an empty list can't reveal an N+1.
Legitimate repeated queries (e.g. one COUNT per row) can be exempted with the escape hatch:
Prosopite.pause do
scopes.index_with { |scope| relation.public_send(scope).count }
endTrek's own concerns already do this where needed (Trek::Scopes#scope_counts). Guard with if defined?(Prosopite) in code that must also run in production, where the gem isn't loaded.