admin
Admin UI and CRUD management tooling for Wagoe entities. Generates admin interfaces automatically from entity configuration — no manual forms.
Technology stack
| Technology | Purpose |
|---|---|
Hiccup |
HTML generation (Clojure data structures → HTML) |
HTMX |
Dynamic interactions without a frontend build step |
daisyUI / Tailwind |
Component classes on the Wagoe design tokens |
Lucide icons |
Icon set via |
Key namespaces
| Namespace | Purpose |
|---|---|
|
Main admin UI rendering (tables, forms, layout composition) |
|
Shared Lucide icon rendering |
|
Admin HTTP handlers and HTMX fragment endpoints |
Entity configuration
Admin entity configs are modular — each module owns its config file:
resources/conf/dev/
├── config.edn ← Main config (uses Aero #include)
└── admin/
└── users.edn ← User module's entity config
└── products.edn ← Product module's entity config
;; admin/products.edn
{:products
{:label "Products"
:label-singular "Product" ; one record, in toasts; default :label made singular
:list-fields [:name :sku :price :created-at]
:search-fields [:name :sku]
:hide-fields #{:deleted-at}
:readonly-fields #{:id :created-at :updated-at}}}
;; config.edn — include all entity configs
:wagoe/admin
{:enabled? true
:entities #merge [#include "admin/users.edn"
#include "admin/products.edn"]
:pagination {:default-page-size 25}}
Access
Under :wagoe/authorization {:mode :off}, the 1.x default, the admin admits
users with role :admin. Under :enforce it reads permissions instead: every
entity has admin.<entity>/read and admin.<entity>/write, and the code roles
admin.<entity>/viewer and admin.<entity>/editor holding them. Every admin
page needs read on one entity in a scope that entity maps, the sidebar lists what the user may read, and actions
they may not take are not shown: a viewer opens a record read-only. A platform
admin holds everything; a tenant :admin holds what their groups give them.
:report keeps the role rule and logs what :enforce would refuse, once a
minute per user and permission.
A right held in a scope admits only records inside it. The entity maps each scope key to a field, or to a function from the allowed values to a HoneySQL condition when the scope lies on a related record:
{:orders {:scope {:domain-id :domain-id}}
:lines {:scope {:domain-id myapp.admin/line-domain}}} ; (fn [ids] [:in :order_id {...}])
Lists, counts and search show only rows inside the scope, and a many-to-many shows only the linked records the user may read. A record outside it is a 404; a create or edit that would put one outside it is refused on the field. A relation condition cannot be checked on a record’s new values, so creating and editing there needs the right unscoped. A bare column in the condition is the entity’s own, whatever a query joins. An entity that maps no key admits only unscoped holders.
:require-role is deprecated: nothing reads it, and it goes in 2.0.
Adding a new module
-
Create
resources/conf/dev/admin/{module}.edn -
Add
#include "admin/{module}.edn"to the#mergevector inconfig.edn -
(reset)to reload configuration
UI conventions
-
Standalone data-entry forms (login, register, create/edit) get
form.form-cardclass -
Button-wrapper forms (logout, delete, revoke) get no class
-
Page header pattern:
h1+pin a plain[:div], with.page-actionsas sibling -
Tables:
[:div.table-wrapper > table.data-table]
Testing
clojure -M:test:test/pg :admin