Getting started
Usage
Timeline
The timeline form component can be used in your forms and infolists to display a timeline of the activities related to the current record. The timeline will automatically be filled with the activities that happened to the current Eloquent model:
use RalphJSmit\Filament\Activitylog\Filament\Infolists\Components\Timeline;
Timeline::make()
->label('History'),
As in Filament V4 you are able to mix form and infolist components together, you can use this infolist component also inside your forms.
In general, you don't have data yet on pages where you create an item. Therefore, by default, the timeline will hide itself on pages where $operation === 'create', so you don't need to manually hide the timeline on create pages. If you still want to show it, just pass ->visible(true) to the component.
If the timeline is empty (meaning that there are no activities yet displayed), then the timeline will display an empty state, which you can also customize (see below).
Customizing timeline items
Icons & colors
By default, each item in the timeline will be a small, gray dot. You can customize the icon and color of each item using the ->itemIcon() and ->itemIconColor() methods.
use Filament\Support\Icons\Heroicon;
use Spatie\Activitylog\Models\Activity;
Timeline::make()
// You can let the package inject both the actual `Activity` Eloquent model, or just the string/nullable event name:
->itemIcon('created', Heroicon::OutlinedPlusCircle)
->itemIcon('deleted', Heroicon::OutlinedTrash)
->itemIcons([
'created' => fn (Activity $activity) => $activity->causer->isAdmin() ? Heroicon::OutlinedPlus : Heroicon::OutlinedPlusSmall,
'deleted' => Heroicon::OutlinedTrash,
])
->itemIconColor('created', 'info')
->itemIconColor('deleted', 'danger')
->itemIconColors([
'created' => 'info',
'deleted' => 'danger',
]),
Tip: generally, in a single timeline, it is nice to use a combination of both items with and without an icon. For example, you could consider giving "major" activity events an icon, like 'created' or 'deleted', whereas you could leave other events (like 'updated') without an icon.
Instead of strings, you can also pass custom closures. You can let the package inject both the actual Activity Eloquent model, or just the string/nullable event name:
use Spatie\Activitylog\Models\Activity;
Timeline::make()
->itemIcon('created', fn (string $event, Activity $activity) => /** */)
->itemIconColor('created', fn (string $event, Activity $activity) => /** */)
You can also scope the icons & colors to specific Eloquent models. This can be useful in cases where you have a timeline with activities for multiple types of Eloquent models (e.g. when working with batches), and you only want to apply a certain icon or color to one or more particular models.
use Filament\Support\Icons\Heroicon;
Timeline::make()
->itemIcon('created', Heroicon::OutlinedPlusCircle, [Post::class]) // Either array of classes or a single class.
->itemIconColor('created', 'success', Post::class)
Badges
If you want, you can add a badge after the event description. You can provide a badge based on the event name using the ->itemBadge() method. A badge color can be provided using the ->itemBadgeColor() method:
use Filament\Support\Colors\Color;
Timeline::make()
->itemBadge('email_sent', function (Activity $activity) {
$status = Status::tryFrom($activity->getExtraProperty('status'));
return $status->getLabel();
})
->itemBadges([
'email_sent' => fn () => ...,
'created' => 'New',
])
->itemBadgeColor('email_sent', function (Activity $activity) {
$status = Status::tryFrom($activity->getExtraProperty('status'));
return match ($status) {
Status::Bounce => 'danger',
Status::Complaint => 'danger',
Status::Reject => Color::Orange,
Status::RenderingFailure => Color::Orange,
Status::Send => Color::Cyan,
Status::DeliveryDelay => Color::Sky,
Status::Delivery => Color::Blue,
Status::Open => Color::Violet,
Status::Click => Color::Purple,
default => 'gray',
};
})
->itemBadgeColors([
'email_sent' => fn () => ...,
'created' => 'info',
])
You can also scope the badges for events to specific Eloquent models. This can be useful in cases where you have a timeline with activities for multiple types of Eloquent models (e.g. when working with batches), and you only want to apply a certain icon or color to one or more particular models.
Timeline::make()
->itemBadge('created', 'New', [Post::class]) // Either array of classes or a single class.
->itemBadgeColor('created', 'success', Post::class)
Actions
You can easily add actions to the timeline using the ->itemActions() method. This method allows you to specify the event name and provide an array of actions that will be displayed beneath each item in the timeline.
The below example shows how to add actions for the 'published' event on a Post model:
use Filament\Actions;
use Filament\Forms;
use Filament\Support\Icons\Heroicon;
Timeline::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)
->hidden(fn (Post $post) => $post->tweeted_at !== null)
->form([
Forms\Components\Textarea::make('content')
->label('Tweet')
->helperText('Max. 280 characters')
->maxLength(280)
->required()
])
->action(function (Post $post, array $data) {
// Send tweet...
$post->touch('tweeted_at');
// Of course, we will also log a new activity to the timeline :)
activity()
->performedOn($post)
->causedBy(auth()->user())
->event('tweeted')
->log('tweeted');
Notification::make()
->title('Tweet sent')
->body('Let\'s hope our followers like it as much as we do 🙏')
->success()
->send();
})
])
]),
If you are mixing timeline items for multiple Eloquent models and you need access to the current underlying Spatie Activitylog model, you can use the $arguments['activity_id'] parameter:
Timeline::make()
->itemActions('published', [
Actions\Action::make('edit')
->fillForm(function (array $arguments) {
$activity = Activity::find($arguments['activity_id']);
// ...
})
->form([
Forms\Components\TextInput::make('title')
])
->action(function (array $arguments, array $data) {
$activity = Activity::find($arguments['activity_id']);
// ...
})
]),
Define icons, colors & actions globally
If you want to globally provide icons, colors and actions, you can make use of the Filament configureUsing() method. When working globally with timelines, it is generally very handy to scope the configuration to specific models:
use Filament\Support\Icons\Heroicon;
Timeline::configureUsing(function (Timeline $timeline) {
return $timeline
->itemIcons([
// Applies to all Eloquent models...
'created' => Heroicon::OutlinedPlusCircle,
'deleted' => Heroicon::OutlinedTrash,
])
->itemIcon('created', Heroicon::OutlinedPencil, [Post::class, GlossaryItem::class]) // Applies only to `Post` and `GlossaryItem` model.
->itemIcon('created', null, User::class) // Disables icon for the `User` model.
->itemIconColors([
'created' => 'info',
'deleted' => 'danger',
])
->itemActions(
event: 'published',
actions: [
// ...
],
subjectScopes: [Post::class],
)
});
These globals are kept as defaults. If you provide a custom override on any individual timeline, then that value will override the global defaults.
Descriptions
By default, the Filament Activitylog package will generate a nice description for you based on the event name and whether or not there's a causer name (see above). If you want to override the description for individual events, you can do so via the ->eventDescription() method:
Timeline::make()
->eventDescription('published', 'Post is now shared with the world 🌎') // Instead of: John Doe published the post.
You can scope the custom descriptions also to specific Eloquent models:
use App\Models\Post;
Timeline::make()
->eventDescription('published', 'Post is now shared with the world 🌎', [Post::class]) // Apply description only to `Post` models and not to `Tweet` models.
You can also set descriptions in bulk:
use Spatie\Activitylog\Models\Activity;
Timeline::make()
->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]
)
If you want to still make use of the description generation service provided by the package, but still modify part of the description, you can use the ->modifyEventDescriptionUsing() method:
use Spatie\Activitylog\Models\Activity;
Timeline::make()
->modifyEventDescriptionUsing(function (string $eventDescription, Activity $activity, string $recordTitle, ?string $causerName, ?string $changesSummary) {
return "{$eventDescription} for {$recordTitle}";
])
Formatting attribute values
If you have enabled the logging of changed attributes, the timeline will automatically display changed attributes in the description for the updated event. For example, consider the following changed attributes:
[
'title' => 'New title',
'some_boolean' => true,
'some_date' => Carbon::parse('2024-01-01 10:00'),
]
The package will automatically generate a description like this:
[Causer name] updated title to New title, some boolean to true and some date to 2024-01-01 10:00.
As you can see, the package automatically formats the values of the attributes in a human-readable way. Strings are kept, booleans are converted to 'true'/'false' in text, arrays are converted to JSON, enums are converted to either their name or to the getLabel() method if you implemented the Filament\Support\Contracts\HasLabel interface and dates are converted to a better format.
Displaying the causer name
The name of the causer is automatically inferred from the causer model using following steps:
- If the causer model implements the
Filament\Models\Contracts\HasNamecontract, then thegetFilamentName()method will be used. - If the causer model has an attribute called
name, then this attribute will be used. - If the causer model has an attribute called
first_nameand/orlast_name, then these attributes will be used. - Otherwise, no name for the causer will be included ("The post was published.").
Alternatively, if you want to override the above logic, you can use the ->causerName() and ->causerNames() methods:
Timeline::make()
->causerName(User::class, fn (User $causer) => ...)
->causerNames([
User::class => fn (User $causer) => ...,
])
Displaying a fallback causer name
If you want to display a fallback causer name when the causer_type/causer_id columns are null, you can pass null as the first argument to the ->causerName() method. This will allow you to use "System" or "Application" as fallback causer name for actions not attached to a user:
Timeline::make()
->causerName(null, 'System')
Adding a causer name url
You can make the causer name a link by providing a closure to the ->causerUrl() method. If your system has multiple types of causers, make sure to handle each type in the closure:
Timeline::make()
->causerUrl(function (User $causer) {
return UserResource::getUrl('edit', ['record' => $causer]);
})
Time format & time zone
You can customize the time format used in the timestamp tooltip by using the ->itemDateTimeFormat() method. The default format is M jS, Y H:i:s. You can also specify a timezone to convert the timestamps to using ->itemDateTimeTimezone().
Timeline::make()
->itemDateTimeFormat('Y-m-d H:i')
->itemDateTimeTimezone(fn () => auth()->user()->timezone)
These are also great candidates for global configuration (see Define icons, colors & actions globally).
Using custom casts
If you have custom casts on your Eloquent, then the Spatie Activitylog will automatically log updates to casted attributes. However, the way these values are stored in the database or displayed might not always work immediately satisfactory out-of-the-box.
First, in order to make the cast formatting work better, make sure that all attributes with custom casts are in the useAttributeRawValues() method in the getActivityLogOptions():
class Order extends Model
{
use LogsActivity;
protected $casts = [
'status' => SomeEnum::class, // Simple enum casts or other native Laravel casts will work right away.
'amount' => MoneyCast::class, // Custom casts should be added in the `useAttributeRawValues()` array.
];
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults()
->useAttributeRawValues(['amount'])
->logAll();
}
}
Explanation: for example, consider that you have an arrayable
Moneyobject as a cast on your Eloquent model. By default, Spatie Activitylog will store the value from the arrayable Money object as a change (so an array as JSON). However, it could be that the database actually contains just an integer value and not a JSON array. Therefore, it is recommended to store custom casts as raw values in the database.
Formatting by attribute
Next, the above should work to automatically cast model values to the correct types. Next, you can provide custom callbacks to format specific attributes in the timeline before they appear:
Timeline::make()
->attributeLabel('amount', 'purchase amount')
->attributeValue('amount', fn (?Money $value) => $value?->formatWithoutZeroes())
// Make sure to also account for `null` values...
You can also use the attributeValues() to provide an array of formatting callbacks:
Timeline::make()
->attributeValues([
'amount' => fn (?Money $value) => $value?->formatWithoutZeroes(),
// ...
]),
These methods can also be scoped to specific Eloquent models if you prefer, similar to the other methods in the package. This can be handy when configuring these callbacks globally.
Formatting by cast class
Alternatively, it can also be handy sometimes to provide a formatting callback for a specific cast class. For example, if you have a Money object in multiple models on different attribute names. You can use the attributeCast() method to provide a formatting calback that will receive the value:
Timeline::make()
->attributeCast(MoneyCast::class, fn (?Money $value) => $value?->formatWithoutZeroes()),
This method can come in handy when globally configuring the timeline:
Timeline::configureUsing(function (Timeline $timeline) {
return $timeline->attributeCast(MoneyCast::class, fn (?Money $value) => $value?->formatWithoutZeroes());
})
Again, you may also use the attributeCasts() method to provide formatting callbacks using an array:
Timeline::make()
->attributeCasts([
MoneyCast::class => fn (?Money $value) => $value?->formatWithoutZeroes()),
CurrencyCast::class => fn (?Currency $value) => $value?->code()),
]),
Hiding old attribute values
By default, the descriptions for an updated event display both the old values and the new values that attributes were changed to. In case you would like to hide the old values in the description, you can pass false to the ->changesSummaryOldAttributeValues() method:
Timeline::make()
->changesSummaryOldAttributeValues(false)
- Before: John Doe updated title from Old title to New title and slug from old-slug to new-slug.
- After: John Doe updated title to New title and slug to new-slug.
Hiding attribute values
Sometimes the changed attribute values can be very long or you have a preference not to include the new values that attributes were updated to in the description. In that case, use the ->changesSummaryAttributeValues() method to hide attribute values in the generated descriptions and changes summary:
Timeline::make()
->changesSummaryAttributeValues(false)
- Before: John Doe updated title to New title and slug to new-slug.
- After: John Doe updated title and slug.
Formatting attribute labels
For getting the human-readable names of the attributes, the package will first automatically and intelligently check whether there is any other form component on the page that has a custom label for this attribute. For example, if you have a DatePicker::make('some_date')->label('Published at'), then the package will automatically use the label 'published at'.
You can also provide custom labels for attributes by using the ->attributeLabel() or ->attributeLabels() method:
Timeline::make()
->attributeLabel('some_date', 'published at')
->attributeLabel('some_date', fn () => 'published at')
->attributeLabels([
'some_boolean' => 'is hidden from SEO',
]),
Finally, if there is still no attribute label found, the package will automatically try to convert the attribute name to a human-readable label. For example, some_date will be converted to some date.
Customizing model labels
By default, when creating descriptions the package will automatically generate model labels based on the Eloquent model. If you use the timeline in a panel, it will also first check if there's an associated resource. If yes, then the ResourceName::getModelLabel() method is used as the model label.
The resulting model label is then lower-cased and used inside the automatically generated descriptions. If you want to provide a custom model label, or if the lower-casing doesn't work for you (e.g. in languages like German), then you can use the ->modelLabel() and ->modelLabels() method:
Timeline::make()
->modelLabel(User::class, 'Benutzer')
->modelLabels([
Post::class => 'Beitrag',
]),
Activity batches
By default, if the package detects that an event belongs to a batch, it will display the batch in a compact, inline timeline. The inline timeline will be 'connected' to the "main" timeline. This looks nice and can be very useful to your users to view related events together.
If you don't have the room for an inline timeline, you can also opt to display the batch inline. This means that the activity items from the batch will be shown inline with the main timeline items, just as if the items from the batch was part of the main timeline.
Timeline::make()
->inlineBatches(),
Customizing query to get batches
By default, the package will automatically retrieve all activity items from the batch that have a subject. You can however modify the query used to retrieve these items using the ->modifyBatchActivitiesQueryUsing() using:
Timeline::make()
->modifyBatchActivitiesQueryUsing(function (Builder $query, Activity $activity, string $batchUuid) {
return $query->whereIn('event', ['deleted', 'restored']);
})
Overriding query to get batches
You can also choose to override the query entirely:
Timeline::make()
->getBatchActivitiesUsing(function (Activity $activity, string $batchUuid) {
return Activity::forBatch($batchUuid)->get();
})
Customizing empty state
When the timeline is empty, it will show an empty state (similar to how the empty state looks in Filament tables). You can customize the empty state using the following methods:
use Filament\Support\Icons\Heroicon;
Timeline::make()
->emptyStateHeading('No history')
->emptyStateDescription('Nothing has happened to this Eloquent model.')
->emptyStateIcon(Heroicon::OutlinedBarsArrowDown),
Compact timeline
By default, the timeline has a very nice design, but with a larger number of activities it can take up a lot of space. To solve this, you can opt to make use of the ->compact() option, which will provide a more compact design:
Timeline::make()
->compact(),
If you want to make all timelines compact by default, you can configure that globally:
Timeline::configureUsing(fn (Timeline $timeline) => $timeline->compact());
Heroicon Mini icons
If you use a compact timeline, it generally looks better to use icons from the heroicons-m- set instead of the heroicons-o- set, because the Heroicon Mini versions are designed for smaller sizes. On compact timelines, the package will automatically convert icons from the heroicons-o- set to the heroicons-m- set, and vice-versa on non-compact timelines.
If you want to disable this, you can do so passing false to the ->convertHeroicons() method:
Timeline::make()
->convertHeroicons(false),
Timeline::configureUsing(fn (Timeline $timeline) => $timeline->convertHeroicons(false));
Searchable timelines
If you have many activities in your timeline, you might want to make the timeline searchable:
Timeline::make()
->searchable(),
This will display a small text input above the timeline. Whenever a user types a search query in the input, it will look through the descriptions of all activities using JavaScript, and only display the activities that match the search query.
Maximum height
If you have a timeline with many activities, you might want to define a maximum height for the timeline. This will make the timeline scrollable:
Timeline::make()
->maxHeight() // Default 500px
->maxHeight(800) // 800 px
->maxHeight('50vh') // Any CSS value
Sorting the activities ascending
By default, the activities are sorted descending (latest first). If you want to sort the activities ascending (oldest first), you can use the ->sortActivitiesDescending(false) method:
Timeline::make()
->sortActivitiesDescending(false)
Getting custom activities
By default, the timeline will assume that your current Eloquent model has the LogsActivity trait. In that case, your model has an ->activities() relationship and the timeline assumes that you want to display these activities. If you want to filter the activities being shown, you can use the ->modifyActivitiesQueryUsing() method:
Timeline::make()
->modifyActivitiesQueryUsing(function (Builder $query) {
return $query->where('log_name', 'notifications');
}),
If you want override the activities logic entirely for determining which activities to display, you can provide a custom callback to the ->getActivitiesUsing() method available on all the components:
Timeline::make()
->getActivitiesUsing(function () {
return Activity::get(); // Provide the activities as you wish.
}),
It would be recommended to first try the
->modifyActivitiesQueryUsing()if possible and only then use the->getActivitiesUsing()method.
Relation support
By default, the timeline component will display only the activities relating to the current Eloquent model in the form. If you want to also display activities for related Eloquent models, you may use the ->withRelations() method. This method will automatically include activities for related models that are linked to the current model.
For example, say that we are editing a Customer model, and each customer can have one or more related Address'es using a hasMany ->addresses() relationship. We can now display the creation and changes of the related address models also in the timeline:
use Filament\Support\Icons\Heroicon;
Timeline::make()
->withRelations(['addresses'])
// Display all address activities with a home icon, regardless of the type of event.
->itemIcon('*', Heroicon::OutlinedHomeModern, [Address::class])
If the timeline detects that an item is for a related model, it will automatically update the text used like "John Doe added related address Main St. 123, NYC." or "John Doe updated related address Main St. 123, NYC street to Main St..".
NB: currently there is one caveat to be aware of. If a related model (like an address) is at any point not currently related to the current Eloquent model anymore, the previous activities would disappear from the timeline. So if an address is transferred to another customer, the activities for that address would disappear entirely from the timeline of the original customer.
Identifying related models by title attribute
By default, the plugin will try to identify the related model by a title attribute. First, it is checked if the model implements the Filament\Support\Contracts\HasName or Filament\Support\Contracts\HasLabel interfaces. If yes, then these will be next. Next, it will be checked if the model has the attributes name, title or label. If yes, then these will be used. If still no result found, then the message will be generic ("John Doe updated a related post" instead of "John Doe updated related post Current Post Title").
You can provide custom attributes to use as title using the following methods:
Timeline::make()
->recordTitleAttribute(Post::class, 'id')
->recordTitleAttributes([
Post::class => 'id',
Address::class => 'display_summary',
])
->getRecordTitleUsing(Post::class, fn (Post $post) => $post->id)
->getRecordTitleUsing(Address::class, fn (Address $address) => "{$address->city}, {$address->state}")
])
Customising URLs to related record
By default, if the Activitylog detects a related record, it will check if there is an associated resource in the current panel for that model. If yes, then it will automatically insert a link to viewing the related record in the description. If you want, you can customise the URL creation logic using the ->recordUrl() method, for example, to link to your frontend page instead:
Timeline::make()
->recordUrl(Post::class, fn (Post $post) => route('frontend.posts.show', ['post' => $post]))
If you want to disable automatic linking to the related record, you can use the ->recordUrl() method as well by returning null:
Timeline::make()
->recordUrl(Post::class, fn (Post $post) => null)
Limiting results
You can also limit the Addresses for which we will retrieve activity items:
Timeline::make()
->withRelations([
'addresses' => function (HasMany $relation) {
return $relation->where('type', AddressType::Shipping);
}
]),
Hiding the label
Usually the design of the timeline will be clear enough to the user what it represents, so hiding the label will make the end result cleaner. You can hide the label using the ->hiddenLabel() method:
Timeline::make()
->hiddenLabel(),