Getting Started with AshBackpex
View SourceThis guide walks you through setting up AshBackpex to create an admin interface for your Ash resources.
Prerequisites
- An existing Phoenix application with Ash Framework configured
- Backpex installed and configured (see Backpex documentation)
- At least one Ash resource you want to administer
Installation
Add ash_backpex to your dependencies in mix.exs:
def deps do
[
{:ash_backpex, "~> 0.3.3"}
]
endRun mix deps.get to install the dependency.
AshBackpex targets Backpex ~> 0.21.0 and declares that dependency itself. If
your application pins Backpex directly, update its constraint to match.
Upgrading an existing admin?
AshBackpex 0.2 and 0.3 each follow a Backpex release with breaking changes. Read Upgrading to 0.3 before you bump the dependency.
Creating Your First Admin LiveResource
1. Ensure You Have an Admin Layout
If you followed the Backpex installation guide, you should already have an admin layout configured. AshBackpex uses the same layout system as Backpex.
If you need to create a new admin layout, Backpex provides the Backpex.HTML.Layout.app_shell/1 component as a foundation. Here's an example following Backpex conventions:
# lib/my_app_web/components/layouts.ex
defmodule MyAppWeb.Layouts do
use MyAppWeb, :html
import Backpex.HTML.Layout
attr :flash, :map, required: true
attr :fluid?, :boolean, default: false
attr :current_url, :string, required: true
attr :socket, :any, required: true
attr :current_theme, :string, required: true
attr :sidebar_open, :boolean, required: true
attr :sidebar_section_states, :map, required: true
attr :preferences_manifest, :map, required: true
slot :inner_block, required: true
def admin(assigns) do
~H"""
<.app_shell
socket={@socket}
fluid={@fluid?}
sidebar_open={@sidebar_open}
preferences_manifest={@preferences_manifest}
>
<:sidebar_branding>
<.sidebar_branding title="My App" />
</:sidebar_branding>
<:topbar>
<.theme_selector
current_theme={@current_theme}
themes={[{"Light", "light"}, {"Dark", "dark"}]}
/>
<.topbar_dropdown>
<:label>
<div class="btn btn-square btn-ghost">
<Backpex.HTML.CoreComponents.icon name="hero-user" class="size-6" />
</div>
</:label>
<li>
<.link href={~p"/"} class="flex justify-between hover:bg-base-200">
<p>Back to App</p>
</.link>
</li>
</.topbar_dropdown>
</:topbar>
<:sidebar>
<.sidebar_section id="blog" sidebar_section_states={@sidebar_section_states}>
<:label>Blog</:label>
<.sidebar_item current_url={@current_url} navigate={~p"/admin/posts"}>
<Backpex.HTML.CoreComponents.icon name="hero-document-text" class="size-5" /> Posts
</.sidebar_item>
<%!-- Add more sidebar items for your resources --%>
</.sidebar_section>
</:sidebar>
<.flash_messages flash={@flash} />
{render_slot(@inner_block)}
</.app_shell>
"""
end
endBackpex.InitAssigns assigns @current_theme, @sidebar_open,
@sidebar_section_states, and @preferences_manifest; the layout passes them
on. Two details of sidebar_section/1 are easy to miss:
idis required and must be unique among your sections. Backpex stores each section's open state under it. Use only letters, digits, underscores, and hyphens.sidebar_section_statesmust be passed explicitly. A function component does not inherit the surrounding assign, so a section without it always renders open and a collapsed section does not stay collapsed.
Use the server-assigned theme in your root layout:
<html data-theme={assigns[:current_theme] || "light"}>See the Backpex layout documentation for more details on available components and customization options.
2. Create a LiveResource
Create a LiveResource module that uses AshBackpex.LiveResource:
# lib/my_app_web/live/admin/post_live.ex
defmodule MyAppWeb.Admin.PostLive do
use AshBackpex.LiveResource
backpex do
resource MyApp.Blog.Post
layout {MyAppWeb.Layouts, :admin}
fields do
field :title
field :content do
module Backpex.Fields.Textarea
end
field :published
field :inserted_at
end
end
end3. Add Routes
Add routes for your admin LiveResource:
# lib/my_app_web/router.ex
import Backpex.Router
scope "/admin", MyAppWeb.Admin do
pipe_through [:browser]
backpex_routes()
live_session :backpex_admin,
on_mount: [{MyAppWeb.LiveUserAuth, :require_admin}, Backpex.InitAssigns] do
live_resources "/posts", PostLive
end
endPut your authentication hook first. It must assign current_user, and halt the
mount for anyone who may not use the admin:
# lib/my_app_web/live_user_auth.ex
defmodule MyAppWeb.LiveUserAuth do
import Phoenix.Component
import Phoenix.LiveView
def on_mount(:require_admin, _params, session, socket) do
socket = assign_new(socket, :current_user, fn -> load_user(session) end)
if admin?(socket.assigns.current_user) do
{:cont, socket}
else
{:halt, redirect(socket, to: "/sign-in")}
end
end
# load_user/1 and admin?/1 depend on how your application authenticates.
endThe order matters for two reasons. AshBackpex uses assigns.current_user as the
Ash actor, so it has to be there before the LiveResource mounts. And
Backpex.InitAssigns reads the user's stored preferences using whatever the
authentication hook assigned, so it has to run after it. If you use
AshAuthentication or mix phx.gen.auth, use the on_mount hook they provide in
place of MyAppWeb.LiveUserAuth.
4. Wire Up the Browser
Backpex sends the browser's stored preferences with the LiveView connection.
Wrap your connect params with backpexParams in assets/js/app.js:
import { Hooks as BackpexHooks, backpexParams } from "backpex";
const liveSocket = new LiveSocket("/live", Socket, {
params: backpexParams({ _csrf_token: csrfToken }),
hooks: { ...BackpexHooks },
});Without it, the theme, sidebar, and column choices revert when the user navigates to another resource, and Backpex logs a console warning.
5. Visit the Admin
Start your Phoenix server and visit http://localhost:4000/admin/posts to see your admin interface.
Admin Pages That Are Not Resources
A dashboard or any other LiveView can use the same admin layout. Route it inside
the same live_session, so both hooks run for it:
live_session :backpex_admin,
on_mount: [{MyAppWeb.LiveUserAuth, :require_admin}, Backpex.InitAssigns] do
live "/", DashboardLive
live_resources "/posts", PostLive
endA LiveResource hands every assign to the layout for you. Your own LiveView has to pass them itself:
# lib/my_app_web/live/admin/dashboard_live.ex
defmodule MyAppWeb.Admin.DashboardLive do
use MyAppWeb, :live_view
def render(assigns) do
~H"""
<MyAppWeb.Layouts.admin
socket={@socket}
flash={@flash}
current_url={@current_url}
current_theme={@current_theme}
sidebar_open={@sidebar_open}
sidebar_section_states={@sidebar_section_states}
preferences_manifest={@preferences_manifest}
>
<h1 class="text-2xl font-semibold">Dashboard</h1>
</MyAppWeb.Layouts.admin>
"""
end
endsocket, sidebar_open, sidebar_section_states, and preferences_manifest
are the ones Backpex 0.20 added, and the ones an older dashboard will be
missing. The layout above requires all of them, so the page raises a KeyError
when one is left out.
Link to the page from the layout's <:sidebar> slot with another
sidebar_item.
Adding More Features
Searchable Fields
Make fields searchable to enable the search box:
fields do
field :title do
searchable true
end
field :content do
searchable true
module Backpex.Fields.Textarea
end
endRelationships
Display relationships with navigation links:
backpex do
resource MyApp.Blog.Post
layout {MyAppWeb.Layouts, :admin}
load [:author] # Preload the relationship
fields do
field :title
field :author do
display_field :name
live_resource MyAppWeb.Admin.UserLive
end
end
endFilters
Add filters to the index view:
backpex do
resource MyApp.Blog.Post
layout {MyAppWeb.Layouts, :admin}
fields do
field :title
field :published
field :status
end
filters do
filter :published do
module Backpex.Filters.Boolean
end
filter :status do
module Backpex.Filters.Select
end
end
endCustom Sort Order
Set the default sort order:
backpex do
resource MyApp.Blog.Post
layout {MyAppWeb.Layouts, :admin}
init_order %{by: :inserted_at, direction: :desc}
fields do
# ...
end
endPersisted Index State
Persistence is opt-in, and the default is to persist nothing. Choose which index settings should survive navigation and reloads:
backpex do
resource MyApp.Blog.Post
layout {MyAppWeb.Layouts, :admin}
persist [:order, :filters, :columns, :metrics]
fields do
# ...
end
endPanels for Form Organization
Organize form fields into panels:
backpex do
resource MyApp.Blog.Post
layout {MyAppWeb.Layouts, :admin}
panels [
content: "Content",
settings: "Settings"
]
fields do
field :title do
panel :content
end
field :content do
panel :content
module Backpex.Fields.Textarea
end
field :published do
panel :settings
end
field :status do
panel :settings
end
end
endCustom Display Names
Customize how resources are labeled:
backpex do
resource MyApp.Blog.Post
layout {MyAppWeb.Layouts, :admin}
singular_name "Blog Post"
plural_name "Blog Posts"
fields do
# ...
end
endCustom Item Actions
Add or remove item actions:
backpex do
resource MyApp.Blog.Post
layout {MyAppWeb.Layouts, :admin}
item_actions do
strip_default [:delete] # Remove delete action
action :archive, MyApp.ItemActions.Archive
end
fields do
# ...
end
endAuthorization
AshBackpex automatically integrates with Ash authorization policies. The admin will:
- Check
Ash.can?/2before showing create/edit/delete buttons, and again before every create, update, delete, and item action - Use
assigns.current_useras the actor for reads and relationship options - Persist create and update changesets with their configured actor
Make sure your Ash resources have policies defined and that you're setting
current_user in your LiveView assigns. If you provide custom create or update
changeset functions, set that actor on the returned Ash changeset.
Policies are for rules that depend on who is asking. When the admin should not offer an operation to anyone, turn it off in the LiveResource instead:
backpex do
resource MyApp.Audit.Event
layout {MyAppWeb.Layouts, :admin}
create_action false # no "New" button, and :new is denied
update_action false # no editing
destroy_action false # no deleting
endDo this even if you also route the resource with except: [:new]. Backpex shows
the "New" button on an empty index page whenever :new is allowed, without
looking at the routes, and the page raises when the route is missing.
Do not define can?/3 in the LiveResource. AshBackpex generates it, and
defining it is a compile error.
Deleting is authorized per record: Backpex checks the destroy action's policies for every selected record, as the current user, before anything is deleted. Backpex does not hand the actor to the delete callback itself, so the destroy action then runs without one. A change or notifier on that action that reads the actor (an audit trail, for example) will not see it.
Next Steps
- See
AshBackpex.LiveResource.Dslfor all available DSL options - Check out the Backpex documentation for field types and customization
- Look at the demo application in the
demo/directory for more examples