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, whichPanel::pages()returns by default; keep it in the list when you overridepages(). See Panels → Dashboard. - It shows the widgets the user may view, in the layout of the user.
Layout
Section titled “Layout”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.
Arranged by every user
Section titled “Arranged by every user”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 dashboards
Section titled “Own dashboards”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.