Skip to content

v4.4.3 - Multisite-aware dashboard widgets

Choose a tag to compare

@tallcms tallcms released this 30 Apr 07:46
· 31 commits to main since this release

Highlights

Adds an explicit dashboard scope picker so admin dashboard widgets honour a real, visible "current site" selection — fixing the bug where widgets always rendered the role-based fallback site (super_admin → default site, others → first owned site) regardless of which site the user thought they were on.

Backwards compatible: the picker seeds itself using the same fallback the widgets already resolve, so first dashboard load after upgrade matches what users were seeing before — they just now have a visible scope control they can change.

What's new

DashboardSitePicker widget

New TallCms\Cms\Filament\Widgets\DashboardSitePicker registered at the top of the admin dashboard. Renders a select with the user's accessible sites (super_admins also see "All Sites"). On selection it writes session('multisite_admin_site_id') and dispatches the dashboard.site-changed Livewire event so other widgets refresh in place — no full page reload.

Server-side authorization

Every selection (mount + updatedSelected) is validated against the current user's access via resolveAuthorizedSiteValue():

  • __all_sites__ is only valid for super_admins.
  • A numeric site_id must reference an active site that the user owns (super_admins can pick any active site).
  • Tampered Livewire input is rejected without dispatching the event.
  • Stale unauthorized session values (e.g. __all_sites__ left over from a previous super_admin login on a shared browser) are normalized on mount.

HasMultisiteWidgetContext trait

New shared trait at TallCms\Cms\Filament\Widgets\Concerns\HasMultisiteWidgetContext. Lifts the byte-for-byte duplicated getMultisiteSiteId() / getMultisiteName() helpers out of MenuOverviewWidget + ContentHealthWidget and adds isAllSitesSelected() for branching on the all-sites case. Both core and Pro widgets consume it.

Cms widgets refresh on scope change

MenuOverviewWidget and ContentHealthWidget now adopt the trait and add #[On('dashboard.site-changed')] listeners so they re-render when the picker fires.

Why a separate picker (not just wiring SiteSwitcher to write session)

SiteSwitcher::navigateToSite() (SiteSwitcher.php:47-51 comment) deliberately doesn't mutate session because authorization there flows through SitePolicy and content scopes through the Site resource's RelationManagers. Hijacking it would conflict with that design and ripple into every consumer of the same session key (ProSetting::resolveCurrentSiteId, PluginLicense::findForCurrentContext, redirect manager, etc.).

The picker keeps both concepts honest: SiteSwitcher = navigate; DashboardSitePicker = scope.

All Sites semantics

Widget All Sites behaviour
MenuOverviewWidget Show global counts (sum across all sites)
ContentHealthWidget Show global counts
AnalyticsOverviewWidget (Pro 1.10.0) Empty-state message — analytics is per-site
LicenseStatusWidget (Pro 1.10.0) Empty-state message — Pro licenses are per-site

The asymmetry is intentional: count(*) is a meaningful aggregate for cms widgets, but per-site provider data (Plausible/Fathom) and per-site Pro licenses don't aggregate cleanly without provider-side fan-out.

Testing

  • Trait unit suite: 11 assertions covering each branch of getMultisiteSiteId (specific id, all-sites sentinel, super_admin default-site fallback, regular-user first-owned fallback, QueryException safety net), getMultisiteName, and isAllSitesSelected.
  • Picker behaviour + authorization: 11 Livewire tests (default-site seeding, regular-user single-owned-site seeding, explicit selection + event dispatch, can_view truthiness, regular user cannot select another user's site / __all_sites__ / inactive site, mount normalizes stale foreign-user session value, mount rejects stale __all_sites__ for non-super-admin).
  • Pro widget render tests in standalone (10 tests, 20 assertions): All Sites empty state, specific-site rendering, setPeriod/refreshData short-circuiting on All Sites, event-driven data clearing, License heading and sentinel status.

Pull request

  • #78 — Add DashboardSitePicker so widgets honour an explicit site scope

Pro plugin

tallcms/pro 1.10.0 (separately released) ships site-aware Analytics + License widgets that consume the new trait, listen for dashboard.site-changed, render an empty-state message under __all_sites__, and honour the picker as the source of truth. Pins compatibility.tallcms: >=4.4.3.

Upgrading

No code changes required. The dashboard will gain a "Dashboard scope" picker at the top; first load picks up the same role-based fallback site the widgets were already showing, so existing users see exactly what they saw before — until they pick a different site.

Note on direction

This release makes the existing session-driven model honest, so super_admins can see and change the dashboard's site scope. It is not a final multisite analytics architecture — provider keys/secrets are still configured globally on the Pro Settings page. A follow-up will move per-site analytics configuration into the Site resource's edit page (alongside Branding, Embed Code, etc.) so analytics provider, site key, and secret are explicitly per-site.