invaders-xx/filament-gridstack-dashboard
A Filament plugin that brings GridStack.js drag-and-drop dashboards to your admin panel, letting users arrange, resize, and persist widget layouts with ease.
This is a Filament panel plugin that swaps the stock dashboard for a GridStack.js canvas, so users can drag/resize dashboard widgets and have their layout persist. v1.9 is a small additive release — three new plugin options (canAccess, shouldRegisterNavigation, navigationLabel) — so there is nothing to migrate, but it's a good moment to tighten how the dashboard is exposed per panel/role.
Minimal path:
composer require invaders-xx/filament-gridstack-dashboard
php artisan vendor:publish --tag="filament-gridstack-dashboard-config" # if a config is shipped
php artisan migrate # if the layout table isn't already present
Register the plugin on the panel you want it on (usually app/Providers/Filament/AdminPanelProvider.php):
use InvadersXX\FilamentGridstackDashboard\GridStackDashboardPlugin;
public function panel(Panel $panel): Panel
{
return $panel
->plugin(GridStackDashboardPlugin::make());
}
Where to look first, in order:
src/GridStackDashboardPlugin.php — the plugin class is the whole public API surface; confirm the exact signatures of the three new setters here before wiring them up (whether canAccess takes a bool or a Closure, and whether it's evaluated per-request).resources/views/ — the Blade view that mounts GridStack and the container the JS binds to.config/filament-gridstack-dashboard.php (if published) — column counts, breakpoints, persistence settings.resources/js/ — the GridStack init; this is where drag handlers and the save call live.First use case: replace the default Dashboard page so an authenticated user sees their own widgets, reorders them once, and finds the same arrangement on their next login.
Per-panel registration. Register the plugin only on the panels that should have a grid dashboard. Filament resolves navigation from registered pages, so if the plugin is added to a panel that also renders the stock Dashboard, you'll end up with two entries — remove or disable the built-in one.
Gating with the new v1.9 options. The natural pattern is to define the dashboard once and vary exposure by role:
GridStackDashboardPlugin::make()
->canAccess(fn (): bool => auth()->user()?->can('view_grid_dashboard') ?? false)
->shouldRegisterNavigation(fn (): bool => auth()->user()?->isInternal() ?? false)
->navigationLabel('My Workspace'),
Think of these as three independent switches rather than one: canAccess controls authorisation, shouldRegisterNavigation controls whether the page appears in the sidebar, and navigationLabel only matters when navigation is registered. Using canAccess(false) with navigation still on gives you a hidden-but-reachable route that 403s — sometimes exactly what you want (deep links from emails), usually a bug.
Layout persistence as a first-class concern. The typical integration is: a widget list is declared (via Filament's getWidgets() / getHeaderWidgets() or the plugin's own registration method), the JS serialises {id, x, y, w, h} on change, and the server stores it keyed by user (and team/tenant if applicable). Treat the saved layout as hints, not as the source of truth for which widgets exist — always render the declared widget set and apply stored coordinates where an id matches.
Role-scoped dashboards. Combine the plugin with per-role widget resolution: canAccess on the dashboard plus canView() on individual widgets. Because GridStack persists positions by widget id, a saved layout containing widgets a user can no longer see should degrade gracefully — filter before handing the layout to the front end.
Multi-panel / tenant setups. If one panel serves multiple teams, key persistence by user_id and the current tenant. Reusing a single key means switching tenants silently reshuffles someone's dashboard.
Testing. The persistence endpoint is the piece worth unit-testing: assert that a posted layout is validated (widget ids belong to the user, coordinates are integers within bounds) and scoped to the authenticated user. The JS behaviour is better covered by a quick Dusk/browser smoke test on drag-and-resize than by trying to assert on GridStack internals.
canAccess accepting a Closure vs a bool changes whether you can do per-request role checks; read GridStackDashboardPlugin.php and don't assume symmetry with Filament's Page::canAccess().canAccess returning false does not remove the route. If navigation is registered, users see a menu item that errors on click. Pair access gating with shouldRegisterNavigation (or hidden()) unless the hidden-but-linkable behaviour is deliberate.shouldRegisterNavigation/navigationLabel, clear caches (php artisan filament:optimize-clear / optimize:clear) before concluding the option "doesn't work" — Filament caches component and navigation metadata.php artisan filament:assets).min-height on the grid item, or disable lazy loading for widgets placed by this plugin.->view()) and publish what you need. Any custom widget needs to be id-stable across deploys — deriving ids from class names is fine, deriving them from array indices will scramble everyone's layout on the next reorder.How can I help you explore Laravel packages today?