Skip to main content

vizier 0.2.0 copy "vizier: ^0.2.0" to clipboard
vizier: ^0.2.0 copied to clipboard

Resource-driven server-rendered admin panels for Maat applications.

Vizier #

vizier — Resource-driven server-rendered admin panels for Maat applications.

Resource-driven, server-rendered admin panels for Maat applications. Describe a model once as an AdminResource and Vizier renders the list, create, edit and show pages, enforces your authorization hooks on every request, and serves the whole panel behind Anubis web sessions. No JavaScript build step: the assets ship inside the package and vizier:install publishes them into your application.

class SlotAdminResource extends AdminResource<Slot> {
  @override
  ModelDefinition<Slot> get model => Slot.def;

  @override
  String get slug => 'slots';

  @override
  String get singularLabel => 'Slot';

  @override
  String get pluralLabel => 'Slots';

  @override
  String get titleAttribute => 'starts_at';

  @override
  AdminForm form(AdminForm form) => form.fields([
    NumberField('resource_id', label: 'Resource id').integer().required(),
    DateTimeField('starts_at').required(),
    DateTimeField('ends_at').required(),
  ]);

  @override
  AdminTable<Slot> table(AdminTable<Slot> table) => table
      .columns([
        DateTimeColumn<Slot>('starts_at').sortable(),
        DateTimeColumn<Slot>('ends_at'),
      ])
      .defaultSort('starts_at');

  @override
  bool canCreate(Request request) => request.user<User>()?.isAdmin ?? false;
}

Installation #

dependencies:
  vizier: ^0.1.0

Vizier needs the packages it builds on: anubis for web sessions and CSRF, khnum_maat for views, and seshat_maat for models.

Register the service provider and the console commands, then publish the views and assets once:

providers: [VizierServiceProvider.new, AdminServiceProvider.new],
commands: [...vizierCommands()],
dart run bin/maat.dart vizier:install

A normal install never overwrites files your application already published; pass --force after reviewing the differences to take newer defaults.

Vizier stores nothing of its own, but it needs Anubis's session table, so run the migrations before the first request:

final migrations = <Migration>[...anubisMigrations, CreateApplicationTables()];
dart run bin/maat.dart migrate

Scaffold a resource and register it in one step:

dart run bin/maat.dart make:admin-resource User

The generator writes lib/app/vizier/resources/user_resource.dart and adds the import and list entry to the registry vizier:install published. Pass --model= for a differently named model and --force to overwrite.

Defining a panel #

A panel is registered from a service provider that boots after VizierServiceProvider, because the panel resolves the anubis.session middleware by alias at registration time.

class AdminServiceProvider extends ServiceProvider {
  AdminServiceProvider(super.app);

  @override
  void boot() {
    Anubis.provider('users', (id) => User.query().find(int.parse(id)));

    Vizier.panel(
      Panel(
        title: 'Booking Admin',
        userProvider: 'users',
        authenticate: (email, password) async {
          final user = await User.query().where('email', email).first();
          final valid = await Hash.checkAsync(password, user?.authPassword ?? '');
          return user != null && valid && user.isStaff ? user : null;
        },
        resources: [BookingAdminResource(), SlotAdminResource()],
      ),
    );
  }
}

authenticate returns the user to sign in, or null to reject. The panel mounts at /admin by default; pass path to move it.

Resources #

Every AdminResource<T> describes one model:

Member Purpose
model, slug, singularLabel, pluralLabel, titleAttribute Identity and how a record is named.
form(AdminForm) Fields for create and edit.
table(AdminTable<T>) Columns, sorting, search and filters for the index page.
navigationIcon, navigationOrder Placement in the sidebar.
query() The base query; override to scope what the panel can see.
beforeCreate, afterCreate, beforeUpdate, afterUpdate Hooks around writes.

Fields: TextField, EmailField, PasswordField, TextareaField, NumberField, BooleanField, SelectField, DateField, DateTimeField, HiddenField.

Columns: TextColumn, BooleanColumn, DateColumn, DateTimeColumn, and ComputedColumn for values derived from the record.

Authorization #

Nothing is writable by default. Each hook receives the current Request, so the same rule that guards your API guards the panel:

@override
bool canViewAny(Request request) => true;

@override
bool canCreate(Request request) => _isAdmin(request);

@override
bool canUpdate(Request request, Slot record) => _isAdmin(request);

@override
bool canDelete(Request request, Slot record) => _isAdmin(request);

Actions #

Row and bulk actions run your own code and report back to the page:

@override
List<AdminAction<Booking>> actions() => [
  AdminAction(
    name: 'mark-no-show',
    label: 'Mark no-show',
    bulk: true,
    icon: VizierIcon.check,
    requiresConfirmation: true,
    handler: (request, records, values) async {
      for (final record in records) {
        await bookings.markNoShow(record);
      }
      return ActionResult(message: '${records.length} bookings updated.');
    },
  ),
];

@override
bool canRunAction(Request request, AdminAction<Booking> action, Booking record) =>
    _isStaff(request);

An action may take a form to collect input before it runs; the values arrive in values.

Relations #

A resource lists the relations that may be managed from its pages. Every relation mutation is denied until the subclass opens it:

class RolesRelation extends AdminRelation<User, Role> {
  RolesRelation(AdminResource<Role> roles)
      : super(
          name: 'roles',
          relatedResource: roles,
          pivotForm: (form) => form.fields([TextField('granted_by').required()]),
        );

  @override
  bool canAttach(Request request, User owner, Role related) => _isAdmin(request);

  @override
  bool canDetach(Request request, User owner, Role related) => _isAdmin(request);
}

@override
List<AdminRelation<User, dynamic>> relations() => [RolesRelation(roles)];

belongsTo, hasOne, hasMany and belongsToMany are managed directly; morphOne, morphMany, morphTo, morphToMany and morphedByMany are managed polymorphically. A morphTo relation declares its own morphTargets allowlist, and a submitted type is checked against it rather than against the global MorphMap.

Every relation page has a full-page selector at …/relations/{name}/select: searchable, paginated, one attach form per row, and — for many-to-many — a checkbox column with a sync form that replaces the set. It all works with JavaScript disabled.

Pages, widgets and extensions #

  • VizierPage renders a custom view at its own slug and appears in the sidebar; dashboard replaces the default DashboardPage.
  • VizierWidget renders a view inside the dashboard.
  • VizierExtension bundles resources, pages, dashboard widgets and translations under a package name, so a third-party package can plug a whole section into a panel with one entry in extensions.

Localization #

Panels ship English and Arabic catalogs, and Arabic renders right-to-left. defaultLocale, supportedLocales, rtlLocales, localeResolver and translations on Panel control the rest; resources and actions accept per-locale labels through singularLabelFor, pluralLabelFor and localizedLabels.

Appearance #

The panel wears Maat's identity: a Nile navigation rail in both themes, Solar Gold marking the active section and the primary action in dark mode, Faience for success and Oxide for anything destructive. Dark mode is written before first paint, remembered in localStorage, and defaulted from prefers-color-scheme.

Every painted value resolves through one layer of custom properties, so an application restyles the whole panel without touching a component rule:

/* public/vendor/vizier/vizier.css, after vizier:install */
:root {
  --vizier-rail-bg: #101828;   /* the navigation column */
  --vizier-accent: #2563eb;    /* brand tile, active navigation marker */
  --vizier-solid-bg: #2563eb;  /* primary buttons */
  --vizier-solid-fg: #ffffff;
}

The layout is written in logical properties, so Arabic mirrors from dir="rtl" alone. Below 48rem the rail becomes a drawer and every control grows to a 2.75rem touch target. See Appearance for the full token list.

Customizing the templates #

vizier:install publishes the Khnum templates to resources/views/vendor/vizier/ and the CSS and JavaScript to public/vendor/vizier/. Edit them freely; your application's copies win over the package defaults.

Assets and deployment #

The CSS and JavaScript are compiled into a generated Dart constant, so an AOT-compiled binary can publish them without reading the package directory. After editing resources/css/vizier.css or resources/js/vizier.js:

dart run tool/build_vizier_assets.dart          # regenerate
dart run tool/build_vizier_assets.dart --check  # CI: fail if stale

Alpine is vendored at a pinned version with its license; nothing is fetched from a CDN at runtime.

dart compile exe bin/server.dart -o build/admin
APP_BASE_PATH=/srv/admin ./build/admin

Use dart build cli --target bin/server.dart -o build/aot instead when a dependency ships native build hooks (the SQLite driver does); it emits the same executable plus its native libraries.

The binary reads the published resources/views/ and public/ directories at APP_BASE_PATH; serve public/ and keep APP_DEBUG false in production.

Scope #

This alpha registers one panel per application, mounted at /admin by default; there is no multi-panel infrastructure. It uses no reflection or model-source parsing — every slug, field, column, relation, morph target and action comes from a registered definition — and every operation keeps a plain page route that works without JavaScript. The field list is fixed to text, email, password, textarea, number, boolean, select, date, datetime and hidden.

Out of scope: file uploads, charts, global search, arbitrary filters, soft-delete management, multi-tenancy, MFA, OAuth, runtime themes, and marketplace or plugin discovery.

Documentation #

The full guide lives in the Maat documentation.

0
likes
120
points
147
downloads

Documentation

API reference

Publisher

verified publishergaitco.com

Weekly Downloads

Resource-driven server-rendered admin panels for Maat applications.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

anubis, khnum_maat, maat, path, seshat_maat

More

Packages that depend on vizier