| Install | |
|---|---|
composer require rjp2525/laravel-dashboards |
|
| Latest Version: | v0.1.4 |
| PHP: | ^8.4 |
This package provides a full-featured, customizable dashboard system for Laravel applications. It pairs a powerful PHP backend of widget registration, data providers, caching, ACL, presets, real-time broadcasting with a Vue 3 + Inertia.js + GridStack.js frontend that lets users drag, drop and resize widgets.
Once installed you can create dashboards like this:
use Reno\Dashboard\Facades\Dashboard;
use Reno\Dashboard\Enums\WidgetType;
use App\Models\Order;
Dashboard::widget('revenue')
->label('Revenue')
->type(WidgetType::STAT)
->using(fn ($context) => WidgetData::stat(
value: Order::whereBetween('created_at', $context->dateRange())->sum('total'),
))
->pollEvery(30)
->cache(300)
->register();
You can install the package via Composer:
composer require rjp2525/laravel-dashboards
Run the install command to publish the config file and run migrations:
php artisan dashboard:install
You can publish the config file manually with:
php artisan vendor:publish --tag="dashboard-config"
And the migrations with:
php artisan vendor:publish --tag="dashboard-migrations"
The most common way to register widgets is through the fluent builder on the Dashboard facade, typically in a service provider:
use Reno\Dashboard\Facades\Dashboard;
use Reno\Dashboard\Enums\WidgetType;
use Reno\Dashboard\Support\WidgetData;
// Stat widget with a simple callback
Dashboard::widget('total-users')
->label('Total Users')
->type(WidgetType::STAT)
->icon('users')
->using(fn ($context) => WidgetData::stat(
value: User::count(),
))
->cache(600)
->register();
// Chart widget using an Eloquent data provider
Dashboard::widget('signups-chart')
->label('Daily Signups')
->type(WidgetType::LINE)
->provider(
EloquentDataProvider::for(User::class)
->count()
->dateColumn('created_at')
)
->position(0, 0, 6, 3)
->pollEvery(60)
->register();
You can also create dedicated widget classes:
php artisan dashboard:widget RevenueWidget
This generates a widget class in app/Dashboard/Widgets:
use Reno\Dashboard\Widgets\StatWidget;
use Reno\Dashboard\Contracts\DataProvider;
use Reno\Dashboard\DataProviders\EloquentDataProvider;
class RevenueWidget extends StatWidget
{
public function key(): string
{
return 'revenue';
}
public function label(): string
{
return 'Revenue';
}
public function dataProvider(): DataProvider
{
return EloquentDataProvider::for(Order::class)
->sum('total')
->dateColumn('created_at');
}
}
Register class-based widgets using the manager:
use Reno\Dashboard\Facades\Dashboard;
Dashboard::register(RevenueWidget::class);
For the simplest setup, annotate your Eloquent models or service classes with PHP attributes and the package will auto-discover and register widgets for you.
Stat widgets on models — add #[DashboardStat] to generate stat widgets backed by EloquentDataProvider:
use Reno\Dashboard\Attributes\Dashboardable;
use Reno\Dashboard\Attributes\DashboardStat;
#[Dashboardable(dateColumn: 'ordered_at', dashboard: 'sales')]
#[DashboardStat(label: 'Total Orders', aggregate: 'count')]
#[DashboardStat(label: 'Revenue', aggregate: 'sum', column: 'total_amount', icon: 'currency-dollar')]
class Order extends Model
{
// ...
}
The #[Dashboardable] attribute is optional and provides shared defaults (date column, dashboard scope, model scope) that cascade to all #[DashboardStat] on the same class. Each stat attribute can override these defaults individually.
Widget keys are auto-generated from the model name and aggregate: order_count, order_sum_total_amount. You can set a custom key via the key parameter.
Supported aggregates: count, sum, avg, min, max.
Custom widgets on methods — use #[AsWidget] on a public static method that accepts WidgetContext and returns WidgetData:
use Reno\Dashboard\Attributes\AsWidget;
use Reno\Dashboard\Enums\WidgetType;
use Reno\Dashboard\Support\WidgetContext;
use Reno\Dashboard\Support\WidgetData;
class AnalyticsService
{
#[AsWidget(key: 'conversion_rate', label: 'Conversion Rate', type: WidgetType::STAT)]
public static function conversionRate(WidgetContext $context): WidgetData
{
$rate = // ... your logic
return WidgetData::stat(value: $rate);
}
}
Configuration — discovery is enabled by default. Configure which directories to scan in config/dashboard.php:
'discovery' => [
'enabled' => true,
'paths' => [
app_path('Models'),
app_path('Widgets'),
],
],
Production caching — for production, cache the discovery manifest to avoid scanning on every request:
php artisan dashboard:discover-cache
Clear the cache during deployment or development:
php artisan dashboard:discover-clear
All attributes support cacheTtl, permissions, dashboard, and icon parameters for fine-grained control.
The package ships with the following widget types:
| Type | Class | Description |
|---|---|---|
stat |
StatWidget |
Single number with change indicator |
line |
ChartWidget |
Line chart |
bar |
ChartWidget |
Bar chart |
area |
ChartWidget |
Area chart |
pie |
PieChartWidget |
Pie chart |
donut |
PieChartWidget |
Donut chart |
table |
TableWidget |
Data table with pagination |
listing |
ListWidget |
Simple list of items |
progress |
ProgressWidget |
Progress bar |
heatmap |
HeatmapWidget |
GitHub-style contribution heatmap |
status_timeline |
StatusTimelineWidget |
Service uptime timeline |
sparkline |
SparklineWidget |
Stat with inline sparkline chart |
progress_circle |
ProgressCircleWidget |
Circular/radial progress indicator |
bar_list |
BarListWidget |
Ranked horizontal bar list |
funnel |
FunnelWidget |
Conversion funnel visualization |
category |
CategoryWidget |
Category breakdown display |
budget |
BudgetWidget |
Budget vs. actual comparison |
gauge |
GaugeWidget |
Gauge/dial meter |
custom |
CustomWidget |
Your own Vue component |
Data providers encapsulate how widget data is fetched. The package includes several built-in providers:
EloquentDataProvider — query Eloquent models with automatic date scoping:
EloquentDataProvider::for(Order::class)
->sum('total')
->dateColumn('created_at')
->scope('completed')
->query(fn ($query, $context) => $query->where('region', $context->filters['region'] ?? null));
QueryBuilderDataProvider — raw query builder for complex queries:
QueryBuilderDataProvider::for('analytics_events', 'analytics')
->count()
->dateColumn('occurred_at');
CallbackDataProvider — simple closure for quick widgets:
CallbackDataProvider::from(fn ($context) => WidgetData::stat(value: 42));
ApiDataProvider — fetch data from external APIs:
ApiDataProvider::from('https://api.example.com/metrics')
->headers(['Authorization' => 'Bearer ' . config('services.metrics.token')])
->timeout(10)
->transform(fn ($response, $context) => WidgetData::stat(value: $response['total']));
RawSqlDataProvider — escape hatch for raw SQL:
RawSqlDataProvider::from('SELECT COUNT(*) as total FROM orders WHERE created_at BETWEEN ? AND ?')
->bindingsFrom(fn ($context) => $context->dateRange());
Widgets automatically support period-based filtering and comparison. The available periods are:
today, 7d, 30d, 90d, ytd, 1y, customPeriod comparison calculates the change between the current and previous period:
$data = WidgetData::stat(
value: 1500,
previousValue: 1200,
);
$data->change; // 300
$data->changePercent; // 25.0
$data->changeDirection; // ChangeDirection::POSITIVE
Each user gets their own dashboard layout stored in the database. Users can drag, drop, and resize widgets to customize their view.
Presets let administrators define reusable layouts:
// Create a system preset via artisan
php artisan dashboard:preset create --dashboard=main --name="Executive View" --system
Or programmatically:
use Reno\Dashboard\Actions\CreatePreset;
use Reno\Dashboard\Actions\ApplyPreset;
// Create a preset
$preset = (new CreatePreset())->execute($user, 'main', 'My Layout', $layoutArray);
// Apply a preset to a user's dashboard
(new ApplyPreset())->execute($user, $preset->id);
Layout resolution priority:
The package includes a pluggable ACL system with three built-in drivers:
Policy driver (default) — uses each widget's authorize() method:
class RevenueWidget extends StatWidget
{
public function authorize(?Authenticatable $user): bool
{
return $user?->hasRole('manager');
}
}
Spatie driver — integrates with spatie/laravel-permission:
// config/dashboard.php
'acl' => [
'driver' => 'spatie',
],
// Widget registration
Dashboard::widget('revenue')
->permissions(['view-revenue', 'access-dashboard'])
->register();
Sync permissions to the Spatie tables:
php artisan dashboard:permissions
Custom driver — implement your own:
// config/dashboard.php
'acl' => [
'driver' => 'custom',
'custom_driver' => App\Dashboard\MyAclDriver::class,
],
Dashboard-level authorization is handled by policies (DashboardPolicy and PresetPolicy) which control view, editLayout, manage, create, update, and delete actions.
Widget data is automatically cached to minimize expensive queries:
// config/dashboard.php
'cache' => [
'enabled' => true,
'store' => null, // null = default store
'prefix' => 'dashboard',
'default_ttl' => 300, // 5 minutes
'tags_enabled' => false,
],
Per-widget cache control:
Dashboard::widget('expensive-report')
->cache(3600) // 1 hour
->register();
Warm the cache for all widgets:
php artisan dashboard:warm
php artisan dashboard:warm --dashboard=main --period=7d
The widget data API endpoints support ETag headers for efficient polling — clients receive 304 Not Modified when data hasn't changed.
The package supports four refresh strategies that control how widgets receive updated data:
| Strategy | Description |
|---|---|
poll |
Default. Fetches widget data via HTTP on a timer (setInterval + fetch). |
push |
Listens for server-sent events via Laravel Echo (Reverb, Pusher, Ably, etc.). |
inertia |
Uses Inertia.js partial reloads to refresh widget props on a timer. |
manual |
No automatic refresh. Data is only loaded on initial mount or explicit call. |
Use the fluent builder or dedicated widget class methods:
// Poll every 30 seconds (default strategy)
Dashboard::widget('active-users')
->pollEvery(30)
->register();
// Push updates via broadcasting
Dashboard::widget('live-orders')
->pushUpdates()
->register();
// Inertia partial reload every 60 seconds
Dashboard::widget('revenue')
->inertiaPolling(60)
->register();
// Manual refresh only
Dashboard::widget('annual-report')
->manualRefresh()
->register();
Or use refreshUsing() with the RefreshStrategy enum for full control:
use Reno\Dashboard\Enums\RefreshStrategy;
Dashboard::widget('stats')
->refreshUsing(RefreshStrategy::INERTIA, interval: 45)
->register();
The realtime.adapter config controls the default polling mechanism on the frontend. When set to 'inertia', all poll strategy widgets are automatically upgraded to use Inertia partial reloads instead of HTTP fetch:
// config/dashboard.php
'realtime' => [
'adapter' => 'inertia', // 'fetch' (default) or 'inertia'
],
Enable broadcasting to push widget updates to connected clients via Laravel Echo:
// config/dashboard.php
'broadcasting' => [
'enabled' => true,
'channel_prefix' => 'dashboard',
],
The package dispatches three broadcast events:
WidgetDataUpdated — when widget data changesDashboardSaved — when a user saves their layoutPresetApplied — when a preset is appliedTrigger updates from your application code:
use Reno\Dashboard\Jobs\RefreshWidgetCache;
// Dispatch after an order is placed
RefreshWidgetCache::dispatch('revenue', 'main', '30d');
Frontend setup — the package reads window.Echo at runtime. Configure Laravel Echo in your app's bootstrap file as you normally would (the package does not bundle laravel-echo):
// resources/js/bootstrap.ts
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Echo = new Echo({
broadcaster: 'reverb', // or 'pusher', 'ably', etc.
// ...your config
});
All widgets on the same dashboard share a single channel subscription (dashboard.{slug}). The useEcho composable filters incoming events by widget_key so each widget only reacts to its own updates.
If window.Echo is not available, the composable logs a warning and the widget falls back gracefully (no crash).
When a widget uses the inertia strategy (or the global adapter is set to 'inertia'), the frontend uses router.reload() from @inertiajs/vue3 with only: ['widgets'] to perform a partial page reload. This is useful when you want to leverage Inertia's server-side data hydration instead of separate API calls.
The @inertiajs/vue3 package is dynamically imported at runtime, so it's not required if you don't use this strategy.
The package exports useEcho and useInertiaPolling composables for advanced use cases:
import { useEcho, useInertiaPolling } from '@rjp2525/laravel-dashboards';
// Listen for push updates on a specific dashboard
const { connected, disconnect } = useEcho('main', 'dashboard', (widgetKey, data) => {
console.log(`Widget ${widgetKey} updated:`, data);
});
// Start Inertia partial reloads every 30 seconds
const { start, stop } = useInertiaPolling(30);
start();
For Livewire-based applications, the package includes two Livewire 3 components that provide the same widget rendering and real-time update capabilities without requiring Vue or Inertia.
Requirements — install Livewire 3 in your application:
composer require livewire/livewire "^3.0"
The package auto-detects Livewire and registers the components automatically. No manual registration is needed.
Render an entire dashboard with all its authorized widgets:
<livewire:livewire-dashboard :slug="'main'" :period="'30d'" />
Render a single widget anywhere in your Blade templates:
<livewire:dashboard-widget
widget-key="revenue"
dashboard-slug="main"
period="30d"
/>
Widgets using the poll strategy automatically include wire:poll with the configured interval. Push strategy widgets listen for Echo events via Livewire's #[On] attribute — updates broadcast on dashboard.{slug} are automatically received and filtered by widget key.
To customize the Livewire Blade templates:
php artisan vendor:publish --tag="dashboard-views"
This publishes the templates to resources/views/vendor/dashboard/livewire/.
Widgets can be exported to CSV:
// config/dashboard.php
'export' => [
'enabled' => true,
'formats' => ['csv'],
'max_rows' => 10000,
],
The export adapts to the widget type — stat widgets export as a single row, chart widgets export series data, and table widgets export all rows.
For multi-tenant applications, the package can automatically scope widget data:
// config/dashboard.php
'tenancy' => [
'enabled' => true,
'resolver' => App\Dashboard\TenantResolver::class,
'column' => 'tenant_id',
],
Data providers automatically filter by the resolved tenant when tenancy is enabled.
The package registers the following API routes (configurable prefix, default api/dashboard):
| Method | URI | Description |
|---|---|---|
GET |
/widgets/{key}/data |
Fetch widget data |
POST |
/widgets/batch |
Fetch multiple widgets in one request |
GET |
/widgets/{key}/export |
Export widget data |
GET |
/{slug}/layout |
Load user layout |
PUT |
/{slug}/layout |
Save user layout |
GET |
/{slug}/presets |
List presets |
POST |
/{slug}/presets |
Create preset |
GET |
/{slug}/presets/{id} |
Show preset |
PUT |
/{slug}/presets/{id} |
Update preset |
DELETE |
/{slug}/presets/{id} |
Delete preset |
POST |
/{slug}/presets/{id}/apply |
Apply preset |
A web route serves the Inertia dashboard page:
| Method | URI | Description |
|---|---|---|
GET |
/dashboard/{slug?} |
Dashboard page |
The package includes a Vue 3 + TypeScript frontend built with GridStack.js for the grid layout and ECharts for charts.
Vue components:
Dashboard — root grid containerDashboardToolbar — period selector, edit toggle, preset pickerWidgetWrapper — widget container with header and error boundaryWidgetPicker — sidebar to add widgets in edit modePresetManager — save, load, and share presetsPeriodSelector — date range pickerStatWidget, ChartWidget, PieChartWidget, TableWidget, ListWidget, ProgressWidget, CustomWidgetComposables:
useDashboard() — dashboard state, editing mode, layout management, broadcastingEnabled and realtimeAdapter refsuseWidget(definition) — widget data, loading, error, refresh with automatic strategy resolution (poll/push/inertia/manual)useWidgetData() — data fetching with ETag supportuseGridStack() — GridStack initialization and eventsusePeriod() — period selection stateusePermissions() — permission checks from Inertia shared datauseFetchClient() — centralized fetch wrapper with XSRF injection and error interceptionuseEcho(slug, prefix, callback) — Echo/Reverb/Pusher listener for push updatesuseInertiaPolling(interval) — Inertia.js partial reload pollingPublishing and styling components:
The package ships pre-built CSS and JS via dist/. Import the stylesheet in your application's entry point:
// resources/js/app.ts
import '@rjp2525/laravel-dashboards/dist/laravel-dashboards.css';
The default styles use CSS custom properties so you can override the theme without editing package files:
:root {
--dashboard-bg: #ffffff;
--dashboard-widget-bg: #f9fafb;
--dashboard-widget-border: #e5e7eb;
--dashboard-widget-radius: 0.5rem;
--dashboard-widget-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
--dashboard-text-primary: #111827;
--dashboard-text-secondary: #6b7280;
--dashboard-accent: #3b82f6;
--dashboard-positive: #10b981;
--dashboard-negative: #ef4444;
}
For dark mode, the package respects prefers-color-scheme: dark automatically when the config theme is set to auto, or you can force it:
// config/dashboard.php
'frontend' => [
'theme' => 'dark', // auto, light, dark
],
All internal fetch calls (layout saves, preset operations, widget data) go through a centralized fetch client. You can register global error handlers using onFetchError to hook into failures — for example, to show toast notifications or report to an error tracking service:
import { onFetchError } from '@rjp2525/laravel-dashboards'
// Register a global error handler (e.g. in app.ts)
const unsubscribe = onFetchError((context) => {
console.error(`Dashboard API error: ${context.method} ${context.url} — ${context.status}`)
toast.error(`Dashboard error: ${context.statusText}`)
})
// Optionally unsubscribe later
unsubscribe()
The context object passed to handlers has the following shape:
interface FetchErrorContext {
url: string // Full request URL
method: string // HTTP method (GET, POST, PUT, DELETE)
status?: number // HTTP status code
statusText?: string // HTTP status text
body?: unknown // Parsed JSON error body (when available)
error: Error // The Error instance that will be thrown
}
Handlers are called before the error is thrown, so individual callers can still catch errors locally if needed. Multiple handlers can be registered and each receives the same context.
To register a custom Vue widget component, use the CustomWidget type and point to your component:
Dashboard::widget('my-custom')
->label('My Custom Widget')
->type(WidgetType::CUSTOM)
->component('MyCustomWidget')
->using(fn ($context) => WidgetData::stat(value: 42))
->register();
Then register the component in your Vue app:
import MyCustomWidget from './components/MyCustomWidget.vue';
app.component('MyCustomWidget', MyCustomWidget);
Your custom component receives widget (definition) and data (WidgetData) as props.
The full config file (config/dashboard.php) covers:
fetch or inertia)echarts, apexcharts, chartjs), theme, localecomposer test
Please see CHANGELOG for more information on what has changed recently.
Please see CONTRIBUTING for details.
Please review our security policy on how to report security vulnerabilities.
The MIT License (MIT). Please see License File for more information.