Ui5Dashboard
Introduction
A Ui5Dashboard is a routable composition container: a named artifact that aggregates Groups, each of which composes Tiles, Cards and Charts into a layout.
A dashboard is not a page you render. It has no Blade template and no HTML endpoint. It serves one JSON manifest describing a UI5 control tree, which the <lux:Dashboard> control in @laravelui5/core walks and materialises into native controls on the client. Server-side you author artifacts; the framework turns them into a control tree.
Two artifact types work together and are documented on this one page, because neither means anything alone:
| Type | ArtifactType | Routable | Role |
|---|---|---|---|
| Dashboard | Dashboard = 7 | Yes — dashboard/{ns}@{ver}/manifest.json | the root container; holds Groups |
| Dashboard Group | DashboardGroup = 13 | No | the composition node; names the Tiles/Cards/Charts it renders |
Conceptual Overview
Three seats, and who owns which
One rule explains every method on both interfaces — and prevents the mistake that costs the most time:
- You own the properties. Each artifact returns a template DTO for the control it renders as —
getVBox()on the dashboard,getPanel()andgetGridContainer()on the group. Set any property you like on them. - The framework owns the child aggregations. It injects the built children via
withItems()/withContent()on those DTOs. Never setitemsorcontentyourself — anything you put there is silently overwritten. - The registry owns resolution. A group names its children by namespace string, not by instance. The framework resolves each against the registry.
So a group declares which children and how the panel looks; it never holds the children and never builds their controls.
Properties are SAP control properties
A dashboard renders as a fixed nesting — VBox → Panel → GridContainer → children — and those are literally sap.m.VBox, sap.m.Panel and sap.f.GridContainer. The DTOs mirror the real control APIs one-to-one.
That is why the group header is headerText and not title: it is the Panel's own property name. Look properties up in the OpenUI5 API reference, not here — if sap.m.Panel has it, the Panel DTO takes it.
How to Generate
Two commands, one per type:
php artisan ui5:dashboard Sales/Overview
php artisan ui5:group Sales/ThisMonth| Command | Argument | Creates |
|---|---|---|
ui5:dashboard | {AppName}/{DashboardName} | src/Dashboards/{Name}.php |
ui5:group | {ModuleName}/{GroupName} | src/Groups/{Name}Group.php |
The PHP and UI5 namespaces derive from the target module — there are no namespace-prefix options.
Output
Given ui5:dashboard Sales/Overview and three ui5:group runs:
ui5/
└── Sales/
└── src/
├── Dashboards/
│ └── Overview.php
└── Groups/
├── ThisMonthGroup.php
├── PipelineGroup.php
└── PerformanceGroup.phpNo Blade files. A dashboard has no template — if you are looking for one, you are looking for the old mechanism.
Artifact Roles
Ui5Dashboard
Identity is declared as class constants; the getters come from AbstractUi5Dashboard. The only required behaviour is naming the groups:
class Overview extends AbstractUi5Dashboard
{
public const string NAMESPACE = 'com.acme.sales.dashboards.overview';
public const string VERSION = '1.0.0';
public const string TITLE = 'Sales Overview';
public const string DESCRIPTION = 'Pipeline, performance and this month at a glance';
/** @return Ui5DashboardGroupInterface[] */
public function getGroups(): array
{
return [
new ThisMonthGroup($this->module),
new PipelineGroup($this->module),
new PerformanceGroup($this->module),
];
}
}Groups are held as instances — a dashboard composes its own groups directly. (Children inside a group are the opposite: namespaces, resolved by the registry. The asymmetry is deliberate; see Why namespaces, not instances.)
Group order is display order. If instantiating is expensive, cache the array in the constructor and return the property instead — the framework calls getGroups() once per request.
To customise the root layout, override getVBox():
public function getVBox(): VBox
{
return new VBox(
renderType: FlexRendertype::Bare,
class: Margin::Responsive,
);
}Set anything except items — the framework injects the built group elements through VBox::withItems().
Ui5DashboardGroup
class PerformanceGroup extends AbstractUi5DashboardGroup
{
public const string NAMESPACE = 'com.acme.sales.groups.performance';
public const string VERSION = '1.0.0';
public const string TITLE = 'Performance';
public const string DESCRIPTION = 'Outcomes — quota, revenue mix, and account detail';
public function getChildNamespaces(): array
{
return [
QuotaAttainmentChart::NAMESPACE,
RevenueMixChart::NAMESPACE,
AccountSnapshotCard::NAMESPACE,
];
}
}getChildNamespaces() is the only abstract method on the base — everything else has a working default. The array order is the display order.
The list does not have to be literal. It is resolved per request, so a group may compute it — the LUX Launchpad harvests every tile carrying a placement marker and returns the namespaces sorted by weight. The contract is only that each returned namespace resolves to a registered artifact.
AbstractUi5DashboardGroup gives you a sap.m.Panel with headerText bound to the group's title and a bare sap.f.GridContainer. Override either to customise:
public function getPanel(): Panel
{
return new Panel(
headerText: $this->getTitle(),
expandable: true,
backgroundDesign: BackgroundDesign::Transparent,
);
}
public function getGridContainer(): GridContainer
{
return new GridContainer(snapToRow: true);
}Set anything except Panel.content and GridContainer.items — the framework owns that nesting and injects through Panel::withContent() and GridContainer::withItems().
Registration
A dashboard is a module artifact, registered like every other:
class SalesModule extends AbstractUi5Module
{
public function getDashboards(): array
{
return [new Dashboards\Overview($this)];
}
public function getCharts(): array
{
return [
new Charts\QuotaAttainmentChart($this),
new Charts\RevenueMixChart($this),
];
}
public function getCards(): array
{
return [new Cards\AccountSnapshotCard($this)];
}
}Every child must also be registered on its module
A namespace listed in getChildNamespaces() is resolved against the registry. If the artifact was never registered via getCards() / getTiles() / getCharts(), composition fails loudly with UnregisteredDashboardChildException — it does not silently 404 later when the client fetches a card manifest. Listing a child and registering it are two separate, deliberate acts.
Groups themselves are not registered on the module. They are reached through the dashboard's getGroups(), and that is their only entry point.
Why namespaces, not instances
A group could have held its children as objects. It names them instead, for one reason: a namespace is a reference another package can also produce. That enables the cross-module composition below, and it keeps a group's declaration free of use statements pointing into foreign packages.
Cross-module composition
Since Core 1.1.0.
A composer require'd module can drop one of its Tiles/Cards/Charts into another module's group without editing that group — the LUX "compose across modules" promise.
The contributing module registers one line in its service provider:
public function boot(): void
{
app(DashboardGroupCollector::class)->add(
PerformanceGroup::NAMESPACE,
MyForecastChart::NAMESPACE,
);
}- Contributions are artifact namespace strings, same currency as
getChildNamespaces(). add()is idempotent per(group, artifact)pair.- Registration order is render order; own children come first, then contributions, deduped.
- A contributed child resolves through the registry exactly like an own child — so it must be a normally registered artifact on its own module.
A group never lists foreign contributions itself, and never learns that it received any. With an empty collector the walk is a plain 1:1 render — the Core-only default.
Slots
Both types implement SlotProposableInterface: they consume slots (getRequiredSlots()) and propose them to the composition (getSlotProposals()). A dashboard is a composition node, so it does both.
public function getRequiredSlots(): array
{
return [CoreSlots::Period, SalesSlots::Region];
}
public function getSlotProposals(): array
{
return [CoreSlots::Period->value => 'current-quarter'];
}Core ships seven canonical slots on CoreSlots (currency, locale, timezone, period, date, date_from, date_to); anything domain-specific is your own backed enum. Proposals are keyed by the slot's backing value — PHP forbids enum instances as array keys.
A proposal is what this node offers downward: a child that requires period and receives no request override resolves it from the composition. The full precedence chain (Request → Actor → Composition → Setting) is in Slots.
Identity never travels through a slot. A provider reads the actor from $context; slots carry wire-shaped values only.
Visibility: the veto chain
To hide a group or a child for some actors, you do not filter getGroups() or getChildNamespaces() — those stay declarative. Register a VetoerInterface by container tag; it answers one question about one artifact:
public function dispose(
Ui5ArtifactInterface $artifact,
Ui5ContextInterface $context,
): Disposition;The chain folds all registered vetoers most-restrictive-wins — any Hide wins and short-circuits, else any Lock, else Show. That makes the outcome order-independent, and a vetoer needs to know neither the other vetoers nor the tree.
The chain is consulted before a hidden artifact is built, so it is pruned pre-data — its provider never runs, and it costs nothing. An empty chain returns Show for everything, which is Core's default. Lock is declared and folds above Show, but is not yet enforced.
This is the seat for access control, lifecycle and personalization alike. A vetoed child leaves no trace on the wire — see the errors note under Failure isolation for how to tell the two apart.
Failure isolation
A child whose provider throws does not 500 the endpoint. Its container emitter catches the cause, records it in the EmitErrorSink, omits the node, and the siblings render. The same applies one level up: a group that throws is dropped while its sibling groups survive.
After the walk the controller report()s each cause once (server-side observability) and stamps the sanitised entries into the envelope's errors array. The wire reason is debug-gated — full Class: message in dev, a stable generic line in production. Raw exception text never reaches a customer browser.
The one unguarded seat is the root VBox: if that throws there is nothing to render, so it bubbles as a whole-dashboard failure.
What the endpoint actually returns
Useful when a tile is missing from the rendered dashboard and you want to know why. The manifest response wraps the control tree in an envelope:
{
"schemaVersion": "1.0",
"namespace": "com.acme.sales.dashboards.overview",
"title": "Sales Overview",
"description": "Pipeline, performance and this month at a glance",
"params": ["period", "region"],
"tree": { "sap.m.VBox": { "…": "…" } },
"errors": []
}errors— one entry per isolated child. A hole in the dashboard with an emptyerrorsarray means the child was vetoed, not that it failed.params— the dashboard's owngetRequiredSlots(), by backing value, so the client knows what it may vary.tree— the control tree. Each node is{"<ui5.class.name>": { …properties }}, recursive through child aggregations.
The response carries Cache-Control: no-store.
Consuming the manifest
The client side is the <lux:Dashboard> control from @laravelui5/core, which fetches the manifest, walks the tree and materialises native controls.
It emits one action event for both consumer-handled card CTAs and tile presses. oEvent.getSource() is the <lux:Dashboard> control itself (idiomatic UI5); the originating child arrives as the child parameter, alongside type ("Custom" for a card action, "Press" for a tile) and the verbatim parameters. Consumers switch on parameters.method, never on type — type is provenance, not routing.
Raw Navigation card actions do not surface: the card navigates itself.
See the frontend reference for the control's API.
Best Practices
- One dashboard per audience, not per data source. Groups are the seam for topical structure; a second dashboard is warranted when a different person looks at it.
- Keep groups small. A group is a Panel of a GridContainer — once it needs a scroll of its own, it wants to be two groups.
- Push work to the leaves. A dashboard resolves nothing itself; the data cost lives in each Tile/Card/Chart provider, where failure isolation and caching apply per child.
- Let children fail. Do not defensively try/catch inside a provider to return an empty shape — a recorded, omitted child is more honest than a rendered lie.
- Reach for a vetoer, not a subclass, whenever the question is "should this be visible for this actor".
Related
- Ui5Tile, Ui5Card, Ui5Chart — the children
- Slots — the resolution chain
- Ui5Module — where artifacts are registered
sap.m.VBox·sap.m.Panel·sap.f.GridContainer