Part 6: Admin Panel¶
In this part you'll add an admin panel with automatic CRUD for questions using ModelAdmin.
Source code: tutorial/step06/
Add the Admin App¶
The admin contrib app coordinates the admin panel. It provides a dashboard, layout, and route grouping. Apps contribute admin pages by implementing HasAdmin.
Update main.go:
import (
"github.com/oliverandrich/burrow/contrib/admin"
)
srv := burrow.NewServer(
// ... existing apps ...
polls.New(),
admin.New(), // new
)
The admin app:
- Provides routes under
/admin/protected byauth.RequireAdmin()middleware - Collects nav items and routes from all
HasAdminapps - Applies its own layout with a sidebar navigation
Set Up ModelAdmin¶
ModelAdmin provides generic CRUD views for any Bun model. Add it to the polls app:
import (
"github.com/oliverandrich/burrow/contrib/admin/modeladmin"
matpl "github.com/oliverandrich/burrow/contrib/admin/modeladmin/templates"
)
type App struct {
repo *Repository
handlers *Handlers
questionsAdmin *modeladmin.ModelAdmin[Question]
}
Add verbose struct tags to the model so ModelAdmin knows how to label columns:
type Question struct {
bun.BaseModel `bun:"table:questions,alias:q"`
ID int64 `bun:",pk,autoincrement" verbose:"ID"`
Text string `bun:",notnull" verbose:"Question"`
PublishedAt time.Time `bun:",notnull,default:current_timestamp" verbose:"Published"`
Choices []Choice `bun:"rel:has-many,join:id=question_id"`
}
Initialise the ModelAdmin in Register():
func (a *App) Register(cfg *burrow.AppConfig) error {
a.repo = NewRepository(cfg.DB)
a.handlers = &Handlers{repo: a.repo}
a.questionsAdmin = &modeladmin.ModelAdmin[Question]{
Slug: "questions",
DisplayName: "Question",
DisplayPluralName: "Questions",
DB: cfg.DB,
Renderer: matpl.DefaultRenderer[Question](),
CanCreate: true,
CanEdit: true,
CanDelete: true,
ListFields: []string{"ID", "Text", "PublishedAt"},
OrderBy: "published_at DESC, id DESC",
}
return nil
}
Implement HasAdmin¶
The HasAdmin interface has two methods: AdminRoutes() and AdminNavItems().
func (a *App) AdminNavItems() []burrow.NavItem {
return []burrow.NavItem{
{Label: "Questions", URL: "/admin/questions", Position: 30},
}
}
func (a *App) AdminRoutes(r chi.Router) {
r.Route("/questions", func(r chi.Router) {
r.Get("/", burrow.Handle(a.questionsAdmin.HandleList))
r.Get("/new", burrow.Handle(a.questionsAdmin.HandleNew))
r.Post("/new", burrow.Handle(a.questionsAdmin.HandleNew))
r.Get("/{id}", burrow.Handle(a.questionsAdmin.HandleDetail))
r.Post("/{id}", burrow.Handle(a.questionsAdmin.HandleDetail))
r.Get("/{id}/delete", burrow.Handle(a.questionsAdmin.HandleDelete))
r.Post("/{id}/delete", burrow.Handle(a.questionsAdmin.HandleDelete))
})
}
AdminRoutes receives a router scoped to /admin/, so the full path becomes /admin/questions.
Run It¶
Register a user, then promote them to admin via the database:
Visit /admin/ to see the dashboard. Click "Questions" in the sidebar to list, create, edit, and delete questions — all without writing any template code.
What You've Learnt¶
admin.New()— coordinates the admin panel with built-in default layout and dashboardModelAdmin— generic CRUD views for any Bun model, configured declarativelyHasAdmin— interface for apps to contribute admin routes and navigationverbosestruct tags — provide human-readable column labels for the admin UI
Next¶
In Part 7, you'll add HTMX for smooth navigation and infinite scroll pagination.