From c22e7456b532e102f2a1ba7e8a4d57ea1d955ba0 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Thu, 22 May 2025 08:41:17 +0200 Subject: [PATCH] Move tables & forms to resources in client area (#1388) --- .../Server/Resources/ActivityResource.php | 107 ++++++++++++- .../ActivityResource/Pages/ListActivities.php | 101 ------------- .../Server/Resources/AllocationResource.php | 61 ++++++++ .../Pages/ListAllocations.php | 65 +------- .../Server/Resources/BackupResource.php | 139 ++++++++++++++++- .../BackupResource/Pages/ListBackups.php | 141 +----------------- .../Server/Resources/DatabaseResource.php | 72 ++++++++- .../DatabaseResource/Pages/ListDatabases.php | 68 --------- .../Server/Resources/ScheduleResource.php | 46 ++++++ .../ScheduleResource/Pages/ListSchedules.php | 52 +------ .../ScheduleResource/Pages/ViewSchedule.php | 7 +- 11 files changed, 426 insertions(+), 433 deletions(-) diff --git a/app/Filament/Server/Resources/ActivityResource.php b/app/Filament/Server/Resources/ActivityResource.php index 584e27cde..97b6fb8dc 100644 --- a/app/Filament/Server/Resources/ActivityResource.php +++ b/app/Filament/Server/Resources/ActivityResource.php @@ -2,6 +2,8 @@ namespace App\Filament\Server\Resources; +use App\Filament\Admin\Resources\UserResource\Pages\EditUser; +use App\Filament\Components\Tables\Columns\DateTimeColumn; use App\Filament\Server\Resources\ActivityResource\Pages; use App\Models\ActivityLog; use App\Models\Permission; @@ -9,9 +11,20 @@ use App\Models\Role; 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\Resource; +use Filament\Tables\Actions\ViewAction; +use Filament\Tables\Columns\TextColumn; +use Filament\Tables\Filters\SelectFilter; +use Filament\Tables\Table; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Query\JoinClause; +use Illuminate\Support\Arr; +use Illuminate\Support\HtmlString; class ActivityResource extends Resource { @@ -25,6 +38,95 @@ class ActivityResource extends Resource protected static ?string $navigationIcon = 'tabler-stack'; + public static function table(Table $table): Table + { + /** @var Server $server */ + $server = Filament::getTenant(); + + return $table + ->paginated([25, 50]) + ->defaultPaginationPageOption(25) + ->columns([ + TextColumn::make('event') + ->html() + ->description(fn ($state) => $state) + ->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 $activityLog->actor_id === null ? 'System' : 'Deleted user'; + } + + $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) => $activityLog->actor instanceof User && auth()->user()->can('update user') ? EditUser::getUrl(['record' => $activityLog->actor], panel: 'admin') : '') + ->grow(false), + DateTimeColumn::make('timestamp') + ->since() + ->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 $activityLog->actor_id === null ? 'System' : 'Deleted user'; + } + + $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(trans('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') + ->formatStateUsing(fn ($state) => Arr::dot($state)), + ]), + ]) + ->filters([ + SelectFilter::make('event') + ->options(fn (Table $table) => $table->getQuery()->pluck('event', 'event')->unique()->sort()) + ->searchable() + ->preload(), + ]); + } + + public static function canViewAny(): bool + { + return auth()->user()->can(Permission::ACTION_ACTIVITY_READ, Filament::getTenant()); + } + public static function getEloquentQuery(): Builder { /** @var Server $server */ @@ -51,11 +153,6 @@ class ActivityResource extends Resource }); } - public static function canViewAny(): bool - { - return auth()->user()->can(Permission::ACTION_ACTIVITY_READ, Filament::getTenant()); - } - public static function getPages(): array { return [ diff --git a/app/Filament/Server/Resources/ActivityResource/Pages/ListActivities.php b/app/Filament/Server/Resources/ActivityResource/Pages/ListActivities.php index 9c953d81d..fca7f6207 100644 --- a/app/Filament/Server/Resources/ActivityResource/Pages/ListActivities.php +++ b/app/Filament/Server/Resources/ActivityResource/Pages/ListActivities.php @@ -2,114 +2,13 @@ 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\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\Arr; -use Illuminate\Support\HtmlString; class ListActivities extends ListRecords { protected static string $resource = ActivityResource::class; - public function table(Table $table): Table - { - /** @var Server $server */ - $server = Filament::getTenant(); - - return $table - ->paginated([25, 50]) - ->defaultPaginationPageOption(25) - ->columns([ - TextColumn::make('event') - ->html() - ->description(fn ($state) => $state) - ->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 $activityLog->actor_id === null ? 'System' : 'Deleted user'; - } - - $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) => $activityLog->actor instanceof User && auth()->user()->can('update user') ? EditUser::getUrl(['record' => $activityLog->actor], panel: 'admin') : '') - ->grow(false), - DateTimeColumn::make('timestamp') - ->since() - ->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 $activityLog->actor_id === null ? 'System' : 'Deleted user'; - } - - $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(trans('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') - ->formatStateUsing(fn ($state) => Arr::dot($state)), - ]), - ]) - ->filters([ - SelectFilter::make('event') - ->options(fn (Table $table) => $table->getQuery()->pluck('event', 'event')->unique()->sort()) - ->searchable() - ->preload(), - ]); - } - public function getBreadcrumbs(): array { return []; diff --git a/app/Filament/Server/Resources/AllocationResource.php b/app/Filament/Server/Resources/AllocationResource.php index 614f52b47..9719cc975 100644 --- a/app/Filament/Server/Resources/AllocationResource.php +++ b/app/Filament/Server/Resources/AllocationResource.php @@ -2,12 +2,18 @@ namespace App\Filament\Server\Resources; +use App\Facades\Activity; use App\Filament\Server\Resources\AllocationResource\Pages; use App\Models\Allocation; use App\Models\Permission; use App\Models\Server; use Filament\Facades\Filament; use Filament\Resources\Resource; +use Filament\Tables\Actions\DetachAction; +use Filament\Tables\Columns\IconColumn; +use Filament\Tables\Columns\TextColumn; +use Filament\Tables\Columns\TextInputColumn; +use Filament\Tables\Table; use Illuminate\Database\Eloquent\Model; class AllocationResource extends Resource @@ -22,6 +28,61 @@ class AllocationResource extends Resource protected static ?string $navigationIcon = 'tabler-network'; + public static function table(Table $table): Table + { + /** @var Server $server */ + $server = Filament::getTenant(); + + return $table + ->columns([ + TextColumn::make('ip') + ->label('Address') + ->formatStateUsing(fn (Allocation $allocation) => $allocation->alias), + TextColumn::make('alias') + ->hidden(), + TextColumn::make('port'), + TextInputColumn::make('notes') + ->visibleFrom('sm') + ->disabled(fn () => !auth()->user()->can(Permission::ACTION_ALLOCATION_UPDATE, $server)) + ->label('Notes') + ->placeholder('No Notes'), + IconColumn::make('primary') + ->icon(fn ($state) => match ($state) { + true => 'tabler-star-filled', + default => 'tabler-star', + }) + ->color(fn ($state) => match ($state) { + true => 'warning', + default => 'gray', + }) + ->action(function (Allocation $allocation) use ($server) { + if (auth()->user()->can(PERMISSION::ACTION_ALLOCATION_UPDATE, $server)) { + return $server->update(['allocation_id' => $allocation->id]); + } + }) + ->default(fn (Allocation $allocation) => $allocation->id === $server->allocation_id) + ->label('Primary'), + ]) + ->actions([ + DetachAction::make() + ->authorize(fn () => auth()->user()->can(Permission::ACTION_ALLOCATION_DELETE, $server)) + ->label('Delete') + ->icon('tabler-trash') + ->hidden(fn (Allocation $allocation) => $allocation->id === $server->allocation_id) + ->action(function (Allocation $allocation) { + Allocation::query()->where('id', $allocation->id)->update([ + 'notes' => null, + 'server_id' => null, + ]); + + Activity::event('server:allocation.delete') + ->subject($allocation) + ->property('allocation', $allocation->toString()) + ->log(); + }), + ]); + } + // TODO: find better way handle server conflict state public static function canAccess(): bool { diff --git a/app/Filament/Server/Resources/AllocationResource/Pages/ListAllocations.php b/app/Filament/Server/Resources/AllocationResource/Pages/ListAllocations.php index 24ed7e407..ee9526cc3 100644 --- a/app/Filament/Server/Resources/AllocationResource/Pages/ListAllocations.php +++ b/app/Filament/Server/Resources/AllocationResource/Pages/ListAllocations.php @@ -4,85 +4,24 @@ namespace App\Filament\Server\Resources\AllocationResource\Pages; use App\Facades\Activity; use App\Filament\Server\Resources\AllocationResource; -use App\Models\Allocation; use App\Models\Permission; use App\Models\Server; use App\Services\Allocations\FindAssignableAllocationService; -use Filament\Actions; +use Filament\Actions\Action; use Filament\Facades\Filament; use Filament\Resources\Pages\ListRecords; -use Filament\Tables\Actions\DetachAction; -use Filament\Tables\Columns\IconColumn; -use Filament\Tables\Columns\TextColumn; -use Filament\Tables\Columns\TextInputColumn; -use Filament\Tables\Table; class ListAllocations extends ListRecords { protected static string $resource = AllocationResource::class; - public function table(Table $table): Table - { - /** @var Server $server */ - $server = Filament::getTenant(); - - return $table - ->columns([ - TextColumn::make('ip') - ->label('Address') - ->formatStateUsing(fn (Allocation $allocation) => $allocation->alias), - TextColumn::make('alias') - ->hidden(), - TextColumn::make('port'), - TextInputColumn::make('notes') - ->visibleFrom('sm') - ->disabled(fn () => !auth()->user()->can(Permission::ACTION_ALLOCATION_UPDATE, $server)) - ->label('Notes') - ->placeholder('No Notes'), - IconColumn::make('primary') - ->icon(fn ($state) => match ($state) { - true => 'tabler-star-filled', - default => 'tabler-star', - }) - ->color(fn ($state) => match ($state) { - true => 'warning', - default => 'gray', - }) - ->action(function (Allocation $allocation) use ($server) { - if (auth()->user()->can(PERMISSION::ACTION_ALLOCATION_UPDATE, $server)) { - return $server->update(['allocation_id' => $allocation->id]); - } - }) - ->default(fn (Allocation $allocation) => $allocation->id === $server->allocation_id) - ->label('Primary'), - ]) - ->actions([ - DetachAction::make() - ->authorize(fn () => auth()->user()->can(Permission::ACTION_ALLOCATION_DELETE, $server)) - ->label('Delete') - ->icon('tabler-trash') - ->hidden(fn (Allocation $allocation) => $allocation->id === $server->allocation_id) - ->action(function (Allocation $allocation) { - Allocation::query()->where('id', $allocation->id)->update([ - 'notes' => null, - 'server_id' => null, - ]); - - Activity::event('server:allocation.delete') - ->subject($allocation) - ->property('allocation', $allocation->toString()) - ->log(); - }), - ]); - } - protected function getHeaderActions(): array { /** @var Server $server */ $server = Filament::getTenant(); return [ - Actions\Action::make('addAllocation') + Action::make('addAllocation') ->authorize(fn () => auth()->user()->can(Permission::ACTION_ALLOCATION_CREATE, $server)) ->label(fn () => $server->allocations()->count() >= $server->allocation_limit ? 'Allocation limit reached' : 'Add Allocation') ->hidden(fn () => !config('panel.client_features.allocations.enabled')) diff --git a/app/Filament/Server/Resources/BackupResource.php b/app/Filament/Server/Resources/BackupResource.php index 13b84f50e..d163b751b 100644 --- a/app/Filament/Server/Resources/BackupResource.php +++ b/app/Filament/Server/Resources/BackupResource.php @@ -2,13 +2,35 @@ namespace App\Filament\Server\Resources; +use App\Enums\BackupStatus; +use App\Enums\ServerState; +use App\Facades\Activity; use App\Filament\Server\Resources\BackupResource\Pages; +use App\Http\Controllers\Api\Client\Servers\BackupController; use App\Models\Backup; use App\Models\Permission; use App\Models\Server; +use App\Repositories\Daemon\DaemonBackupRepository; +use App\Services\Backups\DownloadLinkService; +use App\Filament\Components\Tables\Columns\BytesColumn; +use App\Filament\Components\Tables\Columns\DateTimeColumn; use Filament\Facades\Filament; +use Filament\Forms\Components\Checkbox; +use Filament\Forms\Components\Placeholder; +use Filament\Forms\Components\Textarea; +use Filament\Forms\Components\TextInput; +use Filament\Forms\Components\Toggle; +use Filament\Forms\Form; +use Filament\Notifications\Notification; use Filament\Resources\Resource; +use Filament\Tables\Actions\Action; +use Filament\Tables\Actions\ActionGroup; +use Filament\Tables\Actions\DeleteAction; +use Filament\Tables\Columns\IconColumn; +use Filament\Tables\Columns\TextColumn; +use Filament\Tables\Table; use Illuminate\Database\Eloquent\Model; +use Illuminate\Http\Request; class BackupResource extends Resource { @@ -44,8 +66,121 @@ class BackupResource extends Resource return null; } - return $count >= $limit ? 'danger' - : ($count >= $limit * self::WARNING_THRESHOLD ? 'warning' : 'success'); + return $count >= $limit ? 'danger' : ($count >= $limit * self::WARNING_THRESHOLD ? 'warning' : 'success'); + } + + public static function form(Form $form): Form + { + return $form + ->schema([ + TextInput::make('name') + ->label('Name') + ->columnSpanFull(), + TextArea::make('ignored') + ->columnSpanFull() + ->label('Ignored Files & Directories'), + Toggle::make('is_locked') + ->label('Lock?') + ->helperText('Prevents this backup from being deleted until explicitly unlocked.'), + ]); + } + + public static function table(Table $table): Table + { + /** @var Server $server */ + $server = Filament::getTenant(); + + return $table + ->columns([ + TextColumn::make('name') + ->searchable(), + BytesColumn::make('bytes') + ->label('Size'), + DateTimeColumn::make('created_at') + ->label('Created') + ->since() + ->sortable(), + TextColumn::make('status') + ->label('Status') + ->badge(), + IconColumn::make('is_locked') + ->visibleFrom('md') + ->label('Lock Status') + ->trueIcon('tabler-lock') + ->falseIcon('tabler-lock-open'), + ]) + ->actions([ + ActionGroup::make([ + Action::make('lock') + ->icon(fn (Backup $backup) => !$backup->is_locked ? 'tabler-lock' : 'tabler-lock-open') + ->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_DELETE, $server)) + ->label(fn (Backup $backup) => !$backup->is_locked ? 'Lock' : 'Unlock') + ->action(fn (BackupController $backupController, Backup $backup, Request $request) => $backupController->toggleLock($request, $server, $backup)) + ->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful), + Action::make('download') + ->color('primary') + ->icon('tabler-download') + ->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_DOWNLOAD, $server)) + ->url(fn (DownloadLinkService $downloadLinkService, Backup $backup, Request $request) => $downloadLinkService->handle($backup, $request->user()), true) + ->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful), + Action::make('restore') + ->color('success') + ->icon('tabler-folder-up') + ->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_RESTORE, $server)) + ->form([ + Placeholder::make('') + ->helperText('Your server will be stopped. You will not be able to control the power state, access the file manager, or create additional backups until this process is completed.'), + Checkbox::make('truncate') + ->label('Delete all files before restoring backup?'), + ]) + ->action(function (Backup $backup, $data, DaemonBackupRepository $daemonRepository, DownloadLinkService $downloadLinkService) use ($server) { + if (!is_null($server->status)) { + return Notification::make() + ->danger() + ->title('Backup Restore Failed') + ->body('This server is not currently in a state that allows for a backup to be restored.') + ->send(); + } + + if (!$backup->is_successful && is_null($backup->completed_at)) { //TODO Change to Notifications + return Notification::make() + ->danger() + ->title('Backup Restore Failed') + ->body('This backup cannot be restored at this time: not completed or failed.') + ->send(); + } + + $log = Activity::event('server:backup.restore') + ->subject($backup) + ->property(['name' => $backup->name, 'truncate' => $data['truncate']]); + + $log->transaction(function () use ($downloadLinkService, $daemonRepository, $backup, $server, $data) { + // If the backup is for an S3 file we need to generate a unique Download link for + // it that will allow daemon to actually access the file. + if ($backup->disk === Backup::ADAPTER_AWS_S3) { + $url = $downloadLinkService->handle($backup, auth()->user()); + } + + // Update the status right away for the server so that we know not to allow certain + // actions against it via the Panel API. + $server->update(['status' => ServerState::RestoringBackup]); + + $daemonRepository->setServer($server)->restore($backup, $url ?? null, $data['truncate']); + }); + + return Notification::make() + ->title('Restoring Backup') + ->send(); + }) + ->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful), + DeleteAction::make('delete') + ->disabled(fn (Backup $backup) => $backup->is_locked) + ->modalDescription(fn (Backup $backup) => 'Do you wish to delete, ' . $backup->name . '?') + ->modalSubmitActionLabel('Delete Backup') + ->action(fn (BackupController $backupController, Backup $backup, Request $request) => $backupController->delete($request, $server, $backup)) + ->visible(fn (Backup $backup) => $backup->status !== BackupStatus::InProgress), + ]), + ]); } // TODO: find better way handle server conflict state diff --git a/app/Filament/Server/Resources/BackupResource/Pages/ListBackups.php b/app/Filament/Server/Resources/BackupResource/Pages/ListBackups.php index b6a9d3a6c..abc5c70cc 100644 --- a/app/Filament/Server/Resources/BackupResource/Pages/ListBackups.php +++ b/app/Filament/Server/Resources/BackupResource/Pages/ListBackups.php @@ -2,165 +2,28 @@ namespace App\Filament\Server\Resources\BackupResource\Pages; -use App\Enums\BackupStatus; -use App\Enums\ServerState; use App\Facades\Activity; use App\Filament\Server\Resources\BackupResource; -use App\Http\Controllers\Api\Client\Servers\BackupController; -use App\Models\Backup; use App\Models\Permission; use App\Models\Server; -use App\Repositories\Daemon\DaemonBackupRepository; -use App\Services\Backups\DownloadLinkService; use App\Services\Backups\InitiateBackupService; -use App\Filament\Components\Tables\Columns\BytesColumn; -use App\Filament\Components\Tables\Columns\DateTimeColumn; -use Filament\Actions; +use Filament\Actions\CreateAction; use Filament\Facades\Filament; -use Filament\Forms\Components\Checkbox; -use Filament\Forms\Components\Placeholder; -use Filament\Forms\Components\Textarea; -use Filament\Forms\Components\TextInput; -use Filament\Forms\Components\Toggle; -use Filament\Forms\Form; use Filament\Notifications\Notification; use Filament\Resources\Pages\ListRecords; -use Filament\Tables\Actions\Action; -use Filament\Tables\Actions\ActionGroup; -use Filament\Tables\Actions\DeleteAction; -use Filament\Tables\Columns\IconColumn; -use Filament\Tables\Columns\TextColumn; -use Filament\Tables\Table; -use Illuminate\Http\Request; use Symfony\Component\HttpKernel\Exception\HttpException; class ListBackups extends ListRecords { protected static string $resource = BackupResource::class; - protected static bool $canCreateAnother = false; - - public function form(Form $form): Form - { - return $form - ->schema([ - TextInput::make('name') - ->label('Name') - ->columnSpanFull(), - TextArea::make('ignored') - ->columnSpanFull() - ->label('Ignored Files & Directories'), - Toggle::make('is_locked') - ->label('Lock?') - ->helperText('Prevents this backup from being deleted until explicitly unlocked.'), - ]); - } - - public function table(Table $table): Table - { - /** @var Server $server */ - $server = Filament::getTenant(); - - return $table - ->columns([ - TextColumn::make('name') - ->searchable(), - BytesColumn::make('bytes') - ->label('Size'), - DateTimeColumn::make('created_at') - ->label('Created') - ->since() - ->sortable(), - TextColumn::make('status') - ->label('Status') - ->badge(), - IconColumn::make('is_locked') - ->visibleFrom('md') - ->label('Lock Status') - ->trueIcon('tabler-lock') - ->falseIcon('tabler-lock-open'), - ]) - ->actions([ - ActionGroup::make([ - Action::make('lock') - ->icon(fn (Backup $backup) => !$backup->is_locked ? 'tabler-lock' : 'tabler-lock-open') - ->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_DELETE, $server)) - ->label(fn (Backup $backup) => !$backup->is_locked ? 'Lock' : 'Unlock') - ->action(fn (BackupController $backupController, Backup $backup, Request $request) => $backupController->toggleLock($request, $server, $backup)) - ->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful), - Action::make('download') - ->color('primary') - ->icon('tabler-download') - ->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_DOWNLOAD, $server)) - ->url(fn (DownloadLinkService $downloadLinkService, Backup $backup, Request $request) => $downloadLinkService->handle($backup, $request->user()), true) - ->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful), - Action::make('restore') - ->color('success') - ->icon('tabler-folder-up') - ->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_RESTORE, $server)) - ->form([ - Placeholder::make('') - ->helperText('Your server will be stopped. You will not be able to control the power state, access the file manager, or create additional backups until this process is completed.'), - Checkbox::make('truncate') - ->label('Delete all files before restoring backup?'), - ]) - ->action(function (Backup $backup, $data, DaemonBackupRepository $daemonRepository, DownloadLinkService $downloadLinkService) use ($server) { - if (!is_null($server->status)) { - return Notification::make() - ->danger() - ->title('Backup Restore Failed') - ->body('This server is not currently in a state that allows for a backup to be restored.') - ->send(); - } - - if (!$backup->is_successful && is_null($backup->completed_at)) { //TODO Change to Notifications - return Notification::make() - ->danger() - ->title('Backup Restore Failed') - ->body('This backup cannot be restored at this time: not completed or failed.') - ->send(); - } - - $log = Activity::event('server:backup.restore') - ->subject($backup) - ->property(['name' => $backup->name, 'truncate' => $data['truncate']]); - - $log->transaction(function () use ($downloadLinkService, $daemonRepository, $backup, $server, $data) { - // If the backup is for an S3 file we need to generate a unique Download link for - // it that will allow daemon to actually access the file. - if ($backup->disk === Backup::ADAPTER_AWS_S3) { - $url = $downloadLinkService->handle($backup, auth()->user()); - } - - // Update the status right away for the server so that we know not to allow certain - // actions against it via the Panel API. - $server->update(['status' => ServerState::RestoringBackup]); - - $daemonRepository->setServer($server)->restore($backup, $url ?? null, $data['truncate']); - }); - - return Notification::make() - ->title('Restoring Backup') - ->send(); - }) - ->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful), - DeleteAction::make('delete') - ->disabled(fn (Backup $backup) => $backup->is_locked) - ->modalDescription(fn (Backup $backup) => 'Do you wish to delete, ' . $backup->name . '?') - ->modalSubmitActionLabel('Delete Backup') - ->action(fn (BackupController $backupController, Backup $backup, Request $request) => $backupController->delete($request, $server, $backup)) - ->visible(fn (Backup $backup) => $backup->status !== BackupStatus::InProgress), - ]), - ]); - } - protected function getHeaderActions(): array { /** @var Server $server */ $server = Filament::getTenant(); return [ - Actions\CreateAction::make() + CreateAction::make() ->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_CREATE, $server)) ->label(fn () => $server->backups()->count() >= $server->backup_limit ? 'Backup limit reached' : 'Create Backup') ->disabled(fn () => $server->backups()->count() >= $server->backup_limit) diff --git a/app/Filament/Server/Resources/DatabaseResource.php b/app/Filament/Server/Resources/DatabaseResource.php index 39f339244..4a89a2598 100644 --- a/app/Filament/Server/Resources/DatabaseResource.php +++ b/app/Filament/Server/Resources/DatabaseResource.php @@ -2,13 +2,23 @@ namespace App\Filament\Server\Resources; +use App\Filament\Components\Forms\Actions\RotateDatabasePasswordAction; +use App\Filament\Components\Tables\Columns\DateTimeColumn; use App\Filament\Server\Resources\DatabaseResource\Pages; use App\Models\Database; use App\Models\Permission; use App\Models\Server; +use App\Services\Databases\DatabaseManagementService; use Filament\Facades\Filament; +use Filament\Forms\Components\TextInput; +use Filament\Forms\Form; use Filament\Resources\Resource; +use Filament\Tables\Actions\DeleteAction; +use Filament\Tables\Actions\ViewAction; +use Filament\Tables\Columns\TextColumn; +use Filament\Tables\Table; use Illuminate\Database\Eloquent\Model; +use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction; class DatabaseResource extends Resource { @@ -42,9 +52,65 @@ class DatabaseResource extends Resource return null; } - return $count >= $limit - ? 'danger' - : ($count >= $limit * self::WARNING_THRESHOLD ? 'warning' : 'success'); + return $count >= $limit ? 'danger' : ($count >= $limit * self::WARNING_THRESHOLD ? 'warning' : 'success'); + } + + public static function form(Form $form): Form + { + /** @var Server $server */ + $server = Filament::getTenant(); + + return $form + ->schema([ + TextInput::make('host') + ->formatStateUsing(fn (Database $database) => $database->address()) + ->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null), + TextInput::make('database') + ->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null), + TextInput::make('username') + ->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null), + TextInput::make('password') + ->password()->revealable() + ->hidden(fn () => !auth()->user()->can(Permission::ACTION_DATABASE_VIEW_PASSWORD, $server)) + ->hintAction( + RotateDatabasePasswordAction::make() + ->authorize(fn () => auth()->user()->can(Permission::ACTION_DATABASE_UPDATE, $server)) + ) + ->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null) + ->formatStateUsing(fn (Database $database) => $database->password), + TextInput::make('remote') + ->label('Connections From'), + TextInput::make('max_connections') + ->formatStateUsing(fn (Database $database) => $database->max_connections === 0 ? $database->max_connections : 'Unlimited'), + TextInput::make('jdbc') + ->label('JDBC Connection String') + ->password()->revealable() + ->hidden(!auth()->user()->can(Permission::ACTION_DATABASE_VIEW_PASSWORD, $server)) + ->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null) + ->columnSpanFull() + ->formatStateUsing(fn (Database $database) => $database->jdbc), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + TextColumn::make('host') + ->state(fn (Database $database) => $database->address()) + ->badge(), + TextColumn::make('database'), + TextColumn::make('username'), + TextColumn::make('remote'), + DateTimeColumn::make('created_at') + ->sortable(), + ]) + ->actions([ + ViewAction::make() + ->modalHeading(fn (Database $database) => 'Viewing ' . $database->database), + DeleteAction::make() + ->using(fn (Database $database, DatabaseManagementService $service) => $service->delete($database)), + ]); } // TODO: find better way handle server conflict state diff --git a/app/Filament/Server/Resources/DatabaseResource/Pages/ListDatabases.php b/app/Filament/Server/Resources/DatabaseResource/Pages/ListDatabases.php index 3c7eda0aa..fbb1bd42b 100644 --- a/app/Filament/Server/Resources/DatabaseResource/Pages/ListDatabases.php +++ b/app/Filament/Server/Resources/DatabaseResource/Pages/ListDatabases.php @@ -2,12 +2,8 @@ namespace App\Filament\Server\Resources\DatabaseResource\Pages; -use App\Filament\Components\Forms\Actions\RotateDatabasePasswordAction; -use App\Filament\Components\Tables\Columns\DateTimeColumn; use App\Filament\Server\Resources\DatabaseResource; -use App\Models\Database; use App\Models\DatabaseHost; -use App\Models\Permission; use App\Models\Server; use App\Services\Databases\DatabaseManagementService; use Filament\Actions\CreateAction; @@ -15,76 +11,12 @@ use Filament\Facades\Filament; use Filament\Forms\Components\Grid; use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; -use Filament\Forms\Form; use Filament\Resources\Pages\ListRecords; -use Filament\Tables\Actions\DeleteAction; -use Filament\Tables\Actions\ViewAction; -use Filament\Tables\Columns\TextColumn; -use Filament\Tables\Table; -use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction; class ListDatabases extends ListRecords { protected static string $resource = DatabaseResource::class; - public function form(Form $form): Form - { - /** @var Server $server */ - $server = Filament::getTenant(); - - return $form - ->schema([ - TextInput::make('host') - ->formatStateUsing(fn (Database $database) => $database->address()) - ->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null), - TextInput::make('database') - ->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null), - TextInput::make('username') - ->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null), - TextInput::make('password') - ->password()->revealable() - ->hidden(fn () => !auth()->user()->can(Permission::ACTION_DATABASE_VIEW_PASSWORD, $server)) - ->hintAction( - RotateDatabasePasswordAction::make() - ->authorize(fn () => auth()->user()->can(Permission::ACTION_DATABASE_UPDATE, $server)) - ) - ->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null) - ->formatStateUsing(fn (Database $database) => $database->password), - TextInput::make('remote') - ->label('Connections From'), - TextInput::make('max_connections') - ->formatStateUsing(fn (Database $database) => $database->max_connections === 0 ? $database->max_connections : 'Unlimited'), - TextInput::make('jdbc') - ->label('JDBC Connection String') - ->password()->revealable() - ->hidden(!auth()->user()->can(Permission::ACTION_DATABASE_VIEW_PASSWORD, $server)) - ->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null) - ->columnSpanFull() - ->formatStateUsing(fn (Database $database) => $database->jdbc), - ]); - } - - public function table(Table $table): Table - { - return $table - ->columns([ - TextColumn::make('host') - ->state(fn (Database $database) => $database->address()) - ->badge(), - TextColumn::make('database'), - TextColumn::make('username'), - TextColumn::make('remote'), - DateTimeColumn::make('created_at') - ->sortable(), - ]) - ->actions([ - ViewAction::make() - ->modalHeading(fn (Database $database) => 'Viewing ' . $database->database), - DeleteAction::make() - ->using(fn (Database $database, DatabaseManagementService $service) => $service->delete($database)), - ]); - } - protected function getHeaderActions(): array { /** @var Server $server */ diff --git a/app/Filament/Server/Resources/ScheduleResource.php b/app/Filament/Server/Resources/ScheduleResource.php index 0e07402d7..53f7160ac 100644 --- a/app/Filament/Server/Resources/ScheduleResource.php +++ b/app/Filament/Server/Resources/ScheduleResource.php @@ -2,6 +2,8 @@ namespace App\Filament\Server\Resources; +use App\Facades\Activity; +use App\Filament\Components\Tables\Columns\DateTimeColumn; use App\Filament\Server\Resources\ScheduleResource\Pages; use App\Filament\Server\Resources\ScheduleResource\RelationManagers\TasksRelationManager; use App\Helpers\Utilities; @@ -23,6 +25,12 @@ use Filament\Forms\Set; use Filament\Notifications\Notification; use Filament\Resources\Resource; use Filament\Support\Exceptions\Halt; +use Filament\Tables\Actions\DeleteAction; +use Filament\Tables\Actions\EditAction; +use Filament\Tables\Actions\ViewAction; +use Filament\Tables\Columns\IconColumn; +use Filament\Tables\Columns\TextColumn; +use Filament\Tables\Table; use Illuminate\Database\Eloquent\Model; class ScheduleResource extends Resource @@ -303,6 +311,44 @@ class ScheduleResource extends Resource ]); } + public static function table(Table $table): Table + { + return $table + ->columns([ + TextColumn::make('name') + ->searchable(), + TextColumn::make('cron') + ->state(fn (Schedule $schedule) => $schedule->cron_minute . ' ' . $schedule->cron_hour . ' ' . $schedule->cron_day_of_month . ' ' . $schedule->cron_month . ' ' . $schedule->cron_day_of_week), + TextColumn::make('status') + ->state(fn (Schedule $schedule) => !$schedule->is_active ? 'Inactive' : ($schedule->is_processing ? 'Processing' : 'Active')), + IconColumn::make('only_when_online') + ->boolean() + ->sortable(), + DateTimeColumn::make('last_run_at') + ->label('Last run') + ->placeholder('Never') + ->since() + ->sortable(), + DateTimeColumn::make('next_run_at') + ->label('Next run') + ->placeholder('Never') + ->since() + ->sortable() + ->state(fn (Schedule $schedule) => $schedule->is_active ? $schedule->next_run_at : null), + ]) + ->actions([ + ViewAction::make(), + EditAction::make(), + DeleteAction::make() + ->after(function (Schedule $schedule) { + Activity::event('server:schedule.delete') + ->subject($schedule) + ->property('name', $schedule->name) + ->log(); + }), + ]); + } + public static function getRelations(): array { return [ diff --git a/app/Filament/Server/Resources/ScheduleResource/Pages/ListSchedules.php b/app/Filament/Server/Resources/ScheduleResource/Pages/ListSchedules.php index 0f79e8a40..398f5cd93 100644 --- a/app/Filament/Server/Resources/ScheduleResource/Pages/ListSchedules.php +++ b/app/Filament/Server/Resources/ScheduleResource/Pages/ListSchedules.php @@ -2,65 +2,19 @@ namespace App\Filament\Server\Resources\ScheduleResource\Pages; -use App\Facades\Activity; use App\Filament\Server\Resources\ScheduleResource; -use App\Models\Schedule; -use App\Filament\Components\Tables\Columns\DateTimeColumn; -use Filament\Actions; +use Filament\Actions\CreateAction; use Filament\Resources\Pages\ListRecords; -use Filament\Tables\Actions\DeleteAction; -use Filament\Tables\Actions\EditAction; -use Filament\Tables\Actions\ViewAction; -use Filament\Tables\Columns\IconColumn; -use Filament\Tables\Columns\TextColumn; -use Filament\Tables\Table; class ListSchedules extends ListRecords { protected static string $resource = ScheduleResource::class; - public function table(Table $table): Table - { - return $table - ->columns([ - TextColumn::make('name') - ->searchable(), - TextColumn::make('cron') - ->state(fn (Schedule $schedule) => $schedule->cron_minute . ' ' . $schedule->cron_hour . ' ' . $schedule->cron_day_of_month . ' ' . $schedule->cron_month . ' ' . $schedule->cron_day_of_week), - TextColumn::make('status') - ->state(fn (Schedule $schedule) => !$schedule->is_active ? 'Inactive' : ($schedule->is_processing ? 'Processing' : 'Active')), - IconColumn::make('only_when_online') - ->boolean() - ->sortable(), - DateTimeColumn::make('last_run_at') - ->label('Last run') - ->placeholder('Never') - ->since() - ->sortable(), - DateTimeColumn::make('next_run_at') - ->label('Next run') - ->placeholder('Never') - ->since() - ->sortable() - ->state(fn (Schedule $schedule) => $schedule->is_active ? $schedule->next_run_at : null), - ]) - ->actions([ - ViewAction::make(), - EditAction::make(), - DeleteAction::make() - ->after(function (Schedule $schedule) { - Activity::event('server:schedule.delete') - ->subject($schedule) - ->property('name', $schedule->name) - ->log(); - }), - ]); - } - protected function getHeaderActions(): array { return [ - Actions\CreateAction::make()->label('New Schedule'), + CreateAction::make() + ->label('New Schedule'), ]; } diff --git a/app/Filament/Server/Resources/ScheduleResource/Pages/ViewSchedule.php b/app/Filament/Server/Resources/ScheduleResource/Pages/ViewSchedule.php index a9dd654ea..2ce565da0 100644 --- a/app/Filament/Server/Resources/ScheduleResource/Pages/ViewSchedule.php +++ b/app/Filament/Server/Resources/ScheduleResource/Pages/ViewSchedule.php @@ -7,7 +7,8 @@ use App\Filament\Server\Resources\ScheduleResource; use App\Models\Permission; use App\Models\Schedule; use App\Services\Schedules\ProcessScheduleService; -use Filament\Actions; +use Filament\Actions\Action; +use Filament\Actions\EditAction; use Filament\Facades\Filament; use Filament\Resources\Pages\ViewRecord; @@ -18,7 +19,7 @@ class ViewSchedule extends ViewRecord protected function getHeaderActions(): array { return [ - Actions\Action::make('runNow') + Action::make('runNow') ->authorize(fn () => auth()->user()->can(Permission::ACTION_SCHEDULE_UPDATE, Filament::getTenant())) ->label(fn (Schedule $schedule) => $schedule->tasks->count() === 0 ? 'No tasks' : ($schedule->is_processing ? 'Processing' : 'Run now')) ->color(fn (Schedule $schedule) => $schedule->tasks->count() === 0 || $schedule->is_processing ? 'warning' : 'primary') @@ -33,7 +34,7 @@ class ViewSchedule extends ViewRecord $this->fillForm(); }), - Actions\EditAction::make(), + EditAction::make(), ]; }