Skip to content

Dashboard

The start page of a panel shows the widgets of Panel::widgets(), followed by the resources the user may open.

use Anvil\Panel\Panel;
use Anvil\Widget\GroupedStatsWidget;
use Anvil\Widget\StatWidget;
use Anvil\Widget\TableWidget;
final class AdminPanel extends Panel
{
public function widgets(): array
{
return [
StatWidget::make('Open tasks')->key('open-tasks')
->value(static fn (): int => (new TaskModel())->where('status', 'open')->countAllResults())
->icon('list-check')->url($this->url('tasks'))->permission('tasks.view'),
StatWidget::make('New this week')->key('new-people')
->value(static fn (): int => (new PersonModel())->where('created_at >=', date('Y-m-d', strtotime('monday this week')))->countAllResults())
->icon('person-plus')->color('success')->permission('people.view'),
TableWidget::make('Latest requests')->key('latest-requests')
->resource(PurchaseRequestResource::class)->columns(['title', 'approval_state', 'amount'])
->query(static fn ($model) => $model->orderBy('id', 'desc'))->limit(5)->span(6),
];
}
}
  • The dashboard is Anvil\Page\Dashboard, which Panel::pages() returns by default; keep it in the list when you override pages(). See Panels → Dashboard.
  • It shows the widgets the user may view, in the layout of the user.

Widgets sit in a 12 column grid. The width on large screens comes from span():

Widget Default Range
StatWidget 3 2–12
GroupedStatsWidget 12 2–12
ChartWidget 6 3–12
TableWidget 6 3–12

Values outside the range are clamped. These are the defaults; every user can change them.

Customize switches the dashboard to an editor: users drag widgets (also on touch screens) or move them with the arrow buttons, choose the width (¼, ⅓, ½, ⅔, ¾, full), hide widgets and show them again, or restore the defaults. Each screen area has its own order, visibility and widths:

Area Screen width Width
phone below 768 px always full
tablet 768–1199 px own, default ½ (full for span(8) and more)
desktop from 1200 px own, default span()

The editor works on the area of the current screen: on a tablet the tablet layout is changed, on a desktop the desktop layout.

  • Layouts are personal preferences per user, tenant and page (stored in anvil_collection_views, not audited), like the column choice of lists.
  • The browser only sends keys, areas and widths; the server checks them against the widgets the user may see. A widget without permission is never rendered, whatever the layout says.
  • Widgets hidden in all areas do not run their queries.

Layouts are stored by the key of a widget:

GroupedStatsWidget::make('Purchase requests')->key('purchase-stats')
  • Keys: lower case letters, digits and -, up to 50 characters.
  • Without key() the key is derived from the label; a changed label then starts with the default layout for this widget.
  • Widgets added to the code appear at their place in the code; removed ones are ignored.

Own pages with widgets use the same editor through WidgetBoard:

use Anvil\Endpoint\Component;
use Anvil\Page\HasComponents;
use Anvil\Page\Page;
use Anvil\Widget\Layout\WidgetBoard;
final class SalesPage extends Page implements HasComponents
{
public function title(): string { return 'Sales'; }
public function component(string $kind): ?Component
{
return $kind === WidgetBoard::KIND ? $this->board() : null;
}
public function content(): string
{
return $this->board()->render();
}
private function board(): WidgetBoard
{
return WidgetBoard::for($this->panel, $this, fn (): array => [/* widgets */]);
}
}

The page needs its own permission (default <panel>.page.<slug>); the layouts of the users are stored per page.