reinhardt-admin
Django-style admin panel functionality for Reinhardt framework.
Overview
This crate provides a web-based admin interface for managing database models, built as a WASM single-page application served by a Reinhardt server.
Features
- ✅ Model Management Interface: Web-based CRUD operations for database models
- ✅ Automatic Admin Discovery: Auto-generate admin interfaces from model definitions
- ✅ Bulk Operations: Delete multiple records in a single operation
- ⏳ Customizable Admin Actions (planned; tracked in
#5808): Define custom
ModelAdminactions - ✅ Search and Filtering: Advanced search capabilities with multiple filter types
- ✅ Permissions Integration: Role-based access control for admin operations
- ✅ Change Logging: Per-object audit history without storing submitted values
- ✅ Inline Editing: Edit related models inline
- ✅ Changelist Inline Editing: Edit selected list columns in one atomic batch
- ✅ Responsive Design: Mobile-friendly admin interface with customizable templates
Command-Line Interface (reinhardt-admin-cli)
For project management commands (startproject, startapp), please use
reinhardt-admin-cli.
Installation
Add reinhardt to your Cargo.toml:
[]
= { = "0.4.0-alpha.11", = ["admin"] }
# Or use a preset:
# reinhardt = { version = "0.4.0-alpha.11", features = ["full"] } # All features
Then import admin features:
use ;
use ;
Quick Start
Configuring Admin Models
Register models with AdminSite in a dedicated configuration function:
use ;
Mounting Admin Routes
Admin routes are registered inside the routes() function decorated with
#[routes]. Use admin_routes_with_di() to mount the admin
panel with deferred DI registration:
use UnifiedRouter;
use ;
use routes;
use Arc;
The AdminDatabase is lazily constructed from DatabaseConnection at the
first request, so no database connection is needed during route setup.
Provision the admin history table before serving requests by calling
initialize_admin_history_schema() during application setup, or by applying an
equivalent application migration. Admin request handlers only read and insert
history rows.
Customizing the Admin
Use the #[admin] proc macro to register a model with the admin panel. The macro
automatically implements ModelAdmin — no manual impl block is needed:
use admin;
use crateUser;
, collapsed = true)
],
ordering = ,
date_hierarchy = date_joined,
list_per_page = 25,
)]
;
The #[admin(model, ...)] attribute expands to a full ModelAdmin implementation
at compile time, so you never need to write boilerplate field structs or
impl Default blocks.
list_select_related accepts one-level forward foreign keys. The list query
loads each relation with a LEFT JOIN and returns it as a nested object under
the relation name. Foreign keys that use to_field join against that field's
physical database column.
date_hierarchy accepts a declared Date, DateTime, or TimestampTz field.
The changelist offers year, month, and day choices in sequence, applies each
choice to the current scoped query, and returns to page 1.
The legacy get_list request/response types remain unchanged; the client uses
the versioned get_list_with_date_hierarchy endpoint with
DateHierarchyListQueryParams and DateHierarchyListResponse for this metadata.
Programmatic admins without registry metadata use the configured hierarchy name
as the physical column; registered models retain field-type and column validation.
For computed columns, override list_columns() with a stable key and implement
computed_list_value() for that key:
use AdminResult;
use ;
use async_trait;
use ;
use HashMap;
;
A computed column is sortable only when sort_field names a real database
field. Requests sort by the computed key (for example, -summary), while the
server maps that key and direction to the declared database field before query
execution. Use None for non-sortable values; SQL expressions and computed
aliases are not valid sort mappings. Computed values are rendered as escaped
text in the changelist, and their keys cannot replace the configured primary key.
Existing list_display() implementations remain valid. The default
list_columns() converts every legacy entry to a database-backed field column,
so applications only need the descriptor API when they add computed columns or
custom labels.
For request-specific visibility rules, implement get_queryset and append
filters to the supplied query. These conditions are always combined with
search and client filters using AND, and are reused for both rows and count:
async
Many-to-Many Selectors
Use filter_horizontal for side-by-side lists or filter_vertical for stacked
lists. The same options are available through the trait, builder, and macro:
// Trait
// Builder
let article_admin = builder
.model_name
.table_name
.filter_horizontal
.filter_vertical
.build?;
// Macro
;
Selector names are matched exactly. A field cannot appear in both layouts, and only registered many-to-many fields are accepted. Loading or searching options requires View permission on the related model; that permission is checked again before saving. Each search page returns at most 50 options; use Load more to append later pages, while already chosen values remain available for submission. Parent-row changes and join-table additions or removals are committed in one atomic transaction, so a join failure rolls back the parent mutation.
Foreign-key relation fields
Foreign-key form controls are opt-in. Add a relation to
autocomplete_fields for a searchable control, or to raw_id_fields for a
direct relation-ID input. The two lists are mutually exclusive after field
name normalization:
use ;
let post_admin = builder
.model_name
.autocomplete_fields
.raw_id_fields
.allow_all
.build
.expect;
assert_eq!;
assert_eq!;
Each configured name may be either the model's logical relation name (for
example, author) or its persisted ID column (author_id). Reinhardt uses the
application relationship registry and migration metadata to normalize both
forms to the persisted column used in submissions and to resolve the qualified
target model. An explicit foreign-key to_field is honored for lookup,
validation, and saving. Only foreign keys are accepted; a missing target admin, a table
mismatch, an unknown/non-foreign-key field, or a field configured in both lists
is rejected before form metadata or lookup results are returned.
Autocomplete searches use the related ModelAdmin::search_fields() values as
OR-combined Contains filters. The related admin must configure at least one
search field. A related admin can customize option labels by overriding
ModelAdmin::object_label(); returning None falls back to the related
object's relation target-field value. Raw-ID controls resolve the exact ID so edit forms
also display a permission-checked label.
Both the source admin and the related admin must grant view permission before a
lookup can return any row or label. Create and update operations perform the
same related view, scalar-ID, target-existence, and nullability checks again at
save time, after the normal field allowlist/readonly validation and before
sanitization or the database write. A relation marked in readonly_fields
cannot be changed. Null is accepted only when the foreign-key metadata marks
the relation nullable.
Relation lookups are bounded: the query is at most 200 bytes, the default page
size is 20 and the maximum is 100, page numbers are constrained to 1 through
10,000, and the server fetches at most one extra row to compute has_next.
Responses never contain more than the requested page size. Submitted IDs and
labels are always resolved by the server; client-provided labels are not
trusted.
Registered Model Actions
Manual ModelAdmin implementations can expose actions with stable names,
labels, permissions, and an optional confirmation prompt through actions().
The list page applies an action only to the records selected on the current
page.
Override execute_action() to perform the mutation with the supplied
AdminActionTransaction. The server commits the action only when the hook
returns AdminActionOutcome; an error rolls back the transaction. Return the
canonical, duplicate-free IDs that actually succeeded separately from the
total affected row count so audit and history consumers can record the exact
objects. The hook receives no pooled database handle, so every action write
uses the server-owned transaction.
The endpoint validates CSRF, the registered action name, selection size,
primary-key values, and the declared ModelPermission before calling the
hook. Confirmation metadata is enforced by the browser UI; server-side callers
must still make an explicit action request.
Editing Related Models Inline
Manual admin configuration can place foreign-key children on the parent create
and change forms. The child admin must also be registered with AdminSite
with the typed child's table name because its view, add, change, and delete
permissions are checked independently. Inline children must use a single-field
integer, text-like, or UUID primary key; the parent key follows the same type
restriction.
use ;
let line_items = ?
.style
.extra
.can_delete;
let order_admin = builder
.model_name
.table_name
.fields
.inlines
.build?;
InlineStyle::Stacked renders the same rows as labelled field groups. Blank
configured extra rows create new children; no client-side row factory is
needed. The server rejects submitted foreign keys and assigns the trusted
parent key itself. Parent and child creates, updates, and explicit deletes run
in one transaction, so any child failure rolls back the complete edit.
Inline declarations in #[admin], nested inlines, and dynamically adding more
rows in the browser are not supported. Configure the required number of blank
rows with extra.
list_editable is opt-in; without it, changelists remain read-only. Each entry
must be a real database field in list_display, and cannot be the primary key,
the first displayed row-link field, generated, computed, or read-only. The
admin submits only dirty rows when Save is selected and commits the current
page as one transaction, so any row failure rolls back the complete batch.
Timezone-aware values are displayed in datetime-local controls as UTC;
submitted wall times are also interpreted as UTC. JSON controls validate input
before submission and preserve JSON null separately from SQL NULL. Nullable
text and set controls preserve explicit empty values rather than coercing them
to SQL NULL.
Migration notes
List-view struct literals now carry inline-edit metadata. Add editable,
linked, required, nullable, step, and form_spec to Column and
ColumnInfo, and add pk_field to ListViewData and ListResponse.
ListViewData::records now uses HashMap<String, serde_json::Value> so primary
keys and editable values retain their wire types. Inline mutation struct literals
also include json_fields, which is empty unless a value came from a parsed JSON
control. Use false, false, false, false, None, None, "id", and an
empty vector respectively to preserve the previous read-only behavior.
Grouping Form Fields
Without fieldsets, the existing fields configuration keeps forms flat. Use
one or the other; configuring both is rejected. Programmatic configurations use
the same ordered Fieldset descriptors as the macro:
use ;
let grouped = builder
.model_name
.fieldsets
.build
.unwrap;
assert!;
collapsed sets only the initial state of the native <details> element; the
open state is not persisted. Fieldsets do not support nesting, custom layout
classes, layout grids, or inline form configuration.
Customizing Form Fields
ModelAdmin supports three equivalent configuration paths: an AdminForm
adapter, builder overlays, and the #[admin] attribute. Form inclusion and
order still come only from fields, fieldsets, or the existing fallback;
customization cannot add virtual fields.
AdminForm::normalize receives owned JSON values and validate borrows the
normalized data. Both hooks must be synchronous and pure: they have no request,
user, database, or object instance. Return AdminFormErrors::field for a field-local error or
AdminFormErrors::global for a form-wide error. The server returns these as
HTTP 422 errors, using _all for global messages.
use ;
use Value;
;
formfield_overrides overlay only the properties they set. Resolution is:
inferred model default, configured relation widget, formfield_overrides, then
AdminForm::schema(). Readonly state, nullability, relation authorization, and
save-time relation validation are applied afterward and cannot be disabled.
An override can make a nullable field required, but cannot weaken a
model-required field.
use ;
let article_admin = builder
.model_name
.fields
.formfield_overrides
.prepopulated_fields
.build
.unwrap;
assert_eq!;
Prepopulation uses the framework slugifier on the client for each page mount. An existing non-empty edit value is locked. Once an operator edits or clears a target, later source changes do not overwrite it during that mount. The server never recomputes a submitted target. Targets must be editable registered text fields; sources cannot be file, foreign-key, or many-to-many fields.
Foreign-key and many-to-many overrides remain limited to their compatible widgets and preserve existing lookup permissions and save-time revalidation. Arbitrary components, HTML attributes, asynchronous validation, and virtual fields are not supported.
Configured textarea rows use the additive TextAreaWithRows variants of
FieldType and FormFieldSpec; downstream exhaustive matches must handle
those variants. The legacy unit TextArea variants and their JSON wire shapes
remain available.
The equivalent macro declaration is:
use AdminForm;
use ;
use ;
;
,
prepopulated_fields = ,
)]
;
Architecture
The admin panel is built on several key components:
Database Layer
Advanced filtering and query building with reinhardt-query integration:
- FilterOperator: Eq, Ne, Gt, Gte, Lt, Lte, Contains, StartsWith, EndsWith, In, NotIn, Between, Regex
- FilterCondition: AND/OR conditions for complex queries
- FilterValue: Type-safe value representation (String, Int, Float, Bool, Array)
For detailed database layer documentation, see the core::database module.
Server Functions
All CRUD operations are implemented as reinhardt-pages server functions in
individual modules under src/server/:
get_dashboard— admin dashboard dataget_list— model list view with paginationget_list_action_metadata— primary-key and registered action metadataget_detail— detail view for a single recordget_history— newest-first per-object change history, including deleted recordsget_fields— field metadata for a modelget_relation_options— search and resolve configured relation field optionscreate_record— create a new recordupdate_record— update an existing recordupdate_inline_edits— atomically update dirty changelist rowsdelete_record— delete a single recordbulk_delete_records— bulk delete operationsexecute_admin_action— registered model actionsupdate_inline_edits— atomic changelist inline editsexport_data— export data (CSV, JSON, XML)import_data— import dataadmin_login/admin_login_with_header— admin authenticationadmin_logout— admin session termination
Successful mutations persist their per-object history metadata in the same transaction. History records contain changed field names, but not submitted field values.
Routing
Route registration uses two free functions from core::router:
use ;
use UnifiedRouter;
use Arc;
// Default: uses AdminDefaultUser (table "auth_user")
let site = new;
let = admin_routes_with_di;
let assets = admin_static_routes;
let router = new
.mount
.mount
.with_di_registrations;
// Routes registered under /admin/:
// POST /admin/api/server_fn/get_dashboard
// POST /admin/api/server_fn/get_list
// POST /admin/api/server_fn/get_list_action_metadata
// POST /admin/api/server_fn/get_detail
// POST /admin/api/server_fn/get_history
// POST /admin/api/server_fn/get_fields
// POST /admin/api/server_fn/get_relation_options
// POST /admin/api/server_fn/create_record
// POST /admin/api/server_fn/update_record
// POST /admin/api/server_fn/create_record_multipart
// POST /admin/api/server_fn/update_record_multipart
// POST /admin/api/server_fn/update_inline_edits
// POST /admin/api/server_fn/delete_record
// POST /admin/api/server_fn/bulk_delete_records
// POST /admin/api/server_fn/execute_admin_action
// POST /admin/api/server_fn/update_inline_edits
// POST /admin/api/server_fn/export_data
// POST /admin/api/server_fn/import_data
// POST /admin/api/server_fn/admin_login
// POST /admin/api/server_fn/admin_login_with_header
// POST /admin/api/server_fn/admin_logout
// GET /admin/ (SPA shell)
// GET /admin/{model}/{id}/history/ (per-object history)
// GET /admin/{*tail} (SPA client-side routing)
// Static assets registered under /static/admin/:
// GET /static/admin/{*path}
// HEAD /static/admin/{*path}
For comprehensive routing documentation, see the core::router module.
Feature Flags
| Feature | Description |
|---|---|
adapters |
Adapter layer utilities |
core |
Core admin functionality |
pages |
Page rendering support |
server |
Server-side request handling |
types |
Shared type definitions |
all |
All of the above (adapters, core, pages, server, types) |
file-uploads |
Storage-backed FileField/ImageField admin uploads, validation, replacement, clear, and delete cleanup |
admin |
Admin feature marker |
full |
All features including file-uploads |
By default, no features are enabled (default = []).
Documentation
- API Documentation (coming soon)
- Core Module Documentation
License
Licensed under the BSD 3-Clause License.