Skip to content

Dashboard Widget Customization (Programmatic API)

Who is this for? Package authors and app developers who programmatically register custom widgets, disable built-ins, or control the dashboard layout via code.

TL;DR: Use CapellAdmin::registerOverviewStat() for small package metrics that belong inside the Capell overview widget. Use CapellAdmin::registerDashboardWidget() only for widgets that need their own UI, such as tables, charts, calendars, or health panels.


This guide covers programmatic registration and control of dashboard widgets and overview stats — the code-level APIs for declaring what appears on each dashboard.

For end-users toggling widgets in the admin UI, see Dashboard widgets. The two APIs are layered:

  • The setEnabledWidgets() API sets defaults at app boot time
  • End-users can override those defaults per-role via Settings → Dashboard

Capell Admin ships a small set of core dashboard widgets. Optional reporting, insights, workflow, search, SEO, campaign, deployment, and operational widgets live in their owning packages and register themselves with CapellAdmin::registerDashboardWidget() only when they need a standalone dashboard surface. Small counts and status metrics should contribute to the Capell overview widget instead.

<?php
declare(strict_types=1);
namespace Capell\Admin\Enums;
enum WidgetEnum: string implements HasLabel
{
case CapellInfoWidget = AbstractCapellInfoWidget::class;
case ListPagesWidget = ListPagesWidget::class;
case MyWorkQueueWidget = MyWorkQueueWidget::class;
case PageStatusWidget = PageStatusWidget::class;
case RecentlyPublishedWidget = RecentlyPublishedWidget::class;
case SiteStatsOverviewWidget = SiteStatsOverviewWidget::class;
case UpdateAdvisoryWidget = UpdateAdvisoryWidget::class;
}

Each case’s value is the fully-qualified widget class name. The getLabel() method returns a translatable string for the Filament UI.

To add a new core built-in widget, add a case here and update the getLabel() match. This is a core change and should be coordinated with the Capell maintainers. For package-owned widgets, prefer a package service provider and dashboard settings contributor instead.

Examples:

  • capell-app/dashboard-reports owns generic report widgets such as publishing trend and content health.
  • capell-app/diagnostics owns setup, cache, registry, migration, and package health widgets.
  • capell-app/insights, capell-app/search, capell-app/seo-suite, and capell-app/campaign-studio own their domain reporting widgets.

Public API (CapellAdminManager / CapellAdmin facade)

Section titled “Public API (CapellAdminManager / CapellAdmin facade)”
MethodParametersReturnsPurpose
registerOverviewStat(...)key, label, value, optional group, description, url, color, sort, settingsKey, settingsLabel, settingsDescriptionvoidRegister a small metric inside the Capell overview widget. The metric gets a settings toggle but does not become a standalone dashboard widget.
getOverviewStats(bool $onlyEnabled = true)$onlyEnabled: whether to filter by dashboard settingsarray<CapellOverviewStatData>Resolve registered overview stats for rendering.
registerDashboardWidget(string $widgetClass, DashboardEnum ...$dashboards)$widgetClass: fully-qualified widget class; $dashboards: one or more DashboardEnum values (e.g., DashboardEnum::Main, DashboardEnum::SystemHealth)voidRegister a custom widget class to one or more dashboard pages. The widget is available for toggling in Settings but is not automatically enabled — you must call setEnabledWidgets() or let the user enable it in the UI.
getDashboardWidgets(DashboardEnum $dashboard)$dashboard: which dashboard to queryarray<string> (list of widget class names)Get the list of registered widget classes for a dashboard.
getWidgets(null|bool|Closure $filter = null)$filter: null (all built-ins), true (enabled by current user/role), false (disabled by current user/role), or a callable filterarray<string|WidgetEnum>Fetch built-in widgets from WidgetEnum::cases(). When $filter is null, returns all built-in widget classes as strings. When $filter is a boolean, returns filtered WidgetEnum cases based on user settings. When $filter is callable, applies the callable and returns matching cases.
setEnabledWidgets(array<string|WidgetEnum> $widgets)$widgets: array of widget classes or WidgetEnum cases to enablevoidReplace the enabled widget set in the database. Any built-in or custom widget NOT in this list will be marked disabled for the active role/context. Omitting a widget hides it by default (but users can re-enable it in Settings).

Use overview stats for compact package metrics: counts, percentages, current status values, or links into a package resource. This keeps the dashboard from filling up with one-off stats widgets.

<?php
declare(strict_types=1);
namespace Capell\Blog\Providers;
use Capell\Admin\Facades\CapellAdmin;
use Capell\Blog\Models\Article;
use Illuminate\Support\ServiceProvider;
final class AdminServiceProvider extends ServiceProvider
{
public function boot(): void
{
CapellAdmin::registerOverviewStat(
key: 'blog_overview.articles',
label: fn (): string => __('capell-blog::dashboard.articles'),
value: fn (): int => Article::query()->count(),
group: fn (): string => __('capell-blog::dashboard.group'),
sort: 100,
settingsKey: 'blog_overview',
settingsLabel: fn (): string => __('capell-blog::dashboard.blog_overview'),
);
}
}

Multiple stats can share the same settingsKey. In that case, Settings → Dashboard shows one toggle for the package overview while the Capell overview widget renders each enabled stat.

Use registerDashboardWidget() instead when the package needs a table, chart, calendar, form, setup panel, or richer interaction that cannot fit into a single overview stat.


Create a widget class that extends Filament’s base widget. You can subclass CapellWidget for Capell-specific features (role-gating, settings integration):

<?php
declare(strict_types=1);
namespace App\Filament\Widgets;
use Capell\Admin\Filament\Widgets\CapellWidget;
final class CustomSalesWidget extends CapellWidget
{
protected static ?string $heading = 'Sales This Month';
// Visible to these roles (config keys, resolved via capell.roles.<key>).
protected static array $rolesConfigKeys = ['admin', 'developer'];
// Settings key for the enable / disable toggle on the Settings page.
protected static string $settingsKey = 'custom_sales_widget';
protected static string $view = 'app::widgets.custom-sales';
// getData() and other Filament methods...
}

See docs/admin/dashboard-widgets.md for the full widget-authoring guide.

Call registerDashboardWidget() in your service provider’s boot() method:

<?php
declare(strict_types=1);
namespace App\Providers;
use App\Filament\Widgets\CustomSalesWidget;
use Capell\Admin\Enums\DashboardEnum;
use Capell\Admin\Facades\CapellAdmin;
use Illuminate\Support\ServiceProvider;
final class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
// Register the widget on the main dashboard
CapellAdmin::registerDashboardWidget(
CustomSalesWidget::class,
DashboardEnum::Main,
);
// Or on multiple dashboards:
CapellAdmin::registerDashboardWidget(
CustomSalesWidget::class,
DashboardEnum::Main,
DashboardEnum::SystemHealth,
);
// Extension operational widgets belong on /admin/extensions:
CapellAdmin::registerDashboardWidget(
CustomExtensionHealthWidget::class,
DashboardEnum::Extensions,
);
}
}

Important: This registers the widget as available — it does not automatically enable it. Users (or you) must toggle it in Settings → Dashboard, or use setEnabledWidgets() to set defaults (see next section).


Use setEnabledWidgets() to control which built-in widgets are enabled by default for the active role:

<?php
declare(strict_types=1);
namespace App\Providers;
use Capell\Admin\Enums\WidgetEnum;
use Capell\Admin\Facades\CapellAdmin;
use Illuminate\Support\ServiceProvider;
final class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
// Enable only these built-ins; omit any you want disabled by default
CapellAdmin::setEnabledWidgets([
WidgetEnum::SiteStatsOverviewWidget,
WidgetEnum::ListPagesWidget,
// Omit WidgetEnum::AlertsWidget, etc. to hide them by default
]);
}
}

Semantics: setEnabledWidgets() replaces the enabled widget set in the AdminSettings table. Any widget not listed is marked disabled in the database, though users can re-enable it from Settings → Dashboard at any time.


Widgets are associated with dashboard pages via DashboardEnum:

<?php
declare(strict_types=1);
namespace Capell\Admin\Enums;
enum DashboardEnum: string
{
case Main = 'main'; // Primary dashboard (/admin)
case Extensions = 'extensions'; // Extensions dashboard (/admin/extensions)
case SystemHealth = 'system_health'; // System health dashboard
case NotInstalled = 'not_installed'; // Installation dashboard
}

When registering a widget, specify which dashboard(s) it should appear on by passing the enum case(s) to registerDashboardWidget().


  • Boot timing: registerDashboardWidget() should run in boot() so the admin panel can resolve all service providers before rendering.
  • Widget base class: Custom widgets must extend Filament’s base Widget class (or a subclass like CapellWidget). Simply registering an arbitrary class won’t work.
  • User overrides: End-users can toggle any registered widget in Settings → Dashboard. setEnabledWidgets() sets the initial state, not a lock.
  • Settings table: If the admin_settings table doesn’t exist yet (e.g., during fresh install), getWidgets() gracefully returns all built-in widgets to avoid bootstrap errors.

CapellAdmin::setEnabledWidgets([
WidgetEnum::SiteStatsOverviewWidget,
WidgetEnum::ListPagesWidget,
WidgetEnum::PageStatusWidget,
]);

Register a custom widget and enable it by default

Section titled “Register a custom widget and enable it by default”
// In AppServiceProvider::boot()
CapellAdmin::registerDashboardWidget(CustomSalesWidget::class, DashboardEnum::Main);
// Set up initial enabled state
$enabledWidgets = array_map(
fn (WidgetEnum $widget): string => $widget->value,
WidgetEnum::cases()
);
$enabledWidgets[] = CustomSalesWidget::class; // Add the new widget
CapellAdmin::setEnabledWidgets($enabledWidgets);