Resources
You want an admin page for a model. In Azu, that’s a resource: one file that points at a Lucid model and borrows its policy and validators.
Here’s the demo store’s product resource in full:
/** * Products in the demo store, loaded by convention from * app/admin/resources. The thumbnail uses Drive's default local disk, so * the demo runs offline. */import { defineResource, column, field, filter, action } from '@mezielabs/azu'import Product from '#models/product'import ProductPolicy from '#policies/product_policy'import feature from '#admin/actions/feature'import { createProductValidator, restockValidator, updateProductValidator,} from '#validators/product'
export default defineResource(Product, { label: 'Products', group: 'Catalog', icon: 'package', order: 1, title: 'name', search: ['name', 'sku'], policy: ProductPolicy, validators: { create: createProductValidator, update: updateProductValidator, // The update validator's unique rule reads productId meta: ({ record }) => ({ productId: record?.id }), }, query: (q) => q.preload('category'),
columns: [ column.image('thumbnail'), column.text('name').sortable(), column.decimal('price').format('currency', 'NGN').sortable(), column.belongsTo('category', 'name'), column.badge('status').colors({ draft: 'gray', published: 'green' }), // inertia/admin/components/stock_bar.tsx column.custom('stock', 'StockBar').label('In stock'), column.text('sku').hiddenByDefault(), ],
fields: [ field.text('name').section('Details'), // inertia/admin/components/sku_input.tsx field.text('sku').component('SkuInput').section('Details'), field.decimal('price').section('Pricing').min(0), field.belongsTo('category').section('Organisation').searchable('name'), field.manyToMany('tags').section('Organisation').searchable('name'), field .select('status') .section('Organisation') .options({ draft: 'Draft', published: 'Published' }), field.richText('description').section('Media'), field.image('thumbnail').section('Media').maxSize('2mb'), ],
filters: [ filter.select('status', { draft: 'Draft', published: 'Published' }), filter.belongsTo('category'), filter.dateRange('createdAt'), filter.custom('Low stock', (q) => q.where('stock', '<', 10)), filter.boolean('isFeatured').label('Featured'), ],
actions: [ action .bulk('publish') .confirm('Publish the selected products?') .ability('publish') .handle(async (products, { trx }) => { await Product.query({ client: trx }) .whereIn( 'id', products.map((p) => p.id) ) .update({ status: 'published' })
return { message: `${products.length} products published` } }), // A dialog field, validated by its own VineJS validator action .row('restock') .ability('restock') .fields( [field.number('quantity').section('Stock adjustment').help('Added to the current stock')], restockValidator ) .handle(async (product, { input }) => { product.stock += input.quantity await product.save() return { message: `${product.name} now has ${product.stock} in stock` } }), // A reusable action from azu:action feature, ],})Azu turns it into a paged list, a detail page, and create and edit forms. The rest of the guides take each part in turn.
Typed against the model
Section titled “Typed against the model”Resource definitions are typed against the model. A typo like column.text('nmae') fails tsc, not your client’s demo. You don’t pass any generics; Azu infers everything from the model.
The options
Section titled “The options”| Option | What it does |
|---|---|
label |
The name in the navigation and page titles. Defaults to the model’s name, pluralised. |
singularLabel |
One record’s name, as in “New product”. Defaults to the model’s name. |
group, icon, order |
Where it sits in the navigation. See Home screen and navigation. |
title |
The column that names a record: in breadcrumbs, links, and related pickers. |
search |
Columns the list’s search box matches. See Columns and lists. |
policy |
The Bouncer policy that decides who sees and does what. See Permissions. |
validators |
The VineJS validators the forms save through. See Validation. |
query |
A hook on every query the resource runs. Preload relations here, or scope records. |
columns |
The list’s columns. See Columns and lists. |
fields |
The form and detail fields. See Fields. |
filters |
The list’s filters. See Filters. |
actions |
Row and bulk actions. See Actions. |
beforeSave, afterSave |
Hooks around a form save. Each gets the record and the HTTP context, in the save’s transaction. |
beforeDelete |
A hook before each record is deleted. See Actions. |
slots |
Your components on the resource’s pages. See Customising. |
Loading resources
Section titled “Loading resources”Files in app/admin/resources load automatically. Each one appears in the navigation, grouped by group, for users whose policy allows viewList.
The demo sets the folder explicitly:
/** * Resources in this directory load automatically. Set autoload to * false to register them in start/azu.ts instead. */resources: { autoload: 'app/admin/resources' },Prefer to register resources yourself? Turn autoload off:
resources: { autoload: false },Then register each one in start/azu.ts. The demo’s file shows how, in its comment:
/*|--------------------------------------------------------------------------| Admin resources|--------------------------------------------------------------------------|| Azu loads every resource in app/admin/resources automatically. To| register resources here instead, set resources.autoload to false in| config/azu.ts, then register each one:|| import azu from '@mezielabs/azu/services/main'| import productResource from '#admin/resources/product_resource'|| azu.register(productResource)|*/
export {}Note: registering the same resource twice does nothing. Two resources with the same URL name stop the app from booting.
Generate one
Section titled “Generate one”node ace azu:resource Product writes a resource from a model, with its validators. Quick start walks through it, and Ace commands lists the flags.