Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Filament Activity Timeline Laravel Package

bokshorn-it/filament-activity-timeline

A Filament plugin that turns spatie/laravel-activitylog into readable timelines: enum, date, boolean, and relationship diffs rendered through the subject model's own casts, plus a filterable activity resource, custom events, and German translations.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install the prerequisites first. This package is a presentation layer, not a logger. Nothing appears on screen until spatie/laravel-activitylog is actually recording rows.

composer require spatie/laravel-activitylog
php artisan vendor:publish --provider="Spatie\Activitylog\ActivitylogServiceProvider" --tag="activitylog-migrations"
php artisan migrate

composer require bokshorn-it/filament-activity-timeline

2. Make one model loggable. Start with a single, small model — a Post, Order, or Task — not your User model (see Gotchas). Use the trait and restrict what is logged:

use Spatie\Activitylog\Traits\LogsActivity;
use Spatie\Activitylog\LogOptions;

class Post extends Model
{
    use LogsActivity;

    public function getActivitylogOptions(): LogOptions
    {
        return LogOptions::defaults()
            ->logOnly(['title', 'status', 'author_id', 'due_date'])
            ->logOnlyDirty()
            ->dontLogEmptyChanges()
            ->useLogName('post');
    }
}

3. Register the panel plugin in your PanelProvider, then re-run php artisan filament:optimize-clear (or just refresh — resource/plugin caching bites people here).

->plugin(\BokshornIT\ActivityTimeline\ActivityTimelinePlugin::make())

Verify the exact namespace from the README — plugin class naming is the one thing worth double-checking before you paste.

4. Where to look first, in this order:

  • config/activitylog.php (Spatie's) — delete_records_older_than_days, default_log_name.
  • The package's src/ diff resolver — this is the piece that does the work. Read it before you file a bug about a value rendering oddly; nine times out of ten the answer is "your model is missing a cast".
  • The package's own config, if published, for label/causer resolution and date formatting.
  • The README screenshots — they show the target output, which is the fastest way to understand what "resolved" means here.

5. First use case worth shipping: add the timeline to the View page of your most-edited resource. Open a record, change one field, refresh — you should see Status: Draft → Published rather than status: 2. If you see raw integers, stop and fix your casts before going further; everything downstream depends on them.


Implementation Patterns

Two delivery surfaces, two different jobs

The package gives you (a) a record-scoped timeline for a single model and (b) a global, filterable activity resource. Treat them as serving different audiences:

Surface Audience Where it lives
Record timeline End users / support staff Relation manager or infolist on the View/Edit page
Activity resource Admins, auditors, you at 9am debugging prod Its own nav item, permission-gated

Don't give every panel user the global resource. It's a firehose and it leaks cross-record metadata (who changed what, when) that most roles shouldn't see.

Record timeline: relation manager vs. infolist

Prefer a relation manager when users should interact with entries (filter, paginate, click through). Prefer an infolist entry / custom view when you want the timeline visually embedded next to the record's own fields — it reads better but is less flexible.

Keep it on the View page, not the Edit page. On Edit, the timeline shifts under the user's cursor every time they save, which is disorienting.

Make logName do the labelling work

The timeline's group headings come from log_name. Set it deliberately per model (->useLogName('post'), 'invoice', 'membership') rather than letting everything land in default. This is the single cheapest readability win and requires no package configuration.

Lean on casts — that's the whole trick

The resolver runs values back through the subject model's casts and relationships. So the diff quality is a direct function of how well your model is typed:

protected function casts(): array
{
    return [
        'status'    => PostStatus::class,      // enum → renders as "Draft → Published"
        'due_date'  => 'date:Y-m-d',            // → "2026-08-14"
        'author_id' => 'integer',              // still an ID, but the relation resolves the label
        'is_pinned' => 'boolean',              // → "Yes / No"
    ];
}

public function author(): BelongsTo
{
    return $this->belongsTo(User::class, 'author_id');
}

Two hooks matter: an enum cast (or a Attribute cast with get()/set()) for state columns, and a defined relationship for every *_id you log. Define both and you get human labels for free; skip either and you get raw DB values.

Custom labels for causers and subjects

Spatie exposes tapActivity() / causer conventions, and Filament resolves names via name by default. If your users don't have a name column, add a getFilamentName() / Activity causer resolver rather than patching the views. For subjects, a subject morph label is worth setting so the global resource shows Invoice #1042 instead of App\Models\Invoice (42).

Scoping the global resource

Wire a policy and override the query, or you'll get an "Activity" nav item that shows every tenant's data:

public static function getEloquentQuery(): Builder
{
    return parent::getEloquentQuery()
        ->where('properties->tenant_id', filament()->getTenant()?->id)
        ->latest();
}

If you go the properties route, set those properties consistently at write time via a base model trait or tapActivity() — don't scatter withProperties() calls.

Retention and pruning

Spatie ships activitylog:clean. Schedule it and set delete_records_older_than_days in config. For a busy table, also index (subject_type, subject_id, created_at), causer_type/causer_id, and event in a follow-up migration — the default schema gets slow at seven figures.

Testing

Test that logging happens, not that the timeline renders. Assert Activity::count() / assertDatabaseHas('activity_log', ...) on the model action; treat the timeline as a view with a smoke test (livewire(ActivityResource::class)->assertOk()).


Gotchas and Tips

The User model trap. Adding LogsActivity to User means every login, password reset, and profile tweak writes a row — and the activity table now contains rows about the causer, which the causer resolver then chokes on recursively. Log User deliberately and with logOnly on a tiny set of fields, or not at all.

changes vs properties. Only the attribute-changes payload gets the enum/date/FK resolution treatment. Anything you stash via ->withProperties([...]) is rendered as-is. If a custom property looks ugly in the timeline, that's expected — either cast it before storing or accept it.

Silent fallback, not an error. When the resolver can't do better, it renders the raw value. No exception, no log entry. So "my enum shows 2" always means cast missing on the subject model, never "the plugin is broken". Confirm with:

dump(Post::find($id)->getCasts());          // is status cast?
dump(Post::find($id)->author?->name);      // does the relation resolve?

Deleted relations degrade gracefully — and permanently. If the author is hard-deleted, the diff for old entries can no longer resolve author_id to a name; you'll get a fallback or blank. If audit fidelity matters, use soft deletes on related models, or denormalise the display name into the log at write time.

Morph maps. If you use Relation::enforceMorphMap(), the subject_type in the DB is the alias. The resolver needs that alias registered — it usually is, but be suspicious if the subject label doesn't load after you introduce a morph map.

Timezone drift on dates. The date cast produces a Carbon; the rendered output depends on app.timezone and Filament's display settings. A due_date saved at 23:00 UTC can render as the next day for a CET user. Never store date fields as datetime unless they carry a time — use date casts for genuinely date-only values.

Don't log secrets. logAll() on a model with password, api_token, or remember_token writes them into properties in plaintext, and the timeline then displays them in a Filament UI that's probably readable by the admin you just onboarded. logOnly([...]) is not optional; treat it as a security boundary.

Bulk actions flood the log. A mass delete or bulk status change produces one activity row per model, all within the same second. The timeline becomes unreadable. Either chunk these deliberately or provide filtering by event and batch_uuid (Spatie sets it) so users can collapse them.

created and deleted events have no diff. They render as a single event line with the model's label, not a two-column diff. Don't expect a before/after there — and don't file it as a bug.

Caching after install. Filament caches discovered resources and plugins. After registering the plugin or publishing config, run php artisan filament:optimize-clear — otherwise you'll spend twenty minutes wondering why nothing changed.

Version pinning. With 7 stars and one README+changelog of maturity, read composer.json constraints against your Filament major (3 vs 4) before committing. This is the class of package where a Filament upgrade can break it silently, so pin the version and budget an afternoon when you move Filament.

N+1 in the diff resolver. Each diff row may query the subject + related models. On a timeline showing 50 entries, that's 50+ queries. Eager-load what you can, keep timeline page sizes modest (10–15), and prefer the record-scoped timeline over the global resource on hot pages.

Extend rather than fork. The resolver and the resource are the two natural extension points — override the resource in app/Filament/ and register yours instead if you need custom columns or filters, and use the published config/views to adjust label and date formatting. Given the project's maturity, prefer wrapping to patching; you want to be able to take a bugfix update without re-merging.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums