Filament Plugins

Purchase

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:

php
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.

php
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:

php
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.

php
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:

php
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.

php
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:

php
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:

php
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:

php
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:

php
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:

php
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:

php
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:

php
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:

php
[
    '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:

  1. If the causer model implements the Filament\Models\Contracts\HasName contract, then the getFilamentName() method will be used.
  2. If the causer model has an attribute called name, then this attribute will be used.
  3. If the causer model has an attribute called first_name and/or last_name, then these attributes will be used.
  4. 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:

php
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:

php
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:

php
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().

php
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():

php
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 Money object 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:

php
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:

php
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:

php
Timeline::make()
    ->attributeCast(MoneyCast::class, fn (?Money $value) => $value?->formatWithoutZeroes()),

This method can come in handy when globally configuring the timeline:

php
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:

php
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:

php
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:

php
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:

php
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:

php
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.

php
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:

php
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:

php
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:

php
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:

php
Timeline::make()
    ->compact(),

If you want to make all timelines compact by default, you can configure that globally:

php
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:

php
Timeline::make()
    ->convertHeroicons(false),
php
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:

php
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:

php
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:

php
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:

php
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:

php
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:

php
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.

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:

php
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}")
])

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:

php
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:

php
Timeline::make()
    ->recordUrl(Post::class, fn (Post $post) => null)

Limiting results

You can also limit the Addresses for which we will retrieve activity items:

php
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:

php
Timeline::make()
    ->hiddenLabel(),
© FilamentPlugins.com ✦ 2022 – 2026
PrivacyTerms & Conditions
All rights reserved.