lara-zeus/dynamic-dashboard
A Laravel package by Lara Zeus for building dynamic, customizable dashboards. Add, arrange, and configure widgets per user or role, save layouts, and extend the clean API to craft responsive admin panels quickly.
Confirm the version pairing before writing any code. 5.0.0 is a Filament 5 compatibility release and nothing else. Check the constraint rather than assuming:
composer show lara-zeus/dynamic-dashboard --all | grep -iE 'filament|php|livewire'
If your panel is still on Filament 4, do not install ^5.0 and do not install an older tag "for now" — pin the package bump to the same ticket as the Filament bump so you keep one rollback point.
Register the plugin on the panel you actually want to change:
// app/Providers/Filament/AdminPanelProvider.php
public function panel(Panel $panel): Panel
{
return $panel
->plugins([
\LaraZeus\DynamicDashboard\DynamicDashboardPlugin::make(),
]);
}
Then publish and migrate — and read the migration before you run it. Per-user layouts are user data from the first deploy, so you need to know now whether this is a JSON column on users or a dedicated dashboard_layouts table, and whether an upgrade can change it under you.
php artisan vendor:publish --tag="dynamic-dashboard-config"
php artisan vendor:publish --tag="dynamic-dashboard-migrations"
php artisan migrate
Where to look, in order. The release notes are one line, so vendor/lara-zeus/dynamic-dashboard/src is your documentation: the plugin class (register() / boot()), the config file (widget list + layout defaults), the migration, then published views. Skim those four things before you attempt anything clever — most "does this package support X?" questions are answered there in a few minutes.
First use case: close the loop on one widget. Take a single existing Filament widget (a stat card is ideal), register it through the package, log in as a non-admin user, add it, drag it, refresh, log out, log back in. Register → arrange → persist → reload. If that loop holds, the integration is sound and everything after is widget inventory work. Do this on an internal panel, never on a customer-facing surface.
Funnel widget registration through one seam. This is the highest-leverage thing you do on day one, because it converts "how do we add a widget" from a package question into a one-line diff:
final class WidgetRegistry
{
public static function all(): array
{
return [
RevenueCard::class,
ActiveUsersChart::class,
LatestOrdersTable::class,
];
}
}
Feed that array into whatever registration point the source exposes (config key or plugin method). Never let WidgetClass::class strings scatter across providers and panel configs — you'll be forking this package eventually or replacing it, and the seam is what makes that a day of work.
Write widgets to a Filament-native contract. Whatever the package's registration signature, keep each widget a normal Filament widget so it still works on the stock dashboard:
class RevenueCard extends \Filament\Widgets\StatsOverviewWidget
{
protected static ?int $sort = 1;
protected int|string|array $columnSpan = 1;
public static function canView(): bool
{
return auth()->user()?->can('viewRevenue') ?? false;
}
}
canView() is doing double duty: it hides the widget from the picker and it must be the server-side gate when a persisted layout references a widget the user can no longer see. Treat that as a requirement, not a nicety.
Model default layouts explicitly. An empty first-run dashboard is a bad product. Seed new users with a role-appropriate starter set at account creation, or point the package's global default at a sensible three-widget baseline. Because layouts are per-user, this becomes a config change instead of a build when PM wants to try 3 widgets vs 8 — that experiment velocity is the strongest ongoing argument for the package.
Keep the stock dashboard behind a flag. Add a per-panel toggle so you can switch between the dynamic page and Filament's default dashboard without a deploy. On rollback day you flip a config value; on regression day you flip it back. Cheap insurance for a single-maintainer dependency.
Migrate widgets incrementally, panel by panel. Build an inventory before touching anything: each existing widget is (a) works unchanged once registered, (b) needs re-registration, or (c) needs a rewrite. Class (c) is your actual schedule risk — not the package.
Test behaviours, not the library. The things that break in production are: panel boots, widget renders, reorder persists, layout survives logout/login, a restricted user cannot see a forbidden widget, and query counts stay sane on production-sized data. One short feature test per behaviour:
it('hides widgets the user cannot view', function () {
$user = User::factory()->create();
$this->actingAs($user)->get('/admin')->assertDontSee('Revenue');
});
Assert against whatever routes the package registers — the value is in asserting persistence and authorization, not in the endpoint path.
Document your own widget contract. Write docs/widgets.md covering "how to add a dashboard widget here" (extend this base, add to WidgetRegistry, run these tests). Future-you will not remember which of the package's behaviours were real and which were assumptions.
Upgrade coupling is the price of the package. Assume a major per Filament major. Pin ^5.0 with a committed lockfile, never bump Filament and this package in separate deploys, and git diff the published config, migration, and views before running composer update. A one-line changelog for a major means you learn about behaviour changes by reading source — so budget that time into the upgrade ticket.
Decide who owns the schema. If the package ships a migration that owns a table holding user data, an upgrade could alter or drop it. If that risk is unacceptable, copy the migration into database/migrations/, rename the table to something yours, and point the config at it. Do this at install time — retrofitting table ownership onto live data is far worse.
Orphaned layout rows will happen. Remove a widget from WidgetRegistry and existing users' saved layouts still reference it. Pick a failure mode now: filter unknown classes at render time (safe default), plus an occasional cleanup command for stale rows. The symptom if you skip this is blank gaps or silently empty cards that no one can explain.
One bad widget shouldn't blank the page. Check whether rendering is isolated per widget or whether a thrown exception takes the whole dashboard down. If it isn't isolated, wrap anything with an external dependency — API calls, slow aggregates, third-party dashboards — in try/catch and render a fallback card. Slow widgets deserve the same treatment via caching, since you can't control how many a user stacks up.
Authorization is entirely yours. The package manages layout and knows nothing about your data boundaries. Any widget backed by a query must enforce policy inside the widget. Hiding it from the picker is not authorization — a persisted layout row is a direct reference to a widget class. Verify with a restricted-role user during the spike; this is the most likely source of a visible incident.
Keep customisations on the smallest possible surface. Published config and views, nothing else. Every line you change in vendor-adjacent files is a line you re-apply on upgrade and a line that makes a fork expensive. Route all extension through your registry and the package's plugin/config API.
Rebuild and re-publish after every major. Filament majors routinely rework CSS/JS. Rebuild your theme, re-publish views and assets, and re-check the dashboard in a real browser rather than trusting committed assets — a broken layout after a bump is almost always stale assets, not a package bug.
Keep the blast radius admin-only. Don't reuse the package's layout logic outside Filament panels or expose it on customer-facing surfaces. Staff-visible outages are absorbed by staff; customer-visible ones are not — and with 16 stars and zero dependents, you are effectively the first production user of each release.
How can I help you explore Laravel packages today?