lara-zeus/activity-timeline
A Laravel/Filament package from Lara Zeus that adds a clean activity timeline to your admin panel, making it easy to display, browse, and review model or user activity in chronological order. Simple to install and customize.
Prerequisite: this package is a renderer, not a recorder. It displays rows from spatie/laravel-activitylog. Adopt both or neither.
composer require lara-zeus/activity-timeline spatie/laravel-activitylog
php artisan vendor:publish --provider="Spatie\Activitylog\ActivitylogServiceProvider" --tag="activitylog-migrations"
php artisan migrate
Then, on the panel provider:
->plugins([
\LaraZeus\ActivityTimeline\ActivityTimelinePlugin::make(),
])
and rebuild panel assets:
php artisan filament:assets
Publish the plugin config/views only when you actually need to change markup or defaults:
php artisan vendor:publish --tag="activity-timeline-config"
php artisan vendor:publish --tag="activity-timeline-views"
Where to look first, in this order:
config/activity-timeline.php — density/compact defaults, date grouping, timestamp format.LogsActivity trait and getActivitylogOptions() on the model. If this is missing, the timeline renders empty and you will spend an hour debugging a UI problem that is a data problem.First use case — do this one only: tag your single most-opened resource (orders, tickets, accounts) with LogsActivity, then mount the timeline on that resource's View page. That exercises the record-scoped path you will use ~90% of the time, produces an immediate demo, and defers the harder decisions (panel-wide feeds, cross-tenant access policy) until you have seen real data in the widget.
Minimal model setup:
use Spatie\Activitylog\Traits\LogsActivity;
use Spatie\Activitylog\LogOptions;
class Order extends Model
{
use LogsActivity;
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults()
->logOnly(['status', 'total', 'assigned_to'])
->logOnlyDirty()
->dontLogEmptyChanges()
->useLogName('order');
}
}
logOnly() is not a convenience — it is your field-visibility boundary. See Gotchas.
Register the timeline on the resource's View page or as a dedicated tab. This is your bread-and-butter placement and should be the first thing you ship for every audited resource. Every downstream question — "who changed this?", "when did it move to shipped?" — resolves here.
public static function getWidgets(): array
{
return [
OrderTimeline::class,
];
}
If you need per-placement configuration (density, heading, subject scoping), the package exposes fluent setters on the timeline class or component; confirm exact method names against the README of the version you pin — the API surface is small but has shifted across releases.
The infolist embedding is the most under-used capability and the reason the package exists beyond "another Filament widget." Use it for entity overview pages that are not resources — an account dashboard, a settings page, a bespoke ops screen. You get a chronological feed without spinning up a full resource.
5.0.3 added compact. Treat it as a product lever:
| Placement | Density | Reason |
|---|---|---|
| Dedicated history tab | Full | Vertical space is free; scanability matters |
| Sidebar widget | Compact | Space-constrained; recency beats detail |
| Infolist entry / modal | Compact | Embedding context, not a destination |
| Relation manager | Full | Users are already in "investigate" mode |
Pick one per placement up front. Do not toggle on viewport width — that produces two bad experiences instead of one good one.
For "activity across all orders for this customer" or similar scoped feeds, mount the timeline against a query scoped by subject. Watch the query: a feed that spans many subjects is where N+1 bites.
Technically the same component. Politically a different product: it exposes cross-record and potentially cross-tenant activity. Gate it behind an explicit policy and a permission before you write the page. This is the one placement where "it works in staging" is not evidence it is safe.
LogsActivity + getActivitylogOptions() to the model, make a change, confirm a row lands in activity_log via tinker. Only then mount the widget.activity_log never prunes itself. Add a scheduled command with a configurable window (e.g. 180 days) and an index on (subject_type, subject_id, created_at) in the same PR that ships the timeline.useLogName('order') gives you a cheap way to scope feeds and later to query selectively. Decide a naming convention before you instrument the second model.The timeline is empty and the UI is fine. Nine times out of ten, the model is not logging, logOnly() excludes the attributes you changed, or logOnlyDirty() correctly suppressed a no-op save. Verify with Activity::all() in tinker before touching the view.
hidden() is not a privacy control. Activity log stores old/new attribute values verbatim. A field hidden from the form is still fully visible in the timeline if it is in logOnly(['*']). Audit every getActivitylogOptions() for password hashes, tokens, internal notes, PII, and margins. This is a product decision, not a refactor — route it to whoever owns data governance.
logOnly(['*']) is the tempting footgun. It logs the entire model snapshot on every change, which inflates the table, exposes every column, and makes the timeline unreadable. Enumerate fields. The friction is the point.
N+1 on any feed spanning multiple subjects. The renderer will touch causer and subject per row. On a record-scoped tab this is fine (one subject). On a relation manager or a global feed, eager-load both or watch the page crawl.
No pruning, ever. activity_log grows monotonically. Decide the retention window at adoption and implement it with a scheduled command, plus an index. compact does not help query cost; it only reduces vertical space.
Filament minor-version coupling. The plugin tracks Filament's release line (5.0.3 pins against 5.7.0), and Filament minors have historically moved view internals. Keep the panel and the plugin on the same minor. When you bump Filament, bump this in the same PR and re-run filament:assets — stale assets after an upgrade produce blank or unstyled timelines that look like a bug in the package.
Republishing views freezes your fork of the markup. Once you vendor:publish the views, upstream markup fixes no longer reach you. Publish only if you need it, and note in your upgrade checklist that published views must be re-diffed on each release.
Asset cache is the first debugging step. Blank timeline, unstyled timeline, or layout that ignores config: run php artisan filament:assets, then php artisan optimize:clear. Most "the package is broken" reports end here.
Global vs. record scope is a security boundary. A panel-wide dashboard built on the same component exposes other users' and other tenants' activity. Add a policy before the page, not after the first support ticket asking why a customer can see another customer's order history.
Extension points, in order of safety:
Continuity risk is real and should shape design. 10 stars, 0 dependents, single maintainer. Keep your usage wrapped in a thin local class or component (e.g. App\Filament\Components\OrderTimeline) rather than scattering the package's class names across resources. When the package stalls, replacing the renderer with a Blade partial is then a single-file change instead of a grep across the panel.
Not a compliance answer. A chronological feed over a mutable table is not an immutable audit trail. Fine for internal visibility and support triage; do not hand it to a questionnaire without review.
How can I help you explore Laravel packages today?