diff --git a/app/Filament/Server/Resources/ActivityResource/Pages/ListActivities.php b/app/Filament/Server/Resources/ActivityResource/Pages/ListActivities.php index 039b8c2749..7a0cf0cd9b 100644 --- a/app/Filament/Server/Resources/ActivityResource/Pages/ListActivities.php +++ b/app/Filament/Server/Resources/ActivityResource/Pages/ListActivities.php @@ -5,11 +5,21 @@ 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(); + ->icon(fn (ActivityLog $activityLog) => $activityLog->getIcon()) + ->formatStateUsing(fn (ActivityLog $activityLog) => $activityLog->getLabel()), + TextColumn::make('user') + ->state(function (ActivityLog $activityLog) use ($server) { + if (!$activityLog->actor instanceof User) { + return 'System'; + } - return trans_choice('activity.'.str($state)->replace(':', '.'), array_get($properties, 'count', 1), $properties); - }) - ->tooltip(function (ActivityLog $activityLog) { - $files = array_get($activityLog->properties, 'files', []); + $user = $activityLog->actor->username; - return is_array($files) ? implode(',', $files) : null; - }), - TextColumn::make('user') - ->state(fn (ActivityLog $activityLog) => $activityLog->actor instanceof User ? $activityLog->actor->username : 'System') + // 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') + ->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'), + ]), ]) - ->defaultSort('timestamp', 'desc'); + ->filters([ + SelectFilter::make('event') + ->options(fn (Table $table) => $table->getQuery()->pluck('event', 'event')->unique()->sort()) + ->searchable() + ->preload(), + ]); } public function getBreadcrumbs(): array diff --git a/app/Models/ActivityLog.php b/app/Models/ActivityLog.php index 15e2fd1431..d7ce7530ab 100644 --- a/app/Models/ActivityLog.php +++ b/app/Models/ActivityLog.php @@ -5,6 +5,8 @@ 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 @@ * @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 @@ protected static function boot(): void }); } + 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 @@ public function htmlable(): string 'username' => 'system', ]); } - $properties = $this->wrapProperties(); - $event = trans_choice('activity.'.str($this->event)->replace(':', '.'), array_key_exists('count', $properties) ? $properties['count'] : 1, $properties); return "
@@ -161,7 +177,7 @@ public function htmlable(): string

$user->username — $this->event

-

$event

+

{$this->getLabel()}

$this->ip — {$this->timestamp->diffForHumans()}

@@ -201,4 +217,34 @@ public function wrapProperties(): array 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('/:(?[\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; + } } diff --git a/app/Transformers/Api/Client/ActivityLogTransformer.php b/app/Transformers/Api/Client/ActivityLogTransformer.php index a5d3128d54..c54733f349 100644 --- a/app/Transformers/Api/Client/ActivityLogTransformer.php +++ b/app/Transformers/Api/Client/ActivityLogTransformer.php @@ -29,7 +29,7 @@ public function transform(ActivityLog $model): array '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 @@ public function includeActor(ActivityLog $model): ResourceAbstract 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('/:(?[\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'); } } diff --git a/lang/en/activity.php b/lang/en/activity.php index fa712e40d5..0d921c8a3d 100644 --- a/lang/en/activity.php +++ b/lang/en/activity.php @@ -118,5 +118,6 @@ 'update' => 'Updated the subuser permissions for :email', 'delete' => 'Removed :email as a subuser', ], + 'crashed' => 'Server crashed', ], ];