mirror of
				https://github.com/pelican-dev/panel.git
				synced 2025-11-04 05:16:52 +01:00 
			
		
		
		
	Activity log list improvements (#939)
* handle "server:crashed" log * update activity log list * add event filter * add email to user column * fix phpstan * only show the email if the actor is the server owner/ a subuser or if the viewing user is an admin * Apply same logic from ViewAction & make sure user is admi for url * Add pagination to avoid showing 2000 records at once * update can check & pagination --------- Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
This commit is contained in:
		
							parent
							
								
									71f3abe464
								
							
						
					
					
						commit
						3202a59b07
					
				@ -5,11 +5,21 @@ namespace App\Filament\Server\Resources\ActivityResource\Pages;
 | 
			
		||||
use App\Filament\Admin\Resources\UserResource\Pages\EditUser;
 | 
			
		||||
use App\Filament\Server\Resources\ActivityResource;
 | 
			
		||||
use App\Models\ActivityLog;
 | 
			
		||||
use App\Models\User;
 | 
			
		||||
use App\Filament\Components\Tables\Columns\DateTimeColumn;
 | 
			
		||||
use App\Models\Server;
 | 
			
		||||
use App\Models\User;
 | 
			
		||||
use Filament\Facades\Filament;
 | 
			
		||||
use Filament\Forms\Components\Actions\Action;
 | 
			
		||||
use Filament\Forms\Components\DateTimePicker;
 | 
			
		||||
use Filament\Forms\Components\KeyValue;
 | 
			
		||||
use Filament\Forms\Components\Placeholder;
 | 
			
		||||
use Filament\Forms\Components\TextInput;
 | 
			
		||||
use Filament\Resources\Pages\ListRecords;
 | 
			
		||||
use Filament\Tables\Actions\ViewAction;
 | 
			
		||||
use Filament\Tables\Columns\TextColumn;
 | 
			
		||||
use Filament\Tables\Filters\SelectFilter;
 | 
			
		||||
use Filament\Tables\Table;
 | 
			
		||||
use Illuminate\Support\HtmlString;
 | 
			
		||||
 | 
			
		||||
class ListActivities extends ListRecords
 | 
			
		||||
{
 | 
			
		||||
@ -17,30 +27,85 @@ class ListActivities extends ListRecords
 | 
			
		||||
 | 
			
		||||
    public function table(Table $table): Table
 | 
			
		||||
    {
 | 
			
		||||
        /** @var Server $server */
 | 
			
		||||
        $server = Filament::getTenant();
 | 
			
		||||
 | 
			
		||||
        return $table
 | 
			
		||||
            ->paginated([25, 50, 100, 250])
 | 
			
		||||
            ->defaultPaginationPageOption(25)
 | 
			
		||||
            ->columns([
 | 
			
		||||
                TextColumn::make('event')
 | 
			
		||||
                    ->html()
 | 
			
		||||
                    ->description(fn ($state) => $state)
 | 
			
		||||
                    ->formatStateUsing(function ($state, ActivityLog $activityLog) {
 | 
			
		||||
                        $properties = $activityLog->wrapProperties();
 | 
			
		||||
 | 
			
		||||
                        return trans_choice('activity.'.str($state)->replace(':', '.'), array_get($properties, 'count', 1), $properties);
 | 
			
		||||
                    })
 | 
			
		||||
                    ->tooltip(function (ActivityLog $activityLog) {
 | 
			
		||||
                        $files = array_get($activityLog->properties, 'files', []);
 | 
			
		||||
 | 
			
		||||
                        return is_array($files) ? implode(',', $files) : null;
 | 
			
		||||
                    }),
 | 
			
		||||
                    ->icon(fn (ActivityLog $activityLog) => $activityLog->getIcon())
 | 
			
		||||
                    ->formatStateUsing(fn (ActivityLog $activityLog) => $activityLog->getLabel()),
 | 
			
		||||
                TextColumn::make('user')
 | 
			
		||||
                    ->state(fn (ActivityLog $activityLog) => $activityLog->actor instanceof User ? $activityLog->actor->username : 'System')
 | 
			
		||||
                    ->state(function (ActivityLog $activityLog) use ($server) {
 | 
			
		||||
                        if (!$activityLog->actor instanceof User) {
 | 
			
		||||
                            return 'System';
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        $user = $activityLog->actor->username;
 | 
			
		||||
 | 
			
		||||
                        // Only show the email if the actor is the server owner/ a subuser or if the viewing user is an admin
 | 
			
		||||
                        if (auth()->user()->isAdmin() || $server->owner_id === $activityLog->actor->id || $server->subusers->where('user_id', $activityLog->actor->id)->first()) {
 | 
			
		||||
                            $user .= " ({$activityLog->actor->email})";
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        return $user;
 | 
			
		||||
                    })
 | 
			
		||||
                    ->tooltip(fn (ActivityLog $activityLog) => auth()->user()->can('seeIps activityLog') ? $activityLog->ip : '')
 | 
			
		||||
                    ->url(fn (ActivityLog $activityLog): string => $activityLog->actor instanceof User ? EditUser::getUrl(['record' => $activityLog->actor], panel: 'admin', tenant: null) : ''),
 | 
			
		||||
                    ->url(fn (ActivityLog $activityLog) => $activityLog->actor instanceof User && auth()->user()->can('update user') ? EditUser::getUrl(['record' => $activityLog->actor], panel: 'admin') : '')
 | 
			
		||||
                    ->grow(false),
 | 
			
		||||
                DateTimeColumn::make('timestamp')
 | 
			
		||||
                    ->since()
 | 
			
		||||
                    ->sortable(),
 | 
			
		||||
                    ->sortable()
 | 
			
		||||
                    ->grow(false),
 | 
			
		||||
            ])
 | 
			
		||||
            ->defaultSort('timestamp', 'desc');
 | 
			
		||||
            ->defaultSort('timestamp', 'desc')
 | 
			
		||||
            ->actions([
 | 
			
		||||
                ViewAction::make()
 | 
			
		||||
                    //->visible(fn (ActivityLog $activityLog) => $activityLog->hasAdditionalMetadata())
 | 
			
		||||
                    ->form([
 | 
			
		||||
                        Placeholder::make('event')
 | 
			
		||||
                            ->content(fn (ActivityLog $activityLog) => new HtmlString($activityLog->getLabel())),
 | 
			
		||||
                        TextInput::make('user')
 | 
			
		||||
                            ->formatStateUsing(function (ActivityLog $activityLog) use ($server) {
 | 
			
		||||
                                if (!$activityLog->actor instanceof User) {
 | 
			
		||||
                                    return 'System';
 | 
			
		||||
                                }
 | 
			
		||||
 | 
			
		||||
                                $user = $activityLog->actor->username;
 | 
			
		||||
 | 
			
		||||
                                // Only show the email if the actor is the server owner/ a subuser or if the viewing user is an admin
 | 
			
		||||
                                if (auth()->user()->isAdmin() || $server->owner_id === $activityLog->actor->id || $server->subusers->where('user_id', $activityLog->actor->id)->first()) {
 | 
			
		||||
                                    $user .= " ({$activityLog->actor->email})";
 | 
			
		||||
                                }
 | 
			
		||||
 | 
			
		||||
                                if (auth()->user()->can('seeIps activityLog')) {
 | 
			
		||||
                                    $user .= " - $activityLog->ip";
 | 
			
		||||
                                }
 | 
			
		||||
 | 
			
		||||
                                return $user;
 | 
			
		||||
                            })
 | 
			
		||||
                            ->hintAction(
 | 
			
		||||
                                Action::make('edit')
 | 
			
		||||
                                    ->label(__('filament-actions::edit.single.label'))
 | 
			
		||||
                                    ->icon('tabler-edit')
 | 
			
		||||
                                    ->visible(fn (ActivityLog $activityLog) => $activityLog->actor instanceof User && auth()->user()->can('update user'))
 | 
			
		||||
                                    ->url(fn (ActivityLog $activityLog) => EditUser::getUrl(['record' => $activityLog->actor], panel: 'admin'))
 | 
			
		||||
                            ),
 | 
			
		||||
                        DateTimePicker::make('timestamp'),
 | 
			
		||||
                        KeyValue::make('properties')
 | 
			
		||||
                            ->label('Metadata'),
 | 
			
		||||
                    ]),
 | 
			
		||||
            ])
 | 
			
		||||
            ->filters([
 | 
			
		||||
                SelectFilter::make('event')
 | 
			
		||||
                    ->options(fn (Table $table) => $table->getQuery()->pluck('event', 'event')->unique()->sort())
 | 
			
		||||
                    ->searchable()
 | 
			
		||||
                    ->preload(),
 | 
			
		||||
            ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getBreadcrumbs(): array
 | 
			
		||||
 | 
			
		||||
@ -5,6 +5,8 @@ namespace App\Models;
 | 
			
		||||
use Carbon\Carbon;
 | 
			
		||||
use Illuminate\Support\Facades\Event;
 | 
			
		||||
use App\Events\ActivityLogged;
 | 
			
		||||
use Filament\Support\Contracts\HasIcon;
 | 
			
		||||
use Filament\Support\Contracts\HasLabel;
 | 
			
		||||
use Illuminate\Database\Eloquent\Builder;
 | 
			
		||||
use Illuminate\Database\Eloquent\MassPrunable;
 | 
			
		||||
use Illuminate\Database\Eloquent\Relations\HasOne;
 | 
			
		||||
@ -47,7 +49,7 @@ use Illuminate\Support\Str;
 | 
			
		||||
 * @method static Builder|ActivityLog whereProperties($value)
 | 
			
		||||
 * @method static Builder|ActivityLog whereTimestamp($value)
 | 
			
		||||
 */
 | 
			
		||||
class ActivityLog extends Model
 | 
			
		||||
class ActivityLog extends Model implements HasIcon, HasLabel
 | 
			
		||||
{
 | 
			
		||||
    use MassPrunable;
 | 
			
		||||
 | 
			
		||||
@ -143,6 +145,22 @@ class ActivityLog extends Model
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getIcon(): string
 | 
			
		||||
    {
 | 
			
		||||
        if ($this->apiKey) {
 | 
			
		||||
            return 'tabler-api';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $this->actor instanceof User ? 'tabler-user' : 'tabler-device-desktop';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getLabel(): string
 | 
			
		||||
    {
 | 
			
		||||
        $properties = $this->wrapProperties();
 | 
			
		||||
 | 
			
		||||
        return trans_choice('activity.'.str($this->event)->replace(':', '.'), array_key_exists('count', $properties) ? $properties['count'] : 1, $properties);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function htmlable(): string
 | 
			
		||||
    {
 | 
			
		||||
        $user = $this->actor;
 | 
			
		||||
@ -152,8 +170,6 @@ class ActivityLog extends Model
 | 
			
		||||
                'username' => 'system',
 | 
			
		||||
            ]);
 | 
			
		||||
        }
 | 
			
		||||
        $properties = $this->wrapProperties();
 | 
			
		||||
        $event = trans_choice('activity.'.str($this->event)->replace(':', '.'), array_key_exists('count', $properties) ? $properties['count'] : 1, $properties);
 | 
			
		||||
 | 
			
		||||
        return "
 | 
			
		||||
            <div style='display: flex; align-items: center;'>
 | 
			
		||||
@ -161,7 +177,7 @@ class ActivityLog extends Model
 | 
			
		||||
 | 
			
		||||
                <div>
 | 
			
		||||
                    <p>$user->username — $this->event</p>
 | 
			
		||||
                    <p>$event</p>
 | 
			
		||||
                    <p>{$this->getLabel()}</p>
 | 
			
		||||
                    <p>$this->ip — <span title='{$this->timestamp->format('M j, Y g:ia')}'>{$this->timestamp->diffForHumans()}</span></p>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
@ -201,4 +217,34 @@ class ActivityLog extends Model
 | 
			
		||||
 | 
			
		||||
        return $properties->toArray();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Determines if there are any log properties that we've not already exposed
 | 
			
		||||
     * in the response language string and that are not just the IP address or
 | 
			
		||||
     * the browser useragent.
 | 
			
		||||
     *
 | 
			
		||||
     * This is used by the front-end to selectively display an "additional metadata"
 | 
			
		||||
     * button that is pointless if there is nothing the user can't already see from
 | 
			
		||||
     * the event description.
 | 
			
		||||
     */
 | 
			
		||||
    public function hasAdditionalMetadata(): bool
 | 
			
		||||
    {
 | 
			
		||||
        if (!$this->properties || $this->properties->isEmpty()) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $properties = $this->wrapProperties();
 | 
			
		||||
        $event = trans_choice('activity.'.str($this->event)->replace(':', '.'), array_key_exists('count', $properties) ? $properties['count'] : 1);
 | 
			
		||||
 | 
			
		||||
        preg_match_all('/:(?<key>[\w.-]+\w)(?:[^\w:]?|$)/', $event, $matches);
 | 
			
		||||
 | 
			
		||||
        $exclude = array_merge($matches['key'], ['ip', 'useragent', 'using_sftp']);
 | 
			
		||||
        foreach ($this->properties->keys() as $key) {
 | 
			
		||||
            if (!in_array($key, $exclude, true)) {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -29,7 +29,7 @@ class ActivityLogTransformer extends BaseClientTransformer
 | 
			
		||||
            'ip' => $this->canViewIP($model->actor) ? $model->ip : null,
 | 
			
		||||
            'description' => $model->description,
 | 
			
		||||
            'properties' => $model->wrapProperties(),
 | 
			
		||||
            'has_additional_metadata' => $this->hasAdditionalMetadata($model),
 | 
			
		||||
            'has_additional_metadata' => $model->hasAdditionalMetadata(),
 | 
			
		||||
            'timestamp' => $model->timestamp->toAtomString(),
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
@ -43,40 +43,12 @@ class ActivityLogTransformer extends BaseClientTransformer
 | 
			
		||||
        return $this->item($model->actor, $this->makeTransformer(UserTransformer::class), User::RESOURCE_NAME);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Determines if there are any log properties that we've not already exposed
 | 
			
		||||
     * in the response language string and that are not just the IP address or
 | 
			
		||||
     * the browser useragent.
 | 
			
		||||
     *
 | 
			
		||||
     * This is used by the front-end to selectively display an "additional metadata"
 | 
			
		||||
     * button that is pointless if there is nothing the user can't already see from
 | 
			
		||||
     * the event description.
 | 
			
		||||
     */
 | 
			
		||||
    protected function hasAdditionalMetadata(ActivityLog $model): bool
 | 
			
		||||
    {
 | 
			
		||||
        if (is_null($model->properties) || $model->properties->isEmpty()) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $str = trans('activity.' . str_replace(':', '.', $model->event));
 | 
			
		||||
        preg_match_all('/:(?<key>[\w.-]+\w)(?:[^\w:]?|$)/', $str, $matches);
 | 
			
		||||
 | 
			
		||||
        $exclude = array_merge($matches['key'], ['ip', 'useragent', 'using_sftp']);
 | 
			
		||||
        foreach ($model->properties->keys() as $key) {
 | 
			
		||||
            if (!in_array($key, $exclude, true)) {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Determines if the user can view the IP address in the output either because they are the
 | 
			
		||||
     * actor that performed the action, or because they are an administrator on the Panel.
 | 
			
		||||
     */
 | 
			
		||||
    protected function canViewIP(?Model $actor = null): bool
 | 
			
		||||
    {
 | 
			
		||||
        return $actor?->is($this->request->user()) || $this->request->user()->isRootAdmin();
 | 
			
		||||
        return $actor?->is($this->request->user()) || $this->request->user()->can('seeIps activityLog');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -118,5 +118,6 @@ return [
 | 
			
		||||
            'update' => 'Updated the subuser permissions for <b>:email</b>',
 | 
			
		||||
            'delete' => 'Removed <b>:email</b> as a subuser',
 | 
			
		||||
        ],
 | 
			
		||||
        'crashed' => 'Server crashed',
 | 
			
		||||
    ],
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user