syriable / filament-activitylog
A package to integrate Spatie Activitylog with Filament.
Requires
- php: ^8.4
- filament/filament: ^4.10|^5.5
- filament/forms: ^4.10|^5.5
- filament/support: ^4.10|^5.5
- illuminate/contracts: ^12.0|^13.0
- spatie/laravel-activitylog: ^5.0
- spatie/laravel-package-tools: ^1.14.0
Requires (Dev)
- larastan/larastan: ^3.10
- laravel/pint: ^1.0
- nunomaduro/collision: ^8.0
- orchestra/testbench: ^10.0
- pestphp/pest: ^3.0
- phpstan/phpstan: ^2.2
- spatie/laravel-ray: ^1.26
This package is auto-updated.
Last update: 2026-06-07 16:23:36 UTC
README
Easily add beautiful timelines to your Filament apps — inside panels or stand-alone. Integrates with Spatie Activitylog.
Dark mode ready · Multilingual support · Filament v4 & v5
Table of contents
Features
- Infolist timeline component with many configuration options
- Searchable, compact, collapsible, and scrollable timelines
- Custom actions inside timeline items
ActivitylogTimelineActionfor pages and tables (slide-over modal)- Activity group support with nested or inline display via Spatie v5
groupproperties - Activities from relations and related models, with auto-linking to resources
- Global configuration of icons, colors, actions, descriptions, and more via
ActivitylogTimeline::configureUsing() - Beautiful design integrated with Filament
- Works outside the admin panel in infolists and Livewire components
- Dark mode support
- Fully translatable (English, French, Italian, Dutch included)
- Works out-of-the-box with Spatie model events
Requirements
- PHP 8.4+
- Laravel 12 or 13
- Filament 4.10+ or 5.5+
- Spatie Laravel Activitylog 5.x
You should already have Spatie Activitylog installed and configured before using this package. If you have existing activity data, it will work immediately.
Installation
Install via Composer:
composer require syriable/filament-activitylog
The service provider registers automatically via Laravel package discovery.
Custom theme (required)
For each Filament panel that uses the timeline, create a custom theme and add the following line to your theme.css:
@source '../../../../vendor/syriable/filament-activitylog/resources/**/*.blade.php';
If you use the timeline outside a panel, include the same @source line in the CSS file used by the Livewire components where the timeline is rendered.
Register the plugin
Register the plugin on each panel where you use the timeline:
use Syriable\Filament\Plugins\Activitylog\Activitylog; $panel ->plugin(Activitylog::make());
Publish translations (optional)
php artisan vendor:publish --tag="filament-activitylog-translations"
Usage
Spatie Activitylog supports two main use cases:
- Automatically log model events on Eloquent models.
- Log custom events and associate them with Eloquent models.
Logging model events
See the Spatie documentation on logging model events. Enable model event logging first, then use the timeline to display them.
Custom events
Terminology
Each activity has four main properties:
- Subject — the Eloquent model the event was logged on.
- Causer — the model (usually a user) that caused the event, or
null. - Event — the programmatic event name (
created,updated,deleted,restored, or custom events likepublished). - Description — usually the same as the event name, but can be a custom human-readable string.
Logging custom events
Always include an event name. For example, logging a published event on a Post:
$post = Post::find(1); $johnDoe = User::firstWhere('email', 'johndoe@example.com'); $post->touch('published_at'); activity() ->performedOn($post) ->causedBy($johnDoe) ->event('published') ->log('published');
By default, the timeline displays:
John Doe published the post.
Displaying the causer name
The causer name is inferred automatically:
- If the causer implements
Filament\Models\Contracts\HasName,getFilamentName()is used. - Otherwise, the
nameattribute is used. - Otherwise,
first_nameand/orlast_nameare used. - Otherwise, no causer name is included ("The post was published.").
Override with causerName() or causerNames():
use Syriable\Filament\Plugins\Activitylog\Filament\Infolists\Components\ActivitylogTimeline; ActivitylogTimeline::make() ->causerName(User::class, fn (User $causer) => $causer->full_name) ->causerNames([ User::class => fn (User $causer) => $causer->full_name, ])
Fallback causer name when causer_type / causer_id are null:
ActivitylogTimeline::make() ->causerName(null, 'System')
Causer URL:
ActivitylogTimeline::make() ->causerUrl(function (User $causer) { return UserResource::getUrl('edit', ['record' => $causer]); })
Custom descriptions
If the description passed to ->log() differs from the event name, it is stored as-is (not translatable). For translatable custom descriptions, use eventDescription() on the timeline component instead.
You can use inline markdown for bold or italic formatting in stored descriptions.
Grouping related activities
Spatie Activitylog v5 removed LogBatch and Activity::forBatch(). Use the package ActivityBatch helper to group related activities under a shared group property:
use Syriable\Filament\Plugins\Activitylog\Support\ActivityBatch; use Spatie\Activitylog\Models\Activity; ActivityBatch::withinBatch(function () use ($post) { activity() ->performedOn($post) ->event('published') ->log('published'); $post->tweet()->create(['content' => 'Check out my new blog post']); activity() ->performedOn($post) ->event('tweeted') ->log('tweeted'); }); $groupId = ActivityBatch::getBatchUuid(Activity::query()->latest('id')->first()); ActivityBatch::scopeForBatch(Activity::query(), $groupId)->get();
You can also assign the property manually with ->withProperty(ActivityBatch::PROPERTY_KEY, $groupId) when logging.
See the Spatie v5 upgrading guide for details.
Timeline
Use the timeline infolist entry in forms and infolists. In Filament v4+, infolist components can be used inside forms.
use Syriable\Filament\Plugins\Activitylog\Filament\Infolists\Components\ActivitylogTimeline; ActivitylogTimeline::make() ->label('History')
By default, the timeline hides itself on create pages (no record yet). Pass ->visible(true) to override.
When empty, a customizable empty state is shown.
Relation support
Include activities from related models with withRelations():
use Filament\Support\Icons\Heroicon; ActivitylogTimeline::make() ->withRelations(['addresses']) ->itemIcon('*', Heroicon::OutlinedHomeModern, [Address::class])
Related model activities produce descriptions like "John Doe added related address Main St. 123, NYC."
If a related model is no longer linked to the current record, its historical activities disappear from that record's timeline.
Record title attribute:
ActivitylogTimeline::make() ->recordTitleAttribute(Post::class, 'id') ->recordTitleAttributes([ Post::class => 'id', Address::class => 'display_summary', ]) ->getRecordTitleUsing(Post::class, fn (Post $post) => (string) $post->id) ->getRecordTitleUsing(Address::class, fn (Address $address) => "{$address->city}, {$address->state}")
Record URL:
ActivitylogTimeline::make() ->recordUrl(Post::class, fn (Post $post) => route('frontend.posts.show', ['post' => $post])) // Disable linking: ActivitylogTimeline::make() ->recordUrl(Post::class, fn (Post $post) => null)
Limit related results:
ActivitylogTimeline::make() ->withRelations([ 'addresses' => function (HasMany $relation) { return $relation->where('type', AddressType::Shipping); }, ])
Customizing timeline items
Icons and colors:
use Filament\Support\Icons\Heroicon; use Spatie\Activitylog\Models\Activity; ActivitylogTimeline::make() ->itemIcon('created', Heroicon::OutlinedPlusCircle) ->itemIcon('deleted', Heroicon::OutlinedTrash) ->itemIcons([ 'created' => fn (Activity $activity) => Heroicon::OutlinedPlus, 'deleted' => Heroicon::OutlinedTrash, ]) ->itemIconColor('created', 'info') ->itemIconColor('deleted', 'danger') ->itemIconColors([ 'created' => 'info', 'deleted' => 'danger', ])
Scope to specific models:
ActivitylogTimeline::make() ->itemIcon('created', Heroicon::OutlinedPlusCircle, [Post::class]) ->itemIconColor('created', 'success', Post::class)
Badges:
use Filament\Support\Colors\Color; ActivitylogTimeline::make() ->itemBadge('email_sent', fn (Activity $activity) => Status::tryFrom($activity->getProperty('status'))?->getLabel()) ->itemBadgeColor('email_sent', fn (Activity $activity) => match (Status::tryFrom($activity->getProperty('status'))) { Status::Bounce => 'danger', Status::Delivery => 'success', default => 'gray', })
Time format and timezone:
ActivitylogTimeline::make() ->itemDateTimeFormat('Y-m-d H:i') ->itemDateTimeTimezone(fn () => auth()->user()->timezone)
Item actions:
use Filament\Actions; use Filament\Forms; use Filament\Support\Icons\Heroicon; ActivitylogTimeline::make() ->itemActions('published', [ Actions\Action::make('view_url_in_search_console') ->label('Open Google Search Console') ->icon(Heroicon::MagnifyingGlass) ->url(fn (Post $post) => 'https://search.google.com/...'), Actions\Action::make('create_tweet') ->label('Send Tweet') ->icon(Heroicon::Megaphone) ->form([ Forms\Components\Textarea::make('content') ->maxLength(280) ->required(), ]) ->action(function (Post $post, array $data) { // ... }), ])
Access the underlying activity via $arguments['activity_id'] when needed.
Global defaults:
use Filament\Support\Icons\Heroicon; ActivitylogTimeline::configureUsing(function (ActivitylogTimeline $timeline) { return $timeline ->itemIcons([ 'created' => Heroicon::OutlinedPlusCircle, 'deleted' => Heroicon::OutlinedTrash, ]) ->itemIcon('created', Heroicon::OutlinedPencil, [Post::class]) ->itemIconColors([ 'created' => 'info', 'deleted' => 'danger', ]) ->itemActions( event: 'published', actions: [/* ... */], subjectScopes: [Post::class], ); });
Per-timeline configuration overrides global defaults.
Descriptions
Override generated descriptions per event:
ActivitylogTimeline::make() ->eventDescription('published', 'Post is now shared with the world 🌎', [Post::class]) ->eventDescriptions( descriptions: [ 'created' => fn (Activity $activity) => "{$activity->causer->full_name} started a draft post", 'published' => 'Post is now shared with the world 🌎', ], subjectScopes: [Post::class], ) ->modifyEventDescriptionUsing(function (string $eventDescription, Activity $activity, string $recordTitle, ?string $causerName, ?string $changesSummary) { return "{$eventDescription} for {$recordTitle}"; })
Formatting attribute values
For updated events, changed attributes appear in the description automatically. Strings, booleans, arrays, enums, and dates are formatted for readability.
Custom casts — add casted attributes to useAttributeRawValues() in getActivitylogOptions():
public function getActivitylogOptions(): LogOptions { return LogOptions::defaults() ->useAttributeRawValues(['amount']) ->logAll(); }
Format by attribute:
ActivitylogTimeline::make() ->attributeLabel('amount', 'purchase amount') ->attributeValue('amount', fn (?Money $value) => $value?->formatWithoutZeroes()) ->attributeValues([ 'amount' => fn (?Money $value) => $value?->formatWithoutZeroes(), ])
Format by cast class:
ActivitylogTimeline::make() ->attributeCast(MoneyCast::class, fn (?Money $value) => $value?->formatWithoutZeroes())
Hide old attribute values:
ActivitylogTimeline::make() ->changesSummaryOldAttributeValues(false)
Hide attribute values:
ActivitylogTimeline::make() ->changesSummaryAttributeValues(false)
Attribute labels:
ActivitylogTimeline::make() ->attributeLabel('some_date', 'published at') ->attributeLabels([ 'some_boolean' => 'is hidden from SEO', ])
Labels are also resolved from form/infolist components on the same page when available.
Model labels:
ActivitylogTimeline::make() ->modelLabel(User::class, 'Benutzer') ->modelLabels([ Post::class => 'Beitrag', ])
Activity groups
Activities that share the same group property (via ActivityBatch) display as a nested sub-timeline by default. Display group items inline with the main timeline:
ActivitylogTimeline::make() ->inlineGroups()
Customize the group query:
ActivitylogTimeline::make() ->modifyGroupActivitiesQueryUsing(function (Builder $query, Activity $activity, string $groupKey) { return $query->whereIn('event', ['deleted', 'restored']); })
Override the group query entirely:
use Syriable\Filament\Plugins\Activitylog\Support\ActivityBatch; ActivitylogTimeline::make() ->getGroupActivitiesUsing(function (Activity $activity, string $groupKey) { return ActivityBatch::scopeForBatch(Activity::query(), $groupKey) ->where('id', '!=', $activity->id) ->get(); })
Empty state
use Filament\Support\Icons\Heroicon; ActivitylogTimeline::make() ->emptyStateHeading('No history') ->emptyStateDescription('Nothing has happened to this model yet.') ->emptyStateIcon(Heroicon::OutlinedBarsArrowDown)
Compact timeline
ActivitylogTimeline::make() ->compact()
Globally:
ActivitylogTimeline::configureUsing(fn (ActivitylogTimeline $timeline) => $timeline->compact());
On compact timelines, outline heroicons are automatically converted to mini variants (and vice versa). Disable with:
ActivitylogTimeline::make() ->convertHeroicons(false)
Searchable timelines
ActivitylogTimeline::make() ->searchable()
Search runs in the browser against activity descriptions.
Collapsible timelines
Show only the most recent activities by default, with a toggle to reveal the rest:
ActivitylogTimeline::make() ->collapsible() ->collapsedVisibleCount(5)
Maximum height
ActivitylogTimeline::make() ->maxHeight() // default 500px ->maxHeight(800) ->maxHeight('50vh')
Sort order
ActivitylogTimeline::make() ->sortActivitiesDescending(false) // oldest first
Custom activities query
ActivitylogTimeline::make() ->modifyActivitiesQueryUsing(function (Builder $query) { return $query->where('log_name', 'notifications'); })
Override entirely:
ActivitylogTimeline::make() ->getActivitiesUsing(function () { return Activity::query()->get(); })
Prefer modifyActivitiesQueryUsing() when possible.
Activities limit
For models with long histories, cap how many activities are loaded into the timeline:
ActivitylogTimeline::make() ->activitiesLimit(50)
The limit is applied after sorting and group deduplication. Combine with modifyActivitiesQueryUsing() for database-level filtering when possible.
Hiding the label
ActivitylogTimeline::make() ->hiddenLabel()
Timeline action
Use ActivitylogTimelineAction on pages and in tables to open the timeline in a slide-over:
use Syriable\Filament\Plugins\Activitylog\Filament\Actions\ActivitylogTimelineAction; class EditPost extends EditRecord { protected function getHeaderActions(): array { return [ ActivitylogTimelineAction::make(), ]; } }
$table ->recordActions([ ActivitylogTimelineAction::make(), ]);
Customize the embedded timeline:
ActivitylogTimelineAction::make() ->modifyActivitylogTimelineUsing(function (ActivitylogTimeline $timeline) { return $timeline->compact()->searchable(); })
Translations
Built-in languages: English, French, Italian, Dutch.
Publish and customize:
php artisan vendor:publish --tag="filament-activitylog-translations"
Troubleshooting
Timeline styles are missing
Add the package @source line to your Filament custom theme CSS (see Custom theme).
No activities appear
Confirm Spatie Activitylog is installed, the model uses the LogsActivity trait, and activities exist for the record. Use modifyActivitiesQueryUsing() to debug the underlying query.
Grouped activities do not collapse
Spatie v5 uses a group property on activities, not a batch_uuid column. Use ActivityBatch::withinBatch() or set the group property manually when logging.
Performance on large histories
Use activitiesLimit() or modifyActivitiesQueryUsing() to avoid loading thousands of rows into memory.
Testing
composer test
Changelog
Please see CHANGELOG for more information on what has changed recently.
License
This package is open-sourced software licensed under the MIT license.