Fix translations for activity logs (#907)

* fix translations for activity logs

* add backwards compatibility for old logs

* update lang file

* small cleanup

* fix singular/ plural for "file"

* fix for "rename" + disable bulk move (because it's not working)
This commit is contained in:
Boy132 2025-01-23 09:05:23 +01:00 committed by GitHub
parent 262e2fd09a
commit 37ba62410f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 97 additions and 89 deletions

View File

@ -2,6 +2,7 @@
namespace App\Filament\Server\Resources\ActivityResource\Pages; namespace App\Filament\Server\Resources\ActivityResource\Pages;
use App\Filament\Admin\Resources\UserResource\Pages\EditUser;
use App\Filament\Server\Resources\ActivityResource; use App\Filament\Server\Resources\ActivityResource;
use App\Models\ActivityLog; use App\Models\ActivityLog;
use App\Models\User; use App\Models\User;
@ -20,12 +21,16 @@ class ListActivities extends ListRecords
->columns([ ->columns([
TextColumn::make('event') TextColumn::make('event')
->html() ->html()
->formatStateUsing(fn ($state, ActivityLog $activityLog) => __('activity.'.str($state)->replace(':', '.'))) // TODO: convert properties to a format that trans likes, see ActivityLogEntry.tsx - wrapProperties ->description(fn ($state) => $state)
->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);
}),
TextColumn::make('user') TextColumn::make('user')
->state(fn (ActivityLog $activityLog) => $activityLog->actor instanceof User ? $activityLog->actor->username : 'System') ->state(fn (ActivityLog $activityLog) => $activityLog->actor instanceof User ? $activityLog->actor->username : 'System')
->tooltip(fn (ActivityLog $activityLog) => auth()->user()->can('seeIps activityLog') ? $activityLog->ip : '') ->tooltip(fn (ActivityLog $activityLog) => auth()->user()->can('seeIps activityLog') ? $activityLog->ip : '')
->url(fn (ActivityLog $activityLog): string => $activityLog->actor instanceof User ? route('filament.admin.resources.users.edit', ['record' => $activityLog->actor]) : ''), ->url(fn (ActivityLog $activityLog): string => $activityLog->actor instanceof User ? EditUser::getUrl(['record' => $activityLog->actor], panel: 'admin', tenant: null) : ''),
DateTimeColumn::make('timestamp') DateTimeColumn::make('timestamp')
->since() ->since()
->sortable(), ->sortable(),

View File

@ -130,13 +130,17 @@ class ListFiles extends ListRecords
->required(), ->required(),
]) ])
->action(function ($data, File $file, DaemonFileRepository $fileRepository) use ($server) { ->action(function ($data, File $file, DaemonFileRepository $fileRepository) use ($server) {
$files = [['to' => $data['name'], 'from' => $file->name]];
$fileRepository $fileRepository
->setServer($server) ->setServer($server)
->renameFiles($this->path, [['to' => $data['name'], 'from' => $file->name]]); ->renameFiles($this->path, $files);
Activity::event('server:file.rename') Activity::event('server:file.rename')
->property('directory', $this->path) ->property('directory', $this->path)
->property('files', [['to' => $data['name'], 'from' => $file->name]]) ->property('files', $files)
->property('to', $data['name'])
->property('from', $file->name)
->log(); ->log();
Notification::make() Notification::make()
@ -204,13 +208,17 @@ class ListFiles extends ListRecords
->action(function ($data, File $file, DaemonFileRepository $fileRepository) use ($server) { ->action(function ($data, File $file, DaemonFileRepository $fileRepository) use ($server) {
$location = resolve_path(join_paths($this->path, $data['location'])); $location = resolve_path(join_paths($this->path, $data['location']));
$files = [['to' => $location, 'from' => $file->name]];
$fileRepository $fileRepository
->setServer($server) ->setServer($server)
->renameFiles($this->path, [['to' => $location, 'from' => $file->name]]); ->renameFiles($this->path, $files);
Activity::event('server:file.rename') Activity::event('server:file.rename')
->property('directory', $this->path) ->property('directory', $this->path)
->property('files', [['to' => $location, 'from' => $file->name]]) ->property('files', $files)
->property('to', $location)
->property('from', $file->name)
->log(); ->log();
Notification::make() Notification::make()
@ -309,7 +317,7 @@ class ListFiles extends ListRecords
Activity::event('server:file.decompress') Activity::event('server:file.decompress')
->property('directory', $this->path) ->property('directory', $this->path)
->property('files', $file->name) ->property('file', $file->name)
->log(); ->log();
Notification::make() Notification::make()
@ -342,6 +350,7 @@ class ListFiles extends ListRecords
BulkActionGroup::make([ BulkActionGroup::make([
BulkAction::make('move') BulkAction::make('move')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server)) ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
->hidden() // TODO
->form([ ->form([
TextInput::make('location') TextInput::make('location')
->label('File name') ->label('File name')
@ -366,7 +375,7 @@ class ListFiles extends ListRecords
->log(); ->log();
Notification::make() Notification::make()
->title(count($files) . ' Files were moved from to ' . $location) ->title(count($files) . ' Files were moved from ' . $location)
->success() ->success()
->send(); ->send();
}), }),

View File

@ -146,13 +146,17 @@ class FileController extends ClientApiController
*/ */
public function rename(RenameFileRequest $request, Server $server): JsonResponse public function rename(RenameFileRequest $request, Server $server): JsonResponse
{ {
$files = $request->input('files');
$this->fileRepository $this->fileRepository
->setServer($server) ->setServer($server)
->renameFiles($request->input('root'), $request->input('files')); ->renameFiles($request->input('root'), $files);
Activity::event('server:file.rename') Activity::event('server:file.rename')
->property('directory', $request->input('root')) ->property('directory', $request->input('root'))
->property('files', $request->input('files')) ->property('files', $files)
->property('to', $files['to'])
->property('from', $files['from'])
->log(); ->log();
return new JsonResponse([], Response::HTTP_NO_CONTENT); return new JsonResponse([], Response::HTTP_NO_CONTENT);
@ -210,7 +214,7 @@ class FileController extends ClientApiController
Activity::event('server:file.decompress') Activity::event('server:file.decompress')
->property('directory', $request->input('root')) ->property('directory', $request->input('root'))
->property('files', $request->input('file')) ->property('file', $request->input('file'))
->log(); ->log();
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT); return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);

View File

@ -11,6 +11,7 @@ use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Eloquent\Model as IlluminateModel; use Illuminate\Database\Eloquent\Model as IlluminateModel;
use Illuminate\Support\Str;
/** /**
* \App\Models\ActivityLog. * \App\Models\ActivityLog.
@ -151,8 +152,8 @@ class ActivityLog extends Model
'username' => 'system', 'username' => 'system',
]); ]);
} }
$properties = $this->wrapProperties();
$event = __('activity.'.str($this->event)->replace(':', '.')); $event = trans_choice('activity.'.str($this->event)->replace(':', '.'), array_key_exists('count', $properties) ? $properties['count'] : 1, $properties);
return " return "
<div style='display: flex; align-items: center;'> <div style='display: flex; align-items: center;'>
@ -166,4 +167,38 @@ class ActivityLog extends Model
</div> </div>
"; ";
} }
public function wrapProperties(): array
{
if (!$this->properties || $this->properties->isEmpty()) {
return [];
}
$properties = $this->properties->mapWithKeys(function ($value, $key) {
if (!is_array($value)) {
// Perform some directory normalization at this point.
if ($key === 'directory') {
$value = str_replace('//', '/', '/' . trim($value, '/') . '/');
}
return [$key => $value];
}
$first = array_first($value);
// Backwards compatibility for old logs
if (is_array($first)) {
return ["{$key}_count" => count($value)];
}
return [$key => $first, "{$key}_count" => count($value)];
});
$keys = $properties->keys()->filter(fn ($key) => Str::endsWith($key, '_count'))->values();
if ($keys->containsOneItem()) {
$properties = $properties->merge(['count' => $properties->get($keys[0])])->except([$keys[0]]);
}
return $properties->toArray();
}
} }

View File

@ -2,7 +2,6 @@
namespace App\Transformers\Api\Client; namespace App\Transformers\Api\Client;
use Illuminate\Support\Str;
use App\Models\User; use App\Models\User;
use App\Models\ActivityLog; use App\Models\ActivityLog;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
@ -29,7 +28,7 @@ class ActivityLogTransformer extends BaseClientTransformer
'is_api' => !is_null($model->api_key_id), 'is_api' => !is_null($model->api_key_id),
'ip' => $this->canViewIP($model->actor) ? $model->ip : null, 'ip' => $this->canViewIP($model->actor) ? $model->ip : null,
'description' => $model->description, 'description' => $model->description,
'properties' => $this->properties($model), 'properties' => $model->wrapProperties(),
'has_additional_metadata' => $this->hasAdditionalMetadata($model), 'has_additional_metadata' => $this->hasAdditionalMetadata($model),
'timestamp' => $model->timestamp->toAtomString(), 'timestamp' => $model->timestamp->toAtomString(),
]; ];
@ -44,42 +43,6 @@ class ActivityLogTransformer extends BaseClientTransformer
return $this->item($model->actor, $this->makeTransformer(UserTransformer::class), User::RESOURCE_NAME); return $this->item($model->actor, $this->makeTransformer(UserTransformer::class), User::RESOURCE_NAME);
} }
/**
* Transforms any array values in the properties into a countable field for easier
* use within the translation outputs.
*/
protected function properties(ActivityLog $model): object
{
if (!$model->properties || $model->properties->isEmpty()) {
return (object) [];
}
$properties = $model->properties
->mapWithKeys(function ($value, $key) use ($model) {
if ($key === 'ip' && $model->actor instanceof User && !$model->actor->is($this->request->user())) {
return [$key => '[hidden]'];
}
if (!is_array($value)) {
// Perform some directory normalization at this point.
if ($key === 'directory') {
$value = str_replace('//', '/', '/' . trim($value, '/') . '/');
}
return [$key => $value];
}
return [$key => $value, "{$key}_count" => count($value)];
});
$keys = $properties->keys()->filter(fn ($key) => Str::endsWith($key, '_count'))->values();
if ($keys->containsOneItem()) {
$properties = $properties->merge(['count' => $properties->get($keys[0])])->except([$keys[0]]);
}
return (object) $properties->toArray();
}
/** /**
* Determines if there are any log properties that we've not already exposed * 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 * in the response language string and that are not just the IP address or

View File

@ -15,7 +15,7 @@ return [
'checkpoint' => 'Two-factor authentication requested', 'checkpoint' => 'Two-factor authentication requested',
'recovery-token' => 'Used two-factor recovery token', 'recovery-token' => 'Used two-factor recovery token',
'token' => 'Solved two-factor challenge', 'token' => 'Solved two-factor challenge',
'ip-blocked' => 'Blocked request from unlisted IP address for :identifier', 'ip-blocked' => 'Blocked request from unlisted IP address for <b>:identifier</b>',
'sftp' => [ 'sftp' => [
'fail' => 'Failed SFTP log in', 'fail' => 'Failed SFTP log in',
], ],
@ -26,12 +26,12 @@ return [
'password-changed' => 'Changed password', 'password-changed' => 'Changed password',
], ],
'api-key' => [ 'api-key' => [
'create' => 'Created new API key :identifier', 'create' => 'Created new API key <b>:identifier</b>',
'delete' => 'Deleted API key :identifier', 'delete' => 'Deleted API key <b>:identifier</b>',
], ],
'ssh-key' => [ 'ssh-key' => [
'create' => 'Added SSH key :fingerprint to account', 'create' => 'Added SSH key <b>:fingerprint</b> to account',
'delete' => 'Removed SSH key :fingerprint from account', 'delete' => 'Removed SSH key <b>:fingerprint</b> from account',
], ],
'two-factor' => [ 'two-factor' => [
'create' => 'Enabled two-factor auth', 'create' => 'Enabled two-factor auth',
@ -41,7 +41,7 @@ return [
'server' => [ 'server' => [
'reinstall' => 'Reinstalled server', 'reinstall' => 'Reinstalled server',
'console' => [ 'console' => [
'command' => 'Executed ":command" on the server', 'command' => 'Executed "<b>:command</b>" on the server',
], ],
'power' => [ 'power' => [
'start' => 'Started the server', 'start' => 'Started the server',
@ -52,7 +52,7 @@ return [
'backup' => [ 'backup' => [
'download' => 'Downloaded the <b>:name</b> backup', 'download' => 'Downloaded the <b>:name</b> backup',
'delete' => 'Deleted the <b>:name</b> backup', 'delete' => 'Deleted the <b>:name</b> backup',
'restore' => 'Restored the <b>:name</b> backup (deleted files: :truncate)', 'restore' => 'Restored the <b>:name</b> backup (deleted files: <b>:truncate</b>)',
'restore-complete' => 'Completed restoration of the <b>:name</b> backup', 'restore-complete' => 'Completed restoration of the <b>:name</b> backup',
'restore-failed' => 'Failed to complete restoration of the <b>:name</b> backup', 'restore-failed' => 'Failed to complete restoration of the <b>:name</b> backup',
'start' => 'Started a new backup <b>:name</b>', 'start' => 'Started a new backup <b>:name</b>',
@ -67,40 +67,32 @@ return [
'delete' => 'Deleted database <b>:name</b>', 'delete' => 'Deleted database <b>:name</b>',
], ],
'file' => [ 'file' => [
'compress_one' => 'Compressed :directory:file', 'compress' => 'Compressed <b>:directory:files</b>|Compressed <b>:count</b> files in <b>:directory</b>',
'compress_other' => 'Compressed :count files in :directory', 'read' => 'Viewed the contents of <b>:file</b>',
'read' => 'Viewed the contents of :file', 'copy' => 'Created a copy of <b>:file</b>',
'copy' => 'Created a copy of :file', 'create-directory' => 'Created directory <b>:directory:name</b>',
'create-directory' => 'Created directory :directory<b>:name</b>', 'decompress' => 'Decompressed <b>:file</b> in <b>:directory</b>',
'decompress' => 'Decompressed :files in :directory', 'delete' => 'Deleted <b>:directory:files</b>|Deleted <b>:count</b> files in <b>:directory</b>',
'delete_one' => 'Deleted :directory:files.0', 'download' => 'Downloaded <b>:file</b>',
'delete_other' => 'Deleted :count files in :directory', 'pull' => 'Downloaded a remote file from <b>:url</b> to <b>:directory</b>',
'download' => 'Downloaded :file', 'rename' => 'Renamed <b>:directory:from</b> to <b>:directory:to</b>|Renamed <b>:count</b> files in <b>:directory</b>',
'pull' => 'Downloaded a remote file from :url to :directory', 'write' => 'Wrote new content to <b>:file</b>',
'rename_one' => 'Renamed :directory:files.0.from to :directory:files.0.to',
'rename_other' => 'Renamed :count files in :directory',
'write' => 'Wrote new content to :file',
'upload' => 'Began a file upload', 'upload' => 'Began a file upload',
'uploaded' => 'Uploaded :directory:file', 'uploaded' => 'Uploaded <b>:directory:file</b>',
], ],
'sftp' => [ 'sftp' => [
'denied' => 'Blocked SFTP access due to permissions', 'denied' => 'Blocked SFTP access due to permissions',
'create_one' => 'Created :files.0', 'create' => 'Created <b>:files</b>|Created <b>:count</b> new files',
'create_other' => 'Created :count new files', 'write' => 'Modified the contents of <b>:files</b>|Modified the contents of <b>:count</b> files',
'write_one' => 'Modified the contents of :files.0', 'delete' => 'Deleted <b>:files</b>|Deleted <b>:count</b> files',
'write_other' => 'Modified the contents of :count files', 'create-directory' => 'Created the <b>:files</b> directory|Created <b>:count</b> directories',
'delete_one' => 'Deleted :files.0', 'rename' => 'Renamed <b>:from</b> to <b>:to</b>|Renamed or moved <b>:count</b> files',
'delete_other' => 'Deleted :count files',
'create-directory_one' => 'Created the :files.0 directory',
'create-directory_other' => 'Created :count directories',
'rename_one' => 'Renamed :files.0.from to :files.0.to',
'rename_other' => 'Renamed or moved :count files',
], ],
'allocation' => [ 'allocation' => [
'create' => 'Added :allocation to the server', 'create' => 'Added <b>:allocation</b> to the server',
'notes' => 'Updated the notes for :allocation from "<b>:old</b>" to "<b>:new</b>"', 'notes' => 'Updated the notes for <b>:allocation</b> from "<b>:old</b>" to "<b>:new</b>"',
'primary' => 'Set :allocation as the primary server allocation', 'primary' => 'Set <b>:allocation</b> as the primary server allocation',
'delete' => 'Deleted the :allocation allocation', 'delete' => 'Deleted the <b>:allocation</b> allocation',
], ],
'schedule' => [ 'schedule' => [
'create' => 'Created the <b>:name</b> schedule', 'create' => 'Created the <b>:name</b> schedule',
@ -114,8 +106,8 @@ return [
'delete' => 'Deleted a task for the <b>:name</b> schedule', 'delete' => 'Deleted a task for the <b>:name</b> schedule',
], ],
'settings' => [ 'settings' => [
'rename' => 'Renamed the server from <b>:old</b> to <b>:new</b>', 'rename' => 'Renamed the server from "<b>:old</b>" to "<b>:new</b>"',
'description' => 'Changed the server description from <b>:old</b> to <b>:new</b>', 'description' => 'Changed the server description from "<b>:old</b>" to "<b>:new</b>"',
], ],
'startup' => [ 'startup' => [
'edit' => 'Changed the <b>:variable</b> variable from "<b>:old</b>" to "<b>:new</b>"', 'edit' => 'Changed the <b>:variable</b> variable from "<b>:old</b>" to "<b>:new</b>"',