diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index b00b810b4..da5f6ae55 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -35,7 +35,7 @@ jobs: strategy: fail-fast: false matrix: - php: [8.2, 8.3, 8.4] + php: [ 8.2, 8.3, 8.4 ] steps: - name: Code Checkout uses: actions/checkout@v4 @@ -68,4 +68,4 @@ jobs: run: composer install --no-interaction --no-suggest --no-progress --no-scripts - name: PHPStan - run: vendor/bin/phpstan --memory-limit=-1 \ No newline at end of file + run: vendor/bin/phpstan --memory-limit=-1 --error-format=github diff --git a/.gitignore b/.gitignore index bb60b3369..85ae1a0e8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ /.phpunit.cache /node_modules /public/build -/public/hot /public/storage /storage/*.key /storage/pail diff --git a/Dockerfile b/Dockerfile index f07367734..ccc06a934 100644 --- a/Dockerfile +++ b/Dockerfile @@ -75,14 +75,15 @@ RUN chown root:www-data ./ \ && chmod 750 ./ \ # Files should not have execute set, but directories need it && find ./ -type d -exec chmod 750 {} \; \ - # Create necessary directories + # Create necessary directories && mkdir -p /pelican-data/storage /var/www/html/storage/app/public /var/run/supervisord /etc/supercronic \ # Symlinks for env, database, and avatars && ln -s /pelican-data/.env ./.env \ && ln -s /pelican-data/database/database.sqlite ./database/database.sqlite \ && ln -sf /var/www/html/storage/app/public /var/www/html/public/storage \ && ln -s /pelican-data/storage/avatars /var/www/html/storage/app/public/avatars \ - # Allow www-data write permissions where necessary + && ln -s /pelican-data/storage/fonts /var/www/html/storage/app/public/fonts \ + # Allow www-data write permissions where necessary && chown -R www-data:www-data /pelican-data ./storage ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \ && chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord diff --git a/Dockerfile.dev b/Dockerfile.dev index 078e107c9..797576e25 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -79,14 +79,15 @@ RUN chown root:www-data ./ \ && chmod 750 ./ \ # Files should not have execute set, but directories need it && find ./ -type d -exec chmod 750 {} \; \ - # Create necessary directories + # Create necessary directories && mkdir -p /pelican-data/storage /var/www/html/storage/app/public /var/run/supervisord /etc/supercronic \ # Symlinks for env, database, and avatars && ln -s /pelican-data/.env ./.env \ && ln -s /pelican-data/database/database.sqlite ./database/database.sqlite \ && ln -sf /var/www/html/storage/app/public /var/www/html/public/storage \ && ln -s /pelican-data/storage/avatars /var/www/html/storage/app/public/avatars \ - # Allow www-data write permissions where necessary + && ln -s /pelican-data/storage/fonts /var/www/html/storage/app/public/fonts \ + # Allow www-data write permissions where necessary && chown -R www-data:www-data /pelican-data ./storage ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \ && chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord diff --git a/app/Console/Commands/Environment/QueueWorkerServiceCommand.php b/app/Console/Commands/Environment/QueueWorkerServiceCommand.php index 92ea88e75..23fdff40a 100644 --- a/app/Console/Commands/Environment/QueueWorkerServiceCommand.php +++ b/app/Console/Commands/Environment/QueueWorkerServiceCommand.php @@ -18,6 +18,17 @@ class QueueWorkerServiceCommand extends Command public function handle(): void { + if (@file_exists('/.dockerenv')) { + $result = Process::run('supervisorctl restart queue-worker'); + if ($result->failed()) { + $this->error('Error restarting service: ' . $result->errorOutput()); + + return; + } + $this->line('Queue worker service file updated successfully.'); + + return; + } $serviceName = $this->option('service-name') ?? $this->ask('Queue worker service name', 'pelican-queue'); $path = '/etc/systemd/system/' . $serviceName . '.service'; diff --git a/app/Console/Commands/Node/MakeNodeCommand.php b/app/Console/Commands/Node/MakeNodeCommand.php index 2d3678a79..48a657c6c 100644 --- a/app/Console/Commands/Node/MakeNodeCommand.php +++ b/app/Console/Commands/Node/MakeNodeCommand.php @@ -24,6 +24,7 @@ class MakeNodeCommand extends Command {--overallocateCpu= : Enter the amount of cpu to overallocate (% or -1 to overallocate the maximum).} {--uploadSize= : Enter the maximum upload filesize.} {--daemonListeningPort= : Enter the daemon listening port.} + {--daemonConnectingPort= : Enter the daemon connecting port.} {--daemonSFTPPort= : Enter the daemon SFTP listening port.} {--daemonSFTPAlias= : Enter the daemon SFTP alias.} {--daemonBase= : Enter the base folder.}'; @@ -57,6 +58,7 @@ class MakeNodeCommand extends Command $data['cpu_overallocate'] = $this->option('overallocateCpu') ?? $this->ask(trans('commands.make_node.cpu_overallocate'), '-1'); $data['upload_size'] = $this->option('uploadSize') ?? $this->ask(trans('commands.make_node.upload_size'), '256'); $data['daemon_listen'] = $this->option('daemonListeningPort') ?? $this->ask(trans('commands.make_node.daemonListen'), '8080'); + $data['daemon_connect'] = $this->option('daemonConnectingPort') ?? $this->ask(trans('commands.make_node.daemonConnect'), '8080'); $data['daemon_sftp'] = $this->option('daemonSFTPPort') ?? $this->ask(trans('commands.make_node.daemonSFTP'), '2022'); $data['daemon_sftp_alias'] = $this->option('daemonSFTPAlias') ?? $this->ask(trans('commands.make_node.daemonSFTPAlias'), ''); $data['daemon_base'] = $this->option('daemonBase') ?? $this->ask(trans('commands.make_node.daemonBase'), '/var/lib/pelican/volumes'); diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 7545f177e..48fbe6b84 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -7,7 +7,6 @@ use App\Console\Commands\Maintenance\CleanServiceBackupFilesCommand; use App\Console\Commands\Maintenance\PruneImagesCommand; use App\Console\Commands\Maintenance\PruneOrphanedBackupsCommand; use App\Console\Commands\Schedule\ProcessRunnableCommand; -use App\Jobs\NodeStatistics; use App\Models\ActivityLog; use App\Models\Webhook; use Illuminate\Console\Scheduling\Schedule; @@ -31,8 +30,11 @@ class Kernel extends ConsoleKernel */ protected function schedule(Schedule $schedule): void { - // https://laravel.com/docs/10.x/upgrade#redis-cache-tags - $schedule->command('cache:prune-stale-tags')->hourly(); + if (config('cache.default') === 'redis') { + // https://laravel.com/docs/10.x/upgrade#redis-cache-tags + // This only needs to run when using redis. anything else throws an error. + $schedule->command('cache:prune-stale-tags')->hourly(); + } // Execute scheduled commands for servers every minute, as if there was a normal cron running. $schedule->command(ProcessRunnableCommand::class)->everyMinute()->withoutOverlapping(); @@ -41,8 +43,6 @@ class Kernel extends ConsoleKernel $schedule->command(PruneImagesCommand::class)->daily(); $schedule->command(CheckEggUpdatesCommand::class)->hourly(); - $schedule->job(new NodeStatistics())->everyFiveSeconds()->withoutOverlapping(); - if (config('backups.prune_age')) { // Every 30 minutes, run the backup pruning command so that any abandoned backups can be deleted. $schedule->command(PruneOrphanedBackupsCommand::class)->everyThirtyMinutes(); diff --git a/app/Enums/ContainerStatus.php b/app/Enums/ContainerStatus.php index f18ceed9d..cdfa41be6 100644 --- a/app/Enums/ContainerStatus.php +++ b/app/Enums/ContainerStatus.php @@ -88,7 +88,7 @@ enum ContainerStatus: string implements HasColor, HasIcon, HasLabel public function isStartable(): bool { - return !in_array($this, [ContainerStatus::Running, ContainerStatus::Starting, ContainerStatus::Stopping, ContainerStatus::Restarting]); + return !in_array($this, [ContainerStatus::Running, ContainerStatus::Starting, ContainerStatus::Stopping, ContainerStatus::Restarting, ContainerStatus::Missing]); } public function isRestartable(): bool @@ -97,18 +97,16 @@ enum ContainerStatus: string implements HasColor, HasIcon, HasLabel return true; } - return !in_array($this, [ContainerStatus::Offline]); + return !in_array($this, [ContainerStatus::Offline, ContainerStatus::Missing]); } public function isStoppable(): bool { - return !in_array($this, [ContainerStatus::Starting, ContainerStatus::Stopping, ContainerStatus::Restarting, ContainerStatus::Exited, ContainerStatus::Offline]); + return !in_array($this, [ContainerStatus::Starting, ContainerStatus::Stopping, ContainerStatus::Restarting, ContainerStatus::Exited, ContainerStatus::Offline, ContainerStatus::Missing]); } public function isKillable(): bool { - // [ContainerStatus::Restarting, ContainerStatus::Removing, ContainerStatus::Dead, ContainerStatus::Created] - - return !in_array($this, [ContainerStatus::Offline, ContainerStatus::Running, ContainerStatus::Exited]); + return !in_array($this, [ContainerStatus::Offline, ContainerStatus::Running, ContainerStatus::Exited, ContainerStatus::Missing]); } } diff --git a/app/Enums/HeaderActionPosition.php b/app/Enums/HeaderActionPosition.php new file mode 100644 index 000000000..8fd2db593 --- /dev/null +++ b/app/Enums/HeaderActionPosition.php @@ -0,0 +1,9 @@ + '#2563EB', + self::Suspended => '#D97706', + self::InstallFailed, self::ReinstallFailed => '#EF4444', + }; + } + return match ($this) { self::Normal => 'primary', self::Installing => 'primary', diff --git a/app/Events/Auth/DirectLogin.php b/app/Events/Auth/DirectLogin.php deleted file mode 100644 index 41643edb6..000000000 --- a/app/Events/Auth/DirectLogin.php +++ /dev/null @@ -1,11 +0,0 @@ -serverVariables()->where('env_variable', 'STEAM_ACC')->first(); + $serverVariable = $server->serverVariables()->whereHas('variable', function (Builder $query) { + $query->where('env_variable', 'STEAM_ACC'); + })->first(); return Action::make($this->getId()) ->requiresConfirmation() ->modalHeading('Invalid GSL token') ->modalDescription('It seems like your Gameserver Login Token (GSL token) is invalid or has expired.') ->modalSubmitActionLabel('Update GSL Token') - ->disabledSchema(fn () => !auth()->user()->can(Permission::ACTION_STARTUP_UPDATE, $server)) - ->schema([ - TextEntry::make('java') - ->label('You can either generate a new one and enter it below or leave the field blank to remove it - completely.'), + ->disabledForm(fn () => !auth()->user()->can(Permission::ACTION_STARTUP_UPDATE, $server)) + ->form([ + Placeholder::make('info') + ->label(new HtmlString(Blade::render('You can either generate a new one and enter it below or leave the field blank to remove it completely.'))), TextInput::make('gsltoken') ->label('GSL Token') ->rules([ @@ -105,13 +109,13 @@ class GSLToken extends FeatureProvider Notification::make() ->title('GSL Token updated') - ->body('Restart the server to use the new token.') + ->body('Server will restart now.') ->success() ->send(); - } catch (\Exception $e) { + } catch (Exception $exception) { Notification::make() - ->title('Error') - ->body($e->getMessage()) + ->title('Could not update GSL Token') + ->body($exception->getMessage()) ->danger() ->send(); } diff --git a/app/Extensions/Features/JavaVersion.php b/app/Extensions/Features/JavaVersion.php index 4991e20a7..8f488e840 100644 --- a/app/Extensions/Features/JavaVersion.php +++ b/app/Extensions/Features/JavaVersion.php @@ -6,6 +6,7 @@ use App\Facades\Activity; use App\Models\Permission; use App\Models\Server; use App\Repositories\Daemon\DaemonPowerRepository; +use Exception; use Filament\Actions\Action; use Filament\Facades\Filament; use Filament\Forms\Components\Placeholder; @@ -25,10 +26,11 @@ class JavaVersion extends FeatureProvider { return [ 'java.lang.UnsupportedClassVersionError', - 'minecraft 1.17 requires running the server with java 16 or above', - 'minecraft 1.18 requires running the server with java 17 or above', 'unsupported major.minor version', 'has been compiled by a more recent version of the java runtime', + 'minecraft 1.17 requires running the server with java 16 or above', + 'minecraft 1.18 requires running the server with java 17 or above', + 'minecraft 1.19 requires running the server with java 17 or above', ]; } @@ -73,17 +75,18 @@ class JavaVersion extends FeatureProvider ->property(['old' => $original, 'new' => $new]) ->log(); } + $powerRepository->setServer($server)->send('restart'); Notification::make() ->title('Docker image updated') - ->body('Restart the server to use the new image.') + ->body('Server will restart now.') ->success() ->send(); - } catch (\Exception $e) { + } catch (Exception $exception) { Notification::make() - ->title('Error') - ->body($e->getMessage()) + ->title('Could not update docker image') + ->body($exception->getMessage()) ->danger() ->send(); } diff --git a/app/Extensions/Features/MinecraftEula.php b/app/Extensions/Features/MinecraftEula.php index 7627f9fc3..9b4ccccd1 100644 --- a/app/Extensions/Features/MinecraftEula.php +++ b/app/Extensions/Features/MinecraftEula.php @@ -24,7 +24,7 @@ class MinecraftEula extends FeatureProvider public function getListeners(): array { return [ - 'You need to agree to the EULA in order to run the server', + 'you need to agree to the eula in order to run the server', ]; } @@ -38,31 +38,30 @@ class MinecraftEula extends FeatureProvider return Action::make($this->getId()) ->requiresConfirmation() ->modalHeading('Minecraft EULA') - ->modalDescription(new HtmlString(Blade::render('By pressing "I Accept" below you are indicating your agreement to the Minecraft EULA '))) + ->modalDescription(new HtmlString(Blade::render('By pressing "I Accept" below you are indicating your agreement to the Minecraft EULA .'))) ->modalSubmitActionLabel('I Accept') ->action(function (DaemonFileRepository $fileRepository, DaemonPowerRepository $powerRepository) { try { /** @var Server $server */ $server = Filament::getTenant(); - $content = $fileRepository->setServer($server)->getContent('eula.txt'); - $content = preg_replace('/(eula=)false/', '\1true', $content); - $fileRepository->setServer($server)->putContent('eula.txt', $content); + + $fileRepository->setServer($server)->putContent('eula.txt', 'eula=true'); + $powerRepository->setServer($server)->send('restart'); Notification::make() - ->title('Docker image updated') - ->body('Restart the server.') + ->title('Minecraft EULA accepted') + ->body('Server will restart now.') ->success() ->send(); - } catch (Exception $e) { + } catch (Exception $exception) { Notification::make() - ->title('Error') - ->body($e->getMessage()) + ->title('Could not accept Minecraft EULA') + ->body($exception->getMessage()) ->danger() ->send(); } - } - ); + }); } public static function register(Application $app): self diff --git a/app/Filament/Admin/Pages/Settings.php b/app/Filament/Admin/Pages/Settings.php index 47659c984..9d4d5c0cf 100644 --- a/app/Filament/Admin/Pages/Settings.php +++ b/app/Filament/Admin/Pages/Settings.php @@ -8,9 +8,15 @@ use App\Extensions\OAuth\Providers\OAuthProvider; use App\Models\Backup; use App\Notifications\MailTested; use App\Traits\EnvironmentWriterTrait; -use BackedEnum; +use App\Traits\Filament\CanCustomizeHeaderActions; +use App\Traits\Filament\CanCustomizeHeaderWidgets; use Exception; use Filament\Actions\Action; +use Filament\Actions\ActionGroup; +use Filament\Forms\Components\Actions; +use Filament\Forms\Components\Component; +use Filament\Forms\Components\FileUpload; +use Filament\Forms\Components\Group; use Filament\Forms\Components\Hidden; use Filament\Forms\Components\Select; use Filament\Forms\Components\TagsInput; @@ -31,8 +37,8 @@ use Filament\Schemas\Components\Tabs; use Filament\Schemas\Components\Tabs\Tab; use Filament\Schemas\Components\Utilities\Get; use Filament\Schemas\Components\Utilities\Set; -use Filament\Support\Enums\Width; use Illuminate\Http\Client\Factory; +use Illuminate\Support\Arr; use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Notification as MailNotification; use Illuminate\Support\Str; @@ -43,9 +49,12 @@ use Filament\Schemas\Contracts\HasSchemas; */ class Settings extends Page implements HasSchemas { + use CanCustomizeHeaderActions, InteractsWithHeaderActions { + CanCustomizeHeaderActions::getHeaderActions insteadof InteractsWithHeaderActions; + } + use CanCustomizeHeaderWidgets; use EnvironmentWriterTrait; use InteractsWithForms; - use InteractsWithHeaderActions; protected static string|\BackedEnum|null $navigationIcon = 'tabler-settings'; @@ -138,8 +147,7 @@ class Settings extends Page implements HasSchemas ->placeholder('/pelican.ico'), ]), Group::make() - ->columnSpan(2) - ->columns(4) + ->columns(2) ->schema([ Toggle::make('APP_DEBUG') ->label(trans('admin/setting.general.debug_mode')) @@ -159,6 +167,10 @@ class Settings extends Page implements HasSchemas ]) ->stateCast(new BooleanStateCast(false, true)) ->default(env('FILAMENT_TOP_NAVIGATION', config('panel.filament.top-navigation'))), + ]), + Group::make() + ->columns(2) + ->schema([ Select::make('FILAMENT_AVATAR_PROVIDER') ->label(trans('admin/setting.general.avatar_provider')) ->native(false) @@ -195,12 +207,18 @@ class Settings extends Page implements HasSchemas ->formatStateUsing(fn ($state): int => (int) $state) ->afterStateUpdated(fn ($state, Set $set) => $set('APP_2FA_REQUIRED', (int) $state)) ->default(env('APP_2FA_REQUIRED', config('panel.auth.2fa_required'))), + Select::make('FILAMENT_WIDTH') + ->label(trans('admin/setting.general.display_width')) + ->native(false) + ->options(MaxWidth::class) + ->selectablePlaceholder(false) + ->default(env('FILAMENT_WIDTH', config('panel.filament.display-width'))), TagsInput::make('TRUSTED_PROXIES') ->label(trans('admin/setting.general.trusted_proxies')) ->separator() ->splitKeys(['Tab', ' ']) ->placeholder(trans('admin/setting.general.trusted_proxies_help')) - ->default(env('TRUSTED_PROXIES', implode(',', config('trustedproxy.proxies')))) + ->default(env('TRUSTED_PROXIES', implode(',', Arr::wrap(config('trustedproxy.proxies'))))) ->hintActions([ Action::make('clear') ->label(trans('admin/setting.general.clear')) @@ -235,12 +253,6 @@ class Settings extends Page implements HasSchemas $set('TRUSTED_PROXIES', $ips->values()->all()); }), ]), - Select::make('FILAMENT_WIDTH') - ->label(trans('admin/setting.general.display_width')) - ->native(false) - ->options(Width::class) - ->selectablePlaceholder(false) - ->default(env('FILAMENT_WIDTH', config('panel.filament.display-width'))), ]; } @@ -635,8 +647,8 @@ class Settings extends Page implements HasSchemas ->onColor('success') ->offColor('danger') ->live() - ->columnSpanFull() - ->stateCast(new BooleanStateCast(false)) + ->formatStateUsing(fn ($state): bool => (bool) $state) + ->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_SEND_INSTALL_NOTIFICATION', (bool) $state)) ->default(env('PANEL_SEND_INSTALL_NOTIFICATION', config('panel.email.send_install_notification'))), Toggle::make('PANEL_SEND_REINSTALL_NOTIFICATION') ->label(trans('admin/setting.misc.mail_notifications.server_reinstalled')) @@ -645,8 +657,8 @@ class Settings extends Page implements HasSchemas ->onColor('success') ->offColor('danger') ->live() - ->columnSpanFull() - ->stateCast(new BooleanStateCast(false)) + ->formatStateUsing(fn ($state): bool => (bool) $state) + ->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_SEND_REINSTALL_NOTIFICATION', (bool) $state)) ->default(env('PANEL_SEND_REINSTALL_NOTIFICATION', config('panel.email.send_reinstall_notification'))), ]), Section::make(trans('admin/setting.misc.connections.title')) @@ -731,9 +743,17 @@ class Settings extends Page implements HasSchemas ->onColor('success') ->offColor('danger') ->live() - ->columnSpanFull() - ->stateCast(new BooleanStateCast(false)) + ->columnSpan(1) + ->formatStateUsing(fn ($state): bool => (bool) $state) + ->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_EDITABLE_SERVER_DESCRIPTIONS', (bool) $state)) ->default(env('PANEL_EDITABLE_SERVER_DESCRIPTIONS', config('panel.editable_server_descriptions'))), + FileUpload::make('ConsoleFonts') + ->hint(trans('admin/setting.misc.server.console_font_hint')) + ->label(trans('admin/setting.misc.server.console_font_upload')) + ->directory('fonts') + ->columnSpan(1) + ->maxFiles(1) + ->preserveFilenames(), ]), Section::make(trans('admin/setting.misc.webhook.title')) ->description(trans('admin/setting.misc.webhook.helper')) @@ -762,6 +782,7 @@ class Settings extends Page implements HasSchemas { try { $data = $this->form->getState(); + unset($data['ConsoleFonts']); $data = array_map(function ($value) { // Convert bools to a string, so they are correctly written to the .env file @@ -797,7 +818,8 @@ class Settings extends Page implements HasSchemas } } - protected function getHeaderActions(): array + /** @return array */ + protected function getDefaultHeaderActions(): array { return [ Action::make('save') diff --git a/app/Filament/Admin/Resources/ApiKeyResource.php b/app/Filament/Admin/Resources/ApiKeyResource.php index 26317f7e3..4ebb1f44e 100644 --- a/app/Filament/Admin/Resources/ApiKeyResource.php +++ b/app/Filament/Admin/Resources/ApiKeyResource.php @@ -6,11 +6,17 @@ use App\Filament\Admin\Resources\ApiKeyResource\Pages; use App\Filament\Admin\Resources\UserResource\Pages\EditUser; use App\Filament\Components\Tables\Columns\DateTimeColumn; use App\Models\ApiKey; +use App\Traits\Filament\CanCustomizePages; +use App\Traits\Filament\CanCustomizeRelations; +use App\Traits\Filament\CanModifyForm; +use App\Traits\Filament\CanModifyTable; +use Exception; use Filament\Actions\CreateAction; use Filament\Actions\DeleteAction; use Filament\Forms\Components\TagsInput; use Filament\Forms\Components\Textarea; use Filament\Forms\Components\ToggleButtons; +use Filament\Resources\Pages\PageRegistration; use Filament\Resources\Resource; use Filament\Schemas\Components\Fieldset; use Filament\Schemas\Components\Form; @@ -20,6 +26,11 @@ use Illuminate\Database\Eloquent\Builder; class ApiKeyResource extends Resource { + use CanCustomizePages; + use CanCustomizeRelations; + use CanModifyForm; + use CanModifyTable; + protected static ?string $model = ApiKey::class; protected static string|\BackedEnum|null $navigationIcon = 'tabler-key'; @@ -56,7 +67,10 @@ class ApiKeyResource extends Resource return trans('admin/dashboard.advanced'); } - public static function table(Table $table): Table + /** + * @throws Exception + */ + public static function defaultTable(Table $table): Table { return $table ->columns([ @@ -79,7 +93,7 @@ class ApiKeyResource extends Resource TextColumn::make('user.username') ->label(trans('admin/apikey.table.created_by')) ->icon('tabler-user') - ->url(fn (ApiKey $apiKey) => auth()->user()->can('update user', $apiKey->user) ? EditUser::getUrl(['record' => $apiKey->user]) : null), + ->url(fn (ApiKey $apiKey) => auth()->user()->can('update', $apiKey->user) ? EditUser::getUrl(['record' => $apiKey->user]) : null), ]) ->actions([ DeleteAction::make(), @@ -92,9 +106,12 @@ class ApiKeyResource extends Resource ]); } - public static function form(Form|\Filament\Schemas\Schema $schema): \Filament\Schemas\Schema + /** + * @throws Exception + */ + public static function form(Form|\Filament\Schemas\Schema $form): \Filament\Schemas\Schema { - return $schema + return $form ->schema([ Fieldset::make('Permissions') ->columns([ @@ -142,7 +159,8 @@ class ApiKeyResource extends Resource ]); } - public static function getPages(): array + /** @return array */ + public static function getDefaultPages(): array { return [ 'index' => Pages\ListApiKeys::route('/'), diff --git a/app/Filament/Admin/Resources/ApiKeyResource/Pages/CreateApiKey.php b/app/Filament/Admin/Resources/ApiKeyResource/Pages/CreateApiKey.php index 01a9fd4eb..351c577a2 100644 --- a/app/Filament/Admin/Resources/ApiKeyResource/Pages/CreateApiKey.php +++ b/app/Filament/Admin/Resources/ApiKeyResource/Pages/CreateApiKey.php @@ -4,16 +4,24 @@ namespace App\Filament\Admin\Resources\ApiKeyResource\Pages; use App\Filament\Admin\Resources\ApiKeyResource; use App\Models\ApiKey; +use App\Traits\Filament\CanCustomizeHeaderActions; +use App\Traits\Filament\CanCustomizeHeaderWidgets; +use Filament\Actions\Action; +use Filament\Actions\ActionGroup; use Filament\Resources\Pages\CreateRecord; use Illuminate\Database\Eloquent\Model; class CreateApiKey extends CreateRecord { + use CanCustomizeHeaderActions; + use CanCustomizeHeaderWidgets; + protected static string $resource = ApiKeyResource::class; protected static bool $canCreateAnother = false; - protected function getHeaderActions(): array + /** @return array */ + protected function getDefaultHeaderActions(): array { return [ $this->getCreateFormAction()->formId('form'), diff --git a/app/Filament/Admin/Resources/ApiKeyResource/Pages/ListApiKeys.php b/app/Filament/Admin/Resources/ApiKeyResource/Pages/ListApiKeys.php index a6814419a..e8a996c5e 100644 --- a/app/Filament/Admin/Resources/ApiKeyResource/Pages/ListApiKeys.php +++ b/app/Filament/Admin/Resources/ApiKeyResource/Pages/ListApiKeys.php @@ -4,14 +4,22 @@ namespace App\Filament\Admin\Resources\ApiKeyResource\Pages; use App\Filament\Admin\Resources\ApiKeyResource; use App\Models\ApiKey; +use App\Traits\Filament\CanCustomizeHeaderActions; +use App\Traits\Filament\CanCustomizeHeaderWidgets; +use Filament\Actions\Action; +use Filament\Actions\ActionGroup; use Filament\Actions\CreateAction; use Filament\Resources\Pages\ListRecords; class ListApiKeys extends ListRecords { + use CanCustomizeHeaderActions; + use CanCustomizeHeaderWidgets; + protected static string $resource = ApiKeyResource::class; - protected function getHeaderActions(): array + /** @return array */ + protected function getDefaultHeaderActions(): array { return [ CreateAction::make() diff --git a/app/Filament/Admin/Resources/DatabaseHostResource.php b/app/Filament/Admin/Resources/DatabaseHostResource.php index d4584226d..95d1c1caf 100644 --- a/app/Filament/Admin/Resources/DatabaseHostResource.php +++ b/app/Filament/Admin/Resources/DatabaseHostResource.php @@ -3,22 +3,35 @@ namespace App\Filament\Admin\Resources; use App\Filament\Admin\Resources\DatabaseHostResource\Pages; +use App\Filament\Admin\Resources\DatabaseHostResource\RelationManagers; use App\Models\DatabaseHost; +use App\Traits\Filament\CanCustomizePages; +use App\Traits\Filament\CanCustomizeRelations; +use App\Traits\Filament\CanModifyForm; +use App\Traits\Filament\CanModifyTable; +use Exception; use Filament\Actions\CreateAction; use Filament\Actions\DeleteBulkAction; use Filament\Actions\EditAction; use Filament\Actions\ViewAction; use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; +use Filament\Resources\Pages\PageRegistration; +use Filament\Resources\RelationManagers\RelationManager; use Filament\Resources\Resource; use Filament\Schemas\Components\Section; -use Filament\Schemas\Components\Utilities\Set; use Filament\Schemas\Schema; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; +use Illuminate\Database\Eloquent\Builder; class DatabaseHostResource extends Resource { + use CanCustomizePages; + use CanCustomizeRelations; + use CanModifyForm; + use CanModifyTable; + protected static ?string $model = DatabaseHost::class; protected static string|\BackedEnum|null $navigationIcon = 'tabler-database'; @@ -27,7 +40,7 @@ class DatabaseHostResource extends Resource public static function getNavigationBadge(): ?string { - return static::getModel()::count() ?: null; + return (string) static::getEloquentQuery()->count() ?: null; } public static function getNavigationLabel(): string @@ -50,7 +63,10 @@ class DatabaseHostResource extends Resource return trans('admin/dashboard.advanced'); } - public static function table(Table $table): Table + /** + * @throws Exception + */ + public static function defaultTable(Table $table): Table { return $table ->columns([ @@ -88,9 +104,12 @@ class DatabaseHostResource extends Resource ]); } - public static function form(Schema $schema): Schema + /** + * @throws Exception + */ + public static function form(Schema $form): Schema { - return $schema + return $form ->components([ Section::make() ->columnSpanFull() @@ -145,12 +164,21 @@ class DatabaseHostResource extends Resource ->preload() ->helperText(trans('admin/databasehost.linked_nodes_help')) ->label(trans('admin/databasehost.linked_nodes')) - ->relationship('nodes', 'name'), + ->relationship('nodes', 'name', fn (Builder $query) => $query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id'))), ]), ]); } - public static function getPages(): array + /** @return class-string[] */ + public static function getDefaultRelations(): array + { + return [ + RelationManagers\DatabasesRelationManager::class, + ]; + } + + /** @return array */ + public static function getDefaultPages(): array { return [ 'index' => Pages\ListDatabaseHosts::route('/'), @@ -159,4 +187,15 @@ class DatabaseHostResource extends Resource 'edit' => Pages\EditDatabaseHost::route('/{record}/edit'), ]; } + + public static function getEloquentQuery(): Builder + { + $query = parent::getEloquentQuery(); + + return $query->where(function (Builder $query) { + return $query->whereHas('nodes', function (Builder $query) { + $query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id')); + })->orDoesntHave('nodes'); + }); + } } diff --git a/app/Filament/Admin/Resources/DatabaseHostResource/Pages/CreateDatabaseHost.php b/app/Filament/Admin/Resources/DatabaseHostResource/Pages/CreateDatabaseHost.php index 892ce76e1..2ae64d26f 100644 --- a/app/Filament/Admin/Resources/DatabaseHostResource/Pages/CreateDatabaseHost.php +++ b/app/Filament/Admin/Resources/DatabaseHostResource/Pages/CreateDatabaseHost.php @@ -4,8 +4,9 @@ namespace App\Filament\Admin\Resources\DatabaseHostResource\Pages; use App\Filament\Admin\Resources\DatabaseHostResource; use App\Services\Databases\Hosts\HostCreationService; -use Exception; -use Filament\Schemas\Components\Fieldset; +use App\Traits\Filament\CanCustomizeHeaderActions; +use App\Traits\Filament\CanCustomizeHeaderWidgets; +use Filament\Forms\Components\Fieldset; use Filament\Forms\Components\Hidden; use Filament\Infolists\Components\TextEntry; use Filament\Forms\Components\Select; @@ -18,6 +19,7 @@ use Filament\Resources\Pages\CreateRecord; use Filament\Resources\Pages\CreateRecord\Concerns\HasWizard; use Filament\Schemas\Components\Wizard\Step; use Filament\Support\Exceptions\Halt; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\HtmlString; use Illuminate\Support\Str; @@ -26,6 +28,8 @@ use Throwable; class CreateDatabaseHost extends CreateRecord { + use CanCustomizeHeaderActions; + use CanCustomizeHeaderWidgets; use HasWizard; protected static string $resource = DatabaseHostResource::class; @@ -148,7 +152,7 @@ class CreateDatabaseHost extends CreateRecord ->preload() ->helperText(trans('admin/databasehost.linked_nodes_help')) ->label(trans('admin/databasehost.linked_nodes')) - ->relationship('nodes', 'name'), + ->relationship('nodes', 'name', fn (Builder $query) => $query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id'))), ]), ]; } diff --git a/app/Filament/Admin/Resources/DatabaseHostResource/Pages/EditDatabaseHost.php b/app/Filament/Admin/Resources/DatabaseHostResource/Pages/EditDatabaseHost.php index 782a28b71..2c5041ef5 100644 --- a/app/Filament/Admin/Resources/DatabaseHostResource/Pages/EditDatabaseHost.php +++ b/app/Filament/Admin/Resources/DatabaseHostResource/Pages/EditDatabaseHost.php @@ -3,19 +3,24 @@ namespace App\Filament\Admin\Resources\DatabaseHostResource\Pages; use App\Filament\Admin\Resources\DatabaseHostResource; -use App\Filament\Admin\Resources\DatabaseHostResource\RelationManagers\DatabasesRelationManager; use App\Models\DatabaseHost; use App\Services\Databases\Hosts\HostUpdateService; +use App\Traits\Filament\CanCustomizeHeaderActions; +use App\Traits\Filament\CanCustomizeHeaderWidgets; +use Filament\Actions\Action; +use Filament\Actions\ActionGroup; use Filament\Actions\DeleteAction; use Filament\Notifications\Notification; use Filament\Resources\Pages\EditRecord; use Filament\Support\Exceptions\Halt; use Illuminate\Database\Eloquent\Model; use PDOException; -use Throwable; class EditDatabaseHost extends EditRecord { + use CanCustomizeHeaderActions; + use CanCustomizeHeaderWidgets; + protected static string $resource = DatabaseHostResource::class; private HostUpdateService $hostUpdateService; @@ -25,7 +30,8 @@ class EditDatabaseHost extends EditRecord $this->hostUpdateService = $hostUpdateService; } - protected function getHeaderActions(): array + /** @return array */ + protected function getDefaultHeaderActions(): array { return [ DeleteAction::make() @@ -40,21 +46,6 @@ class EditDatabaseHost extends EditRecord return []; } - public function getRelationManagers(): array - { - if (DatabasesRelationManager::canViewForRecord($this->getRecord(), static::class)) { - return [ - DatabasesRelationManager::class, - ]; - } - - return []; - } - - /** - * @throws Halt - * @throws Throwable - */ protected function handleRecordUpdate(Model $record, array $data): Model { if (!$record instanceof DatabaseHost) { diff --git a/app/Filament/Admin/Resources/DatabaseHostResource/Pages/ListDatabaseHosts.php b/app/Filament/Admin/Resources/DatabaseHostResource/Pages/ListDatabaseHosts.php index 1c0f312d6..ccb1d3de6 100644 --- a/app/Filament/Admin/Resources/DatabaseHostResource/Pages/ListDatabaseHosts.php +++ b/app/Filament/Admin/Resources/DatabaseHostResource/Pages/ListDatabaseHosts.php @@ -4,14 +4,22 @@ namespace App\Filament\Admin\Resources\DatabaseHostResource\Pages; use App\Filament\Admin\Resources\DatabaseHostResource; use App\Models\DatabaseHost; +use App\Traits\Filament\CanCustomizeHeaderActions; +use App\Traits\Filament\CanCustomizeHeaderWidgets; +use Filament\Actions\Action; +use Filament\Actions\ActionGroup; use Filament\Actions\CreateAction; use Filament\Resources\Pages\ListRecords; class ListDatabaseHosts extends ListRecords { + use CanCustomizeHeaderActions; + use CanCustomizeHeaderWidgets; + protected static string $resource = DatabaseHostResource::class; - protected function getHeaderActions(): array + /** @return array */ + protected function getDefaultHeaderActions(): array { return [ CreateAction::make() diff --git a/app/Filament/Admin/Resources/DatabaseHostResource/Pages/ViewDatabaseHost.php b/app/Filament/Admin/Resources/DatabaseHostResource/Pages/ViewDatabaseHost.php index a32a89438..dcd2a74cd 100644 --- a/app/Filament/Admin/Resources/DatabaseHostResource/Pages/ViewDatabaseHost.php +++ b/app/Filament/Admin/Resources/DatabaseHostResource/Pages/ViewDatabaseHost.php @@ -3,29 +3,25 @@ namespace App\Filament\Admin\Resources\DatabaseHostResource\Pages; use App\Filament\Admin\Resources\DatabaseHostResource; -use App\Filament\Admin\Resources\DatabaseHostResource\RelationManagers\DatabasesRelationManager; +use App\Traits\Filament\CanCustomizeHeaderActions; +use App\Traits\Filament\CanCustomizeHeaderWidgets; +use Filament\Actions\Action; +use Filament\Actions\ActionGroup; use Filament\Actions\EditAction; use Filament\Resources\Pages\ViewRecord; class ViewDatabaseHost extends ViewRecord { + use CanCustomizeHeaderActions; + use CanCustomizeHeaderWidgets; + protected static string $resource = DatabaseHostResource::class; - protected function getHeaderActions(): array + /** @return array */ + protected function getDefaultHeaderActions(): array { return [ EditAction::make(), ]; } - - public function getRelationManagers(): array - { - if (DatabasesRelationManager::canViewForRecord($this->getRecord(), static::class)) { - return [ - DatabasesRelationManager::class, - ]; - } - - return []; - } } diff --git a/app/Filament/Admin/Resources/DatabaseHostResource/RelationManagers/DatabasesRelationManager.php b/app/Filament/Admin/Resources/DatabaseHostResource/RelationManagers/DatabasesRelationManager.php index 8cb2d5140..26255a34c 100644 --- a/app/Filament/Admin/Resources/DatabaseHostResource/RelationManagers/DatabasesRelationManager.php +++ b/app/Filament/Admin/Resources/DatabaseHostResource/RelationManagers/DatabasesRelationManager.php @@ -68,11 +68,11 @@ class DatabasesRelationManager extends RelationManager ->label(trans('admin/databasehost.table.created_at')), ]) ->actions([ - \Filament\Actions\DeleteAction::make() - ->authorize(fn (Database $database) => auth()->user()->can('delete database', $database)), - \Filament\Actions\ViewAction::make() + DeleteAction::make() + ->authorize(fn (Database $database) => auth()->user()->can('delete', $database)), + ViewAction::make() ->color('primary') - ->hidden(fn () => !auth()->user()->can('viewList database')), + ->hidden(fn () => !auth()->user()->can('viewAny', Database::class)), ]); } } diff --git a/app/Filament/Admin/Resources/EggResource.php b/app/Filament/Admin/Resources/EggResource.php index 65a0e3ba0..ce5edf297 100644 --- a/app/Filament/Admin/Resources/EggResource.php +++ b/app/Filament/Admin/Resources/EggResource.php @@ -3,11 +3,19 @@ namespace App\Filament\Admin\Resources; use App\Filament\Admin\Resources\EggResource\Pages; +use App\Filament\Admin\Resources\EggResource\RelationManagers; use App\Models\Egg; +use App\Traits\Filament\CanCustomizePages; +use App\Traits\Filament\CanCustomizeRelations; +use Filament\Resources\Pages\PageRegistration; +use Filament\Resources\RelationManagers\RelationManager; use Filament\Resources\Resource; class EggResource extends Resource { + use CanCustomizePages; + use CanCustomizeRelations; + protected static ?string $model = Egg::class; protected static string|\BackedEnum|null $navigationIcon = 'tabler-eggs'; @@ -44,7 +52,16 @@ class EggResource extends Resource return ['name', 'tags', 'uuid', 'id']; } - public static function getPages(): array + /** @return class-string[] */ + public static function getDefaultRelations(): array + { + return [ + RelationManagers\ServersRelationManager::class, + ]; + } + + /** @return array */ + public static function getDefaultPages(): array { return [ 'index' => Pages\ListEggs::route('/'), diff --git a/app/Filament/Admin/Resources/EggResource/Pages/CreateEgg.php b/app/Filament/Admin/Resources/EggResource/Pages/CreateEgg.php index 9b06efff0..052f4cac9 100644 --- a/app/Filament/Admin/Resources/EggResource/Pages/CreateEgg.php +++ b/app/Filament/Admin/Resources/EggResource/Pages/CreateEgg.php @@ -5,6 +5,10 @@ namespace App\Filament\Admin\Resources\EggResource\Pages; use App\Filament\Admin\Resources\EggResource; use App\Filament\Components\Forms\Fields\CopyFrom; use App\Models\EggVariable; +use App\Traits\Filament\CanCustomizeHeaderActions; +use App\Traits\Filament\CanCustomizeHeaderWidgets; +use Filament\Actions\Action; +use Filament\Actions\ActionGroup; use Filament\Forms\Components\Checkbox; use Filament\Forms\Components\CodeEditor; use Filament\Forms\Components\Hidden; @@ -28,11 +32,15 @@ use Filament\Schemas\Schema; class CreateEgg extends CreateRecord { + use CanCustomizeHeaderActions; + use CanCustomizeHeaderWidgets; + protected static string $resource = EggResource::class; protected static bool $canCreateAnother = false; - protected function getHeaderActions(): array + /** @return array */ + protected function getDefaultHeaderActions(): array { return [ $this->getCreateFormAction()->formId('form'), diff --git a/app/Filament/Admin/Resources/EggResource/Pages/EditEgg.php b/app/Filament/Admin/Resources/EggResource/Pages/EditEgg.php index f5d0fe8ce..b0b01c9ac 100644 --- a/app/Filament/Admin/Resources/EggResource/Pages/EditEgg.php +++ b/app/Filament/Admin/Resources/EggResource/Pages/EditEgg.php @@ -3,12 +3,15 @@ namespace App\Filament\Admin\Resources\EggResource\Pages; use App\Filament\Admin\Resources\EggResource; -use App\Filament\Admin\Resources\EggResource\RelationManagers\ServersRelationManager; use App\Filament\Components\Actions\ExportEggAction; use App\Filament\Components\Actions\ImportEggAction; use App\Filament\Components\Forms\Fields\CopyFrom; use App\Models\Egg; use App\Models\EggVariable; +use App\Traits\Filament\CanCustomizeHeaderActions; +use App\Traits\Filament\CanCustomizeHeaderWidgets; +use Filament\Actions\Action; +use Filament\Actions\ActionGroup; use Filament\Actions\DeleteAction; use Filament\Forms\Components\Checkbox; use Filament\Forms\Components\CodeEditor; @@ -31,6 +34,9 @@ use Filament\Schemas\Schema; class EditEgg extends EditRecord { + use CanCustomizeHeaderActions; + use CanCustomizeHeaderWidgets; + protected static string $resource = EggResource::class; /** @@ -250,7 +256,8 @@ class EditEgg extends EditRecord ]); } - protected function getHeaderActions(): array + /** @return array */ + protected function getDefaultHeaderActions(): array { return [ DeleteAction::make() @@ -272,11 +279,4 @@ class EditEgg extends EditRecord { return []; } - - public function getRelationManagers(): array - { - return [ - ServersRelationManager::class, - ]; - } } diff --git a/app/Filament/Admin/Resources/EggResource/Pages/ListEggs.php b/app/Filament/Admin/Resources/EggResource/Pages/ListEggs.php index cc7bb6842..6dfa9a7e9 100644 --- a/app/Filament/Admin/Resources/EggResource/Pages/ListEggs.php +++ b/app/Filament/Admin/Resources/EggResource/Pages/ListEggs.php @@ -10,7 +10,10 @@ use App\Filament\Components\Tables\Actions\UpdateEggAction; use App\Filament\Components\Tables\Actions\UpdateEggBulkAction; use App\Filament\Components\Tables\Filters\TagsFilter; use App\Models\Egg; -use Filament\Actions\CreateAction; +use App\Traits\Filament\CanCustomizeHeaderActions; +use App\Traits\Filament\CanCustomizeHeaderWidgets; +use Filament\Actions\Action; +use Filament\Actions\ActionGroup; use Filament\Actions\CreateAction as CreateHeaderAction; use Filament\Actions\DeleteBulkAction; use Filament\Actions\EditAction; @@ -23,6 +26,9 @@ use Illuminate\Support\Str; class ListEggs extends ListRecords { + use CanCustomizeHeaderActions; + use CanCustomizeHeaderWidgets; + protected static string $resource = EggResource::class; public function table(Table $table): Table @@ -95,7 +101,8 @@ class ListEggs extends ListRecords ]); } - protected function getHeaderActions(): array + /** @return array */ + protected function getDefaultHeaderActions(): array { return [ ImportEggHeaderAction::make() diff --git a/app/Filament/Admin/Resources/MountResource.php b/app/Filament/Admin/Resources/MountResource.php index fa8e46a58..b5b79fde8 100644 --- a/app/Filament/Admin/Resources/MountResource.php +++ b/app/Filament/Admin/Resources/MountResource.php @@ -4,6 +4,10 @@ namespace App\Filament\Admin\Resources; use App\Filament\Admin\Resources\MountResource\Pages; use App\Models\Mount; +use App\Traits\Filament\CanCustomizePages; +use App\Traits\Filament\CanCustomizeRelations; +use App\Traits\Filament\CanModifyForm; +use App\Traits\Filament\CanModifyTable; use Filament\Actions\CreateAction; use Filament\Actions\DeleteBulkAction; use Filament\Actions\EditAction; @@ -12,15 +16,22 @@ use Filament\Forms\Components\Select; use Filament\Forms\Components\Textarea; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\ToggleButtons; +use Filament\Resources\Pages\PageRegistration; use Filament\Resources\Resource; -use Filament\Schemas\Components\Form; use Filament\Schemas\Components\Group; use Filament\Schemas\Components\Section; +use Filament\Schemas\Schema; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; +use Illuminate\Database\Eloquent\Builder; class MountResource extends Resource { + use CanCustomizePages; + use CanCustomizeRelations; + use CanModifyForm; + use CanModifyTable; + protected static ?string $model = Mount::class; protected static string|\BackedEnum|null $navigationIcon = 'tabler-layers-linked'; @@ -44,7 +55,7 @@ class MountResource extends Resource public static function getNavigationBadge(): ?string { - return static::getModel()::count() ?: null; + return (string) static::getEloquentQuery()->count() ?: null; } public static function getNavigationGroup(): ?string @@ -52,7 +63,10 @@ class MountResource extends Resource return trans('admin/dashboard.advanced'); } - public static function table(Table $table): Table + /** + * @throws \Exception + */ + public static function defaultTable(Table $table): Table { return $table ->columns([ @@ -75,7 +89,7 @@ class MountResource extends Resource ->badge() ->icon(fn ($state) => $state ? 'tabler-writing-off' : 'tabler-writing') ->color(fn ($state) => $state ? 'success' : 'warning') - ->formatStateUsing(fn ($state) => $state ? trans('admin/mount.toggles.read_only') : trans('admin/mount.toggles.writeable')), + ->formatStateUsing(fn ($state) => $state ? trans('admin/mount.toggles.read_only') : trans('admin/mount.toggles.writable')), ]) ->actions([ ViewAction::make() @@ -93,9 +107,12 @@ class MountResource extends Resource ]); } - public static function form(Form|\Filament\Schemas\Schema $schema): \Filament\Schemas\Schema + /** + * @throws \Exception + */ + public static function form(Schema $form): Schema { - return $schema + return $form ->schema([ Section::make()->schema([ TextInput::make('name') @@ -147,7 +164,7 @@ class MountResource extends Resource ->preload(), Select::make('nodes')->multiple() ->label(trans('admin/mount.nodes')) - ->relationship('nodes', 'name') + ->relationship('nodes', 'name', fn (Builder $query) => $query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id'))) ->searchable(['name', 'fqdn']) ->preload(), ]), @@ -161,7 +178,8 @@ class MountResource extends Resource ]); } - public static function getPages(): array + /** @return array */ + public static function getDefaultPages(): array { return [ 'index' => Pages\ListMounts::route('/'), @@ -170,4 +188,15 @@ class MountResource extends Resource 'edit' => Pages\EditMount::route('/{record}/edit'), ]; } + + public static function getEloquentQuery(): Builder + { + $query = parent::getEloquentQuery(); + + return $query->where(function (Builder $query) { + return $query->whereHas('nodes', function (Builder $query) { + $query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id')); + })->orDoesntHave('nodes'); + }); + } } diff --git a/app/Filament/Admin/Resources/MountResource/Pages/CreateMount.php b/app/Filament/Admin/Resources/MountResource/Pages/CreateMount.php index 67fb04b58..9ce6692f6 100644 --- a/app/Filament/Admin/Resources/MountResource/Pages/CreateMount.php +++ b/app/Filament/Admin/Resources/MountResource/Pages/CreateMount.php @@ -3,17 +3,25 @@ namespace App\Filament\Admin\Resources\MountResource\Pages; use App\Filament\Admin\Resources\MountResource; +use App\Traits\Filament\CanCustomizeHeaderActions; +use App\Traits\Filament\CanCustomizeHeaderWidgets; +use Filament\Actions\Action; +use Filament\Actions\ActionGroup; use Filament\Resources\Pages\CreateRecord; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Str; class CreateMount extends CreateRecord { + use CanCustomizeHeaderActions; + use CanCustomizeHeaderWidgets; + protected static string $resource = MountResource::class; protected static bool $canCreateAnother = false; - protected function getHeaderActions(): array + /** @return array */ + protected function getDefaultHeaderActions(): array { return [ $this->getCreateFormAction()->formId('form'), diff --git a/app/Filament/Admin/Resources/MountResource/Pages/EditMount.php b/app/Filament/Admin/Resources/MountResource/Pages/EditMount.php index d669bea59..428863085 100644 --- a/app/Filament/Admin/Resources/MountResource/Pages/EditMount.php +++ b/app/Filament/Admin/Resources/MountResource/Pages/EditMount.php @@ -3,14 +3,22 @@ namespace App\Filament\Admin\Resources\MountResource\Pages; use App\Filament\Admin\Resources\MountResource; +use App\Traits\Filament\CanCustomizeHeaderActions; +use App\Traits\Filament\CanCustomizeHeaderWidgets; +use Filament\Actions\Action; +use Filament\Actions\ActionGroup; use Filament\Actions\DeleteAction; use Filament\Resources\Pages\EditRecord; class EditMount extends EditRecord { + use CanCustomizeHeaderActions; + use CanCustomizeHeaderWidgets; + protected static string $resource = MountResource::class; - protected function getHeaderActions(): array + /** @return array */ + protected function getDefaultHeaderActions(): array { return [ DeleteAction::make(), diff --git a/app/Filament/Admin/Resources/MountResource/Pages/ListMounts.php b/app/Filament/Admin/Resources/MountResource/Pages/ListMounts.php index 41b9e8b65..ee7dfe7fa 100644 --- a/app/Filament/Admin/Resources/MountResource/Pages/ListMounts.php +++ b/app/Filament/Admin/Resources/MountResource/Pages/ListMounts.php @@ -4,14 +4,22 @@ namespace App\Filament\Admin\Resources\MountResource\Pages; use App\Filament\Admin\Resources\MountResource; use App\Models\Mount; +use App\Traits\Filament\CanCustomizeHeaderActions; +use App\Traits\Filament\CanCustomizeHeaderWidgets; +use Filament\Actions\Action; +use Filament\Actions\ActionGroup; use Filament\Actions\CreateAction; use Filament\Resources\Pages\ListRecords; class ListMounts extends ListRecords { + use CanCustomizeHeaderActions; + use CanCustomizeHeaderWidgets; + protected static string $resource = MountResource::class; - protected function getHeaderActions(): array + /** @return array */ + protected function getDefaultHeaderActions(): array { return [ CreateAction::make() diff --git a/app/Filament/Admin/Resources/MountResource/Pages/ViewMount.php b/app/Filament/Admin/Resources/MountResource/Pages/ViewMount.php index e7fe423c5..95b818f7a 100644 --- a/app/Filament/Admin/Resources/MountResource/Pages/ViewMount.php +++ b/app/Filament/Admin/Resources/MountResource/Pages/ViewMount.php @@ -3,14 +3,22 @@ namespace App\Filament\Admin\Resources\MountResource\Pages; use App\Filament\Admin\Resources\MountResource; +use App\Traits\Filament\CanCustomizeHeaderActions; +use App\Traits\Filament\CanCustomizeHeaderWidgets; +use Filament\Actions\Action; +use Filament\Actions\ActionGroup; use Filament\Actions\EditAction; use Filament\Resources\Pages\ViewRecord; class ViewMount extends ViewRecord { + use CanCustomizeHeaderActions; + use CanCustomizeHeaderWidgets; + protected static string $resource = MountResource::class; - protected function getHeaderActions(): array + /** @return array */ + protected function getDefaultHeaderActions(): array { return [ EditAction::make(), diff --git a/app/Filament/Admin/Resources/NodeResource.php b/app/Filament/Admin/Resources/NodeResource.php index 12d640c8a..fae88154e 100644 --- a/app/Filament/Admin/Resources/NodeResource.php +++ b/app/Filament/Admin/Resources/NodeResource.php @@ -5,10 +5,18 @@ namespace App\Filament\Admin\Resources; use App\Filament\Admin\Resources\NodeResource\Pages; use App\Filament\Admin\Resources\NodeResource\RelationManagers; use App\Models\Node; +use App\Traits\Filament\CanCustomizePages; +use App\Traits\Filament\CanCustomizeRelations; +use Filament\Resources\Pages\PageRegistration; +use Filament\Resources\RelationManagers\RelationManager; use Filament\Resources\Resource; +use Illuminate\Database\Eloquent\Builder; class NodeResource extends Resource { + use CanCustomizePages; + use CanCustomizeRelations; + protected static ?string $model = Node::class; protected static string|\BackedEnum|null $navigationIcon = 'tabler-server-2'; @@ -37,10 +45,11 @@ class NodeResource extends Resource public static function getNavigationBadge(): ?string { - return static::getModel()::count() ?: null; + return (string) static::getEloquentQuery()->count() ?: null; } - public static function getRelations(): array + /** @return class-string[] */ + public static function getDefaultRelations(): array { return [ RelationManagers\AllocationsRelationManager::class, @@ -48,7 +57,8 @@ class NodeResource extends Resource ]; } - public static function getPages(): array + /** @return array */ + public static function getDefaultPages(): array { return [ 'index' => Pages\ListNodes::route('/'), @@ -56,4 +66,11 @@ class NodeResource extends Resource 'edit' => Pages\EditNode::route('/{record}/edit'), ]; } + + public static function getEloquentQuery(): Builder + { + $query = parent::getEloquentQuery(); + + return $query->whereIn('id', auth()->user()->accessibleNodes()->pluck('id')); + } } diff --git a/app/Filament/Admin/Resources/NodeResource/Pages/CreateNode.php b/app/Filament/Admin/Resources/NodeResource/Pages/CreateNode.php index 3490c19fd..a89a70520 100644 --- a/app/Filament/Admin/Resources/NodeResource/Pages/CreateNode.php +++ b/app/Filament/Admin/Resources/NodeResource/Pages/CreateNode.php @@ -4,9 +4,11 @@ namespace App\Filament\Admin\Resources\NodeResource\Pages; use App\Filament\Admin\Resources\NodeResource; use App\Models\Node; -use Filament\Actions\Action; -use Filament\Schemas\Components\Form; -use Filament\Schemas\Components\Grid; +use App\Traits\Filament\CanCustomizeHeaderActions; +use App\Traits\Filament\CanCustomizeHeaderWidgets; +use Filament\Forms\Components\Actions\Action; +use Filament\Forms\Components\Grid; +use Filament\Forms\Components\Hidden; use Filament\Forms\Components\TagsInput; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\ToggleButtons; @@ -21,6 +23,9 @@ use Filament\Schemas\Schema; class CreateNode extends CreateRecord { + use CanCustomizeHeaderActions; + use CanCustomizeHeaderWidgets; + protected static string $resource = NodeResource::class; protected static bool $canCreateAnother = false; @@ -124,15 +129,10 @@ class CreateNode extends CreateRecord 'lg' => 1, ]), - TextInput::make('daemon_listen') - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 1, - 'lg' => 1, - ]) - ->label(trans('admin/node.port')) - ->helperText(trans('admin/node.port_help')) + TextInput::make('daemon_connect') + ->columnSpan(1) + ->label(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port') : trans('admin/node.port')) + ->helperText(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port_help') : trans('admin/node.port_help')) ->minValue(1) ->maxValue(65535) ->default(8080) @@ -150,14 +150,15 @@ class CreateNode extends CreateRecord ->required() ->maxLength(100), - ToggleButtons::make('scheme') + Hidden::make('scheme') + ->default(fn () => request()->isSecure() ? 'https' : 'http'), + + Hidden::make('behind_proxy') + ->default(false), + + ToggleButtons::make('connection') ->label(trans('admin/node.ssl')) - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 1, - 'lg' => 1, - ]) + ->columnSpan(1) ->inline() ->helperText(function (Get $get) { if (request()->isSecure()) { @@ -170,20 +171,43 @@ class CreateNode extends CreateRecord return ''; }) - ->disableOptionWhen(fn (string $value): bool => $value === 'http' && request()->isSecure()) + ->disableOptionWhen(fn (string $value) => $value === 'http' && request()->isSecure()) ->options([ 'http' => 'HTTP', 'https' => 'HTTPS (SSL)', + 'https_proxy' => 'HTTPS with (reverse) proxy', ]) ->colors([ 'http' => 'warning', 'https' => 'success', + 'https_proxy' => 'success', ]) ->icons([ 'http' => 'tabler-lock-open-off', 'https' => 'tabler-lock', + 'https_proxy' => 'tabler-shield-lock', ]) - ->default(fn () => request()->isSecure() ? 'https' : 'http'), + ->default(fn () => request()->isSecure() ? 'https' : 'http') + ->live() + ->dehydrated(false) + ->afterStateUpdated(function ($state, Set $set) { + $set('scheme', $state === 'http' ? 'http' : 'https'); + $set('behind_proxy', $state === 'https_proxy'); + + $set('daemon_connect', $state === 'https_proxy' ? 443 : 8080); + $set('daemon_listen', 8080); + }), + + TextInput::make('daemon_listen') + ->columnSpan(1) + ->label(trans('admin/node.listen_port')) + ->helperText(trans('admin/node.listen_port_help')) + ->minValue(1) + ->maxValue(65535) + ->default(8080) + ->required() + ->integer() + ->visible(fn (Get $get) => $get('connection') === 'https_proxy'), ]), Step::make('advanced') ->label(trans('admin/node.tabs.advanced_settings')) @@ -399,4 +423,13 @@ class CreateNode extends CreateRecord { return []; } + + protected function mutateFormDataBeforeCreate(array $data): array + { + if (!$data['behind_proxy']) { + $data['daemon_listen'] = $data['daemon_connect']; + } + + return $data; + } } diff --git a/app/Filament/Admin/Resources/NodeResource/Pages/EditNode.php b/app/Filament/Admin/Resources/NodeResource/Pages/EditNode.php index 7056b0e95..eee9ae2a4 100644 --- a/app/Filament/Admin/Resources/NodeResource/Pages/EditNode.php +++ b/app/Filament/Admin/Resources/NodeResource/Pages/EditNode.php @@ -8,12 +8,15 @@ use App\Repositories\Daemon\DaemonConfigurationRepository; use App\Services\Helpers\SoftwareVersionService; use App\Services\Nodes\NodeAutoDeployService; use App\Services\Nodes\NodeUpdateService; +use App\Traits\Filament\CanCustomizeHeaderActions; +use App\Traits\Filament\CanCustomizeHeaderWidgets; use Exception; -use Filament\Actions\Action; -use Filament\Actions\DeleteAction; -use Filament\Schemas\Components\Actions; -use Filament\Schemas\Components\Fieldset; -use Filament\Infolists\Components\TextEntry; +use Filament\Actions; +use Filament\Forms\Components\Fieldset; +use Filament\Forms\Components\Grid; +use Filament\Forms\Components\Hidden; +use Filament\Forms\Components\Tabs; +use Filament\Forms\Components\Tabs\Tab; use Filament\Forms\Components\TagsInput; use Filament\Forms\Components\Textarea; use Filament\Forms\Components\TextInput; @@ -35,6 +38,9 @@ use Illuminate\Support\HtmlString; class EditNode extends EditRecord { + use CanCustomizeHeaderActions; + use CanCustomizeHeaderWidgets; + protected static string $resource = NodeResource::class; private DaemonConfigurationRepository $daemonConfigurationRepository; @@ -184,10 +190,10 @@ class EditNode extends EditRecord 0 => 'danger', ]) ->columnSpan(1), - TextInput::make('daemon_listen') + TextInput::make('daemon_connect') ->columnSpan(1) - ->label(trans('admin/node.port')) - ->helperText(trans('admin/node.port_help')) + ->label(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port') : trans('admin/node.port')) + ->helperText(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port_help') : trans('admin/node.port_help')) ->minValue(1) ->maxValue(65535) ->default(8080) @@ -203,7 +209,9 @@ class EditNode extends EditRecord ]) ->required() ->maxLength(100), - ToggleButtons::make('scheme') + Hidden::make('scheme'), + Hidden::make('behind_proxy'), + ToggleButtons::make('connection') ->label(trans('admin/node.ssl')) ->columnSpan(1) ->inline() @@ -218,20 +226,43 @@ class EditNode extends EditRecord return ''; }) - ->disableOptionWhen(fn (string $value): bool => $value === 'http' && request()->isSecure()) + ->disableOptionWhen(fn (string $value) => $value === 'http' && request()->isSecure()) ->options([ 'http' => 'HTTP', 'https' => 'HTTPS (SSL)', + 'https_proxy' => 'HTTPS with (reverse) proxy', ]) ->colors([ 'http' => 'warning', 'https' => 'success', + 'https_proxy' => 'success', ]) ->icons([ 'http' => 'tabler-lock-open-off', 'https' => 'tabler-lock', + 'https_proxy' => 'tabler-shield-lock', ]) - ->default(fn () => request()->isSecure() ? 'https' : 'http'), ]), + ->formatStateUsing(fn (Get $get) => $get('scheme') === 'http' ? 'http' : ($get('behind_proxy') ? 'https_proxy' : 'https')) + ->live() + ->dehydrated(false) + ->afterStateUpdated(function ($state, Set $set) { + $set('scheme', $state === 'http' ? 'http' : 'https'); + $set('behind_proxy', $state === 'https_proxy'); + + $set('daemon_connect', $state === 'https_proxy' ? 443 : 8080); + $set('daemon_listen', 8080); + }), + TextInput::make('daemon_listen') + ->columnSpan(1) + ->label(trans('admin/node.listen_port')) + ->helperText(trans('admin/node.listen_port_help')) + ->minValue(1) + ->maxValue(65535) + ->default(8080) + ->required() + ->integer() + ->visible(fn (Get $get) => $get('connection') === 'https_proxy'), + ]), Tab::make('adv') ->label(trans('admin/node.tabs.advanced_settings')) ->columns([ @@ -618,7 +649,8 @@ class EditNode extends EditRecord return []; } - protected function getHeaderActions(): array + /** @return array */ + protected function getDefaultHeaderActions(): array { return [ DeleteAction::make() @@ -628,6 +660,15 @@ class EditNode extends EditRecord ]; } + protected function mutateFormDataBeforeSave(array $data): array + { + if (!$data['behind_proxy']) { + $data['daemon_listen'] = $data['daemon_connect']; + } + + return $data; + } + protected function afterSave(): void { $this->fillForm(); diff --git a/app/Filament/Admin/Resources/NodeResource/Pages/ListNodes.php b/app/Filament/Admin/Resources/NodeResource/Pages/ListNodes.php index cdb726147..3ff77f8ca 100644 --- a/app/Filament/Admin/Resources/NodeResource/Pages/ListNodes.php +++ b/app/Filament/Admin/Resources/NodeResource/Pages/ListNodes.php @@ -6,6 +6,9 @@ use App\Filament\Admin\Resources\NodeResource; use App\Filament\Components\Tables\Columns\NodeHealthColumn; use App\Filament\Components\Tables\Filters\TagsFilter; use App\Models\Node; +use App\Traits\Filament\CanCustomizeHeaderActions; +use App\Traits\Filament\CanCustomizeHeaderWidgets; +use Filament\Actions; use Filament\Resources\Pages\ListRecords; use Filament\Actions\CreateAction; use Filament\Actions\EditAction; @@ -15,6 +18,9 @@ use Filament\Tables\Table; class ListNodes extends ListRecords { + use CanCustomizeHeaderActions; + use CanCustomizeHeaderWidgets; + protected static string $resource = NodeResource::class; public function table(Table $table): Table @@ -72,7 +78,8 @@ class ListNodes extends ListRecords ]); } - protected function getHeaderActions(): array + /** @return array */ + protected function getDefaultHeaderActions(): array { return [ CreateAction::make() diff --git a/app/Filament/Admin/Resources/NodeResource/RelationManagers/AllocationsRelationManager.php b/app/Filament/Admin/Resources/NodeResource/RelationManagers/AllocationsRelationManager.php index 614e6c68b..5683afbfa 100644 --- a/app/Filament/Admin/Resources/NodeResource/RelationManagers/AllocationsRelationManager.php +++ b/app/Filament/Admin/Resources/NodeResource/RelationManagers/AllocationsRelationManager.php @@ -13,7 +13,6 @@ use Filament\Forms\Components\TextInput; use Filament\Schemas\Components\Utilities\Get; use Filament\Schemas\Components\Utilities\Set; use Filament\Resources\RelationManagers\RelationManager; -use Filament\Actions\BulkActionGroup; use Filament\Actions\DeleteBulkAction; use Filament\Tables\Columns\SelectColumn; use Filament\Tables\Columns\TextColumn; @@ -32,18 +31,12 @@ class AllocationsRelationManager extends RelationManager public function setTitle(): string { return trans('admin/server.allocations'); - } public function table(Table $table): Table { return $table - ->recordTitleAttribute('ip') - - // Non Primary Allocations - // ->checkIfRecordIsSelectableUsing(fn (Allocation $allocation) => $allocation->id !== $allocation->server?->allocation_id) - - // All assigned allocations + ->recordTitleAttribute('address') ->checkIfRecordIsSelectableUsing(fn (Allocation $allocation) => $allocation->server_id === null) ->paginationPageOptions(['10', '20', '50', '100', '200', '500']) ->searchable() @@ -79,7 +72,7 @@ class AllocationsRelationManager extends RelationManager ->options(collect($this->getOwnerRecord()->ipAddresses())->mapWithKeys(fn (string $ip) => [$ip => $ip])) ->label(trans('admin/node.ip_address')) ->inlineLabel() - ->ipv4() + ->ip() ->helperText(trans('admin/node.ip_help')) ->afterStateUpdated(fn (Set $set) => $set('allocation_ports', [])) ->live() @@ -96,19 +89,15 @@ class AllocationsRelationManager extends RelationManager ->inlineLabel() ->live() ->disabled(fn (Get $get) => empty($get('allocation_ip'))) - ->afterStateUpdated(fn ($state, Set $set, Get $get) => $set('allocation_ports', - CreateServer::retrieveValidPorts($this->getOwnerRecord(), $state, $get('allocation_ip'))) - ) + ->afterStateUpdated(fn ($state, Set $set, Get $get) => $set('allocation_ports', CreateServer::retrieveValidPorts($this->getOwnerRecord(), $state, $get('allocation_ip')))) ->splitKeys(['Tab', ' ', ',']) ->required(), ]) ->action(fn (array $data, AssignmentService $service) => $service->handle($this->getOwnerRecord(), $data)), ]) - ->bulkActions([ - BulkActionGroup::make([ - DeleteBulkAction::make() - ->authorize(fn () => auth()->user()->can('update node')), - ]), + ->groupedBulkActions([ + DeleteBulkAction::make() + ->authorize(fn () => auth()->user()->can('update', $this->getOwnerRecord())), ]); } } diff --git a/app/Filament/Admin/Resources/NodeResource/Widgets/NodeCpuChart.php b/app/Filament/Admin/Resources/NodeResource/Widgets/NodeCpuChart.php index 197a3df32..22c0f6b42 100644 --- a/app/Filament/Admin/Resources/NodeResource/Widgets/NodeCpuChart.php +++ b/app/Filament/Admin/Resources/NodeResource/Widgets/NodeCpuChart.php @@ -3,7 +3,6 @@ namespace App\Filament\Admin\Resources\NodeResource\Widgets; use App\Models\Node; -use Carbon\Carbon; use Filament\Support\RawJs; use Filament\Widgets\ChartWidget; use Illuminate\Support\Number; @@ -16,22 +15,34 @@ class NodeCpuChart extends ChartWidget public Node $node; + /** + * @var array + */ + protected array $cpuHistory = []; + + protected int $threads = 0; + protected function getData(): array { - $threads = $this->node->systemInformation()['cpu_count'] ?? 0; + $sessionKey = "node_stats.{$this->node->id}"; - $cpu = collect(cache()->get("nodes.{$this->node->id}.cpu_percent")) - ->slice(-10) - ->map(fn ($value, $key) => [ - 'cpu' => round($value * $threads, 2), - 'timestamp' => Carbon::createFromTimestamp($key, auth()->user()->timezone ?? 'UTC')->format('H:i:s'), - ]) - ->all(); + $data = $this->node->statistics(); + + $this->threads = session("{$sessionKey}.threads", $this->node->systemInformation()['cpu_count'] ?? 0); + + $this->cpuHistory = session("{$sessionKey}.cpu_history", []); + $this->cpuHistory[] = [ + 'cpu' => round($data['cpu_percent'] * $this->threads, 2), + 'timestamp' => now(auth()->user()->timezone ?? 'UTC')->format('H:i:s'), + ]; + + $this->cpuHistory = array_slice($this->cpuHistory, -60); + session()->put("{$sessionKey}.cpu_history", $this->cpuHistory); return [ 'datasets' => [ [ - 'data' => array_column($cpu, 'cpu'), + 'data' => array_column($this->cpuHistory, 'cpu'), 'backgroundColor' => [ 'rgba(96, 165, 250, 0.3)', ], @@ -39,7 +50,7 @@ class NodeCpuChart extends ChartWidget 'fill' => true, ], ], - 'labels' => array_column($cpu, 'timestamp'), + 'labels' => array_column($this->cpuHistory, 'timestamp'), 'locale' => auth()->user()->language ?? 'en', ]; } @@ -69,10 +80,10 @@ class NodeCpuChart extends ChartWidget public function getHeading(): string { - $threads = $this->node->systemInformation()['cpu_count'] ?? 0; + $data = array_slice(end($this->cpuHistory), -60); - $cpu = Number::format(collect(cache()->get("nodes.{$this->node->id}.cpu_percent"))->last() * $threads, maxPrecision: 2, locale: auth()->user()->language); - $max = Number::format($threads * 100, locale: auth()->user()->language); + $cpu = Number::format($data['cpu'], maxPrecision: 2, locale: auth()->user()->language); + $max = Number::format($this->threads * 100, locale: auth()->user()->language); return trans('admin/node.cpu_chart', ['cpu' => $cpu, 'max' => $max]); } diff --git a/app/Filament/Admin/Resources/NodeResource/Widgets/NodeMemoryChart.php b/app/Filament/Admin/Resources/NodeResource/Widgets/NodeMemoryChart.php index a3e9ef7cc..77e2b4091 100644 --- a/app/Filament/Admin/Resources/NodeResource/Widgets/NodeMemoryChart.php +++ b/app/Filament/Admin/Resources/NodeResource/Widgets/NodeMemoryChart.php @@ -3,7 +3,6 @@ namespace App\Filament\Admin\Resources\NodeResource\Widgets; use App\Models\Node; -use Carbon\Carbon; use Filament\Support\RawJs; use Filament\Widgets\ChartWidget; use Illuminate\Support\Number; @@ -16,19 +15,36 @@ class NodeMemoryChart extends ChartWidget public Node $node; + /** + * @var array + */ + protected array $memoryHistory = []; + + protected int $totalMemory = 0; + protected function getData(): array { - $memUsed = collect(cache()->get("nodes.{$this->node->id}.memory_used"))->slice(-10) - ->map(fn ($value, $key) => [ - 'memory' => round(config('panel.use_binary_prefix') ? $value / 1024 / 1024 / 1024 : $value / 1000 / 1000 / 1000, 2), - 'timestamp' => Carbon::createFromTimestamp($key, auth()->user()->timezone ?? 'UTC')->format('H:i:s'), - ]) - ->all(); + $sessionKey = "node_stats.{$this->node->id}"; + + $data = $this->node->statistics(); + + $this->totalMemory = session("{$sessionKey}.total_memory", $data['memory_total']); + + $this->memoryHistory = session("{$sessionKey}.memory_history", []); + $this->memoryHistory[] = [ + 'memory' => round(config('panel.use_binary_prefix') + ? $data['memory_used'] / 1024 / 1024 / 1024 + : $data['memory_used'] / 1000 / 1000 / 1000, 2), + 'timestamp' => now(auth()->user()->timezone ?? 'UTC')->format('H:i:s'), + ]; + + $this->memoryHistory = array_slice($this->memoryHistory, -60); + session()->put("{$sessionKey}.memory_history", $this->memoryHistory); return [ 'datasets' => [ [ - 'data' => array_column($memUsed, 'memory'), + 'data' => array_column($this->memoryHistory, 'memory'), 'backgroundColor' => [ 'rgba(96, 165, 250, 0.3)', ], @@ -36,7 +52,7 @@ class NodeMemoryChart extends ChartWidget 'fill' => true, ], ], - 'labels' => array_column($memUsed, 'timestamp'), + 'labels' => array_column($this->memoryHistory, 'timestamp'), 'locale' => auth()->user()->language ?? 'en', ]; } @@ -66,16 +82,15 @@ class NodeMemoryChart extends ChartWidget public function getHeading(): string { - $latestMemoryUsed = collect(cache()->get("nodes.{$this->node->id}.memory_used"))->last(); - $totalMemory = collect(cache()->get("nodes.{$this->node->id}.memory_total"))->last(); + $latestMemoryUsed = array_slice(end($this->memoryHistory), -60); $used = config('panel.use_binary_prefix') - ? Number::format($latestMemoryUsed / 1024 / 1024 / 1024, maxPrecision: 2, locale: auth()->user()->language) .' GiB' - : Number::format($latestMemoryUsed / 1000 / 1000 / 1000, maxPrecision: 2, locale: auth()->user()->language) . ' GB'; + ? Number::format($latestMemoryUsed['memory'], maxPrecision: 2, locale: auth()->user()->language) .' GiB' + : Number::format($latestMemoryUsed['memory'], maxPrecision: 2, locale: auth()->user()->language) . ' GB'; $total = config('panel.use_binary_prefix') - ? Number::format($totalMemory / 1024 / 1024 / 1024, maxPrecision: 2, locale: auth()->user()->language) .' GiB' - : Number::format($totalMemory / 1000 / 1000 / 1000, maxPrecision: 2, locale: auth()->user()->language) . ' GB'; + ? Number::format($this->totalMemory / 1024 / 1024 / 1024, maxPrecision: 2, locale: auth()->user()->language) .' GiB' + : Number::format($this->totalMemory / 1000 / 1000 / 1000, maxPrecision: 2, locale: auth()->user()->language) . ' GB'; return trans('admin/node.memory_chart', ['used' => $used, 'total' => $total]); } diff --git a/app/Filament/Admin/Resources/RoleResource.php b/app/Filament/Admin/Resources/RoleResource.php index d4f0d18ab..757a17289 100644 --- a/app/Filament/Admin/Resources/RoleResource.php +++ b/app/Filament/Admin/Resources/RoleResource.php @@ -10,10 +10,16 @@ use Filament\Actions\CreateAction; use Filament\Actions\DeleteBulkAction; use Filament\Actions\EditAction; use Filament\Actions\ViewAction; +use App\Traits\Filament\CanCustomizePages; +use App\Traits\Filament\CanCustomizeRelations; +use App\Traits\Filament\CanModifyForm; +use App\Traits\Filament\CanModifyTable; use Filament\Forms\Components\CheckboxList; use Filament\Schemas\Schema; +use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; use Filament\Infolists\Components\TextEntry; +use Filament\Resources\Pages\PageRegistration; use Filament\Resources\Resource; use Filament\Schemas\Components\Component; use Filament\Schemas\Components\Fieldset; @@ -26,6 +32,11 @@ use Spatie\Permission\Contracts\Permission; class RoleResource extends Resource { + use CanCustomizePages; + use CanCustomizeRelations; + use CanModifyForm; + use CanModifyTable; + protected static ?string $model = Role::class; protected static string|\BackedEnum|null $navigationIcon = 'tabler-users-group'; @@ -57,7 +68,7 @@ class RoleResource extends Resource return static::getModel()::count() ?: null; } - public static function table(Table $table): Table + public static function defaultTable(Table $table): Table { return $table ->columns([ @@ -69,6 +80,11 @@ class RoleResource extends Resource ->badge() ->counts('permissions') ->formatStateUsing(fn (Role $role, $state) => $role->isRootAdmin() ? trans('admin/role.all') : $state), + TextColumn::make('nodes.name') + ->icon('tabler-server-2') + ->label(trans('admin/role.nodes')) + ->badge() + ->placeholder(trans('admin/role.all')), TextColumn::make('users_count') ->label(trans('admin/role.users')) ->counts('users') @@ -94,7 +110,7 @@ class RoleResource extends Resource /** * @throws Exception */ - public static function form(Schema $schema): Schema + public static function form(Schema $form): Schema { $permissionSections = []; @@ -108,7 +124,7 @@ class RoleResource extends Resource $permissionSections[] = self::makeSection($model, $options); } - return $schema + return $form ->columns(1) ->components([ TextInput::make('name') @@ -128,6 +144,14 @@ class RoleResource extends Resource ->label(trans('admin/role.permissions')) ->state(trans('admin/role.root_admin', ['role' => Role::ROOT_ADMIN])) ->visible(fn (Get $get) => $get('name') === Role::ROOT_ADMIN), + Select::make('nodes') + ->label(trans('admin/role.nodes')) + ->multiple() + ->relationship('nodes', 'name') + ->searchable(['name', 'fqdn']) + ->preload() + ->hint(trans('admin/role.nodes_hint')) + ->hidden(fn (Get $get) => $get('name') === Role::ROOT_ADMIN), ]); } @@ -138,6 +162,8 @@ class RoleResource extends Resource */ private static function makeSection(string $model, array $options): Section { + $model = ucwords($model); + $icon = null; if (class_exists('\App\Filament\Admin\Resources\\' . $model . 'Resource')) { @@ -189,7 +215,8 @@ class RoleResource extends Resource ]); } - public static function getPages(): array + /** @return array */ + public static function getDefaultPages(): array { return [ 'index' => Pages\ListRoles::route('/'), diff --git a/app/Filament/Admin/Resources/RoleResource/Pages/CreateRole.php b/app/Filament/Admin/Resources/RoleResource/Pages/CreateRole.php index 7f908cfbc..63b1581b5 100644 --- a/app/Filament/Admin/Resources/RoleResource/Pages/CreateRole.php +++ b/app/Filament/Admin/Resources/RoleResource/Pages/CreateRole.php @@ -4,6 +4,10 @@ namespace App\Filament\Admin\Resources\RoleResource\Pages; use App\Filament\Admin\Resources\RoleResource; use App\Models\Role; +use App\Traits\Filament\CanCustomizeHeaderActions; +use App\Traits\Filament\CanCustomizeHeaderWidgets; +use Filament\Actions\Action; +use Filament\Actions\ActionGroup; use Filament\Resources\Pages\CreateRecord; use Illuminate\Support\Arr; use Illuminate\Support\Collection; @@ -14,13 +18,17 @@ use Spatie\Permission\Models\Permission; */ class CreateRole extends CreateRecord { + use CanCustomizeHeaderActions; + use CanCustomizeHeaderWidgets; + public Collection $permissions; protected static string $resource = RoleResource::class; protected static bool $canCreateAnother = false; - protected function getHeaderActions(): array + /** @return array */ + protected function getDefaultHeaderActions(): array { return [ $this->getCreateFormAction()->formId('form'), diff --git a/app/Filament/Admin/Resources/RoleResource/Pages/EditRole.php b/app/Filament/Admin/Resources/RoleResource/Pages/EditRole.php index 1bee1277a..fee7c7df2 100644 --- a/app/Filament/Admin/Resources/RoleResource/Pages/EditRole.php +++ b/app/Filament/Admin/Resources/RoleResource/Pages/EditRole.php @@ -4,6 +4,10 @@ namespace App\Filament\Admin\Resources\RoleResource\Pages; use App\Filament\Admin\Resources\RoleResource; use App\Models\Role; +use App\Traits\Filament\CanCustomizeHeaderActions; +use App\Traits\Filament\CanCustomizeHeaderWidgets; +use Filament\Actions\Action; +use Filament\Actions\ActionGroup; use Filament\Actions\DeleteAction; use Filament\Resources\Pages\EditRecord; use Illuminate\Support\Arr; @@ -15,6 +19,9 @@ use Spatie\Permission\Models\Permission; */ class EditRole extends EditRecord { + use CanCustomizeHeaderActions; + use CanCustomizeHeaderWidgets; + protected static string $resource = RoleResource::class; public Collection $permissions; @@ -45,7 +52,8 @@ class EditRole extends EditRecord $this->record->syncPermissions($permissionModels); } - protected function getHeaderActions(): array + /** @return array */ + protected function getDefaultHeaderActions(): array { return [ DeleteAction::make() diff --git a/app/Filament/Admin/Resources/RoleResource/Pages/ListRoles.php b/app/Filament/Admin/Resources/RoleResource/Pages/ListRoles.php index 5120509fa..7af5402a6 100644 --- a/app/Filament/Admin/Resources/RoleResource/Pages/ListRoles.php +++ b/app/Filament/Admin/Resources/RoleResource/Pages/ListRoles.php @@ -3,14 +3,22 @@ namespace App\Filament\Admin\Resources\RoleResource\Pages; use App\Filament\Admin\Resources\RoleResource; +use App\Traits\Filament\CanCustomizeHeaderActions; +use App\Traits\Filament\CanCustomizeHeaderWidgets; +use Filament\Actions\Action; +use Filament\Actions\ActionGroup; use Filament\Actions\CreateAction; use Filament\Resources\Pages\ListRecords; class ListRoles extends ListRecords { + use CanCustomizeHeaderActions; + use CanCustomizeHeaderWidgets; + protected static string $resource = RoleResource::class; - protected function getHeaderActions(): array + /** @return array */ + protected function getDefaultHeaderActions(): array { return [ CreateAction::make(), diff --git a/app/Filament/Admin/Resources/RoleResource/Pages/ViewRole.php b/app/Filament/Admin/Resources/RoleResource/Pages/ViewRole.php index d8a41e058..ab7e7e218 100644 --- a/app/Filament/Admin/Resources/RoleResource/Pages/ViewRole.php +++ b/app/Filament/Admin/Resources/RoleResource/Pages/ViewRole.php @@ -3,14 +3,22 @@ namespace App\Filament\Admin\Resources\RoleResource\Pages; use App\Filament\Admin\Resources\RoleResource; +use App\Traits\Filament\CanCustomizeHeaderActions; +use App\Traits\Filament\CanCustomizeHeaderWidgets; +use Filament\Actions\Action; +use Filament\Actions\ActionGroup; use Filament\Actions\EditAction; use Filament\Resources\Pages\ViewRecord; class ViewRole extends ViewRecord { + use CanCustomizeHeaderActions; + use CanCustomizeHeaderWidgets; + protected static string $resource = RoleResource::class; - protected function getHeaderActions(): array + /** @return array */ + protected function getDefaultHeaderActions(): array { return [ EditAction::make(), diff --git a/app/Filament/Admin/Resources/ServerResource.php b/app/Filament/Admin/Resources/ServerResource.php index e1e2c2c91..50a2ac1af 100644 --- a/app/Filament/Admin/Resources/ServerResource.php +++ b/app/Filament/Admin/Resources/ServerResource.php @@ -3,11 +3,23 @@ namespace App\Filament\Admin\Resources; use App\Filament\Admin\Resources\ServerResource\Pages; +use App\Filament\Admin\Resources\ServerResource\RelationManagers; +use App\Models\Mount; use App\Models\Server; +use App\Traits\Filament\CanCustomizePages; +use App\Traits\Filament\CanCustomizeRelations; +use Filament\Forms\Components\CheckboxList; +use Filament\Forms\Get; +use Filament\Resources\Pages\PageRegistration; +use Filament\Resources\RelationManagers\RelationManager; use Filament\Resources\Resource; +use Illuminate\Database\Eloquent\Builder; class ServerResource extends Resource { + use CanCustomizePages; + use CanCustomizeRelations; + protected static ?string $model = Server::class; protected static string|\BackedEnum|null $navigationIcon = 'tabler-brand-docker'; @@ -36,10 +48,42 @@ class ServerResource extends Resource public static function getNavigationBadge(): ?string { - return static::getModel()::count() ?: null; + return (string) static::getEloquentQuery()->count() ?: null; } - public static function getPages(): array + public static function getMountCheckboxList(Get $get): CheckboxList + { + $allowedMounts = Mount::all(); + $node = $get('node_id'); + $egg = $get('egg_id'); + + if ($node && $egg) { + $allowedMounts = $allowedMounts->filter(fn (Mount $mount) => ($mount->nodes->isEmpty() || $mount->nodes->contains($node)) && + ($mount->eggs->isEmpty() || $mount->eggs->contains($egg)) + ); + } + + return CheckboxList::make('mounts') + ->label('') + ->relationship('mounts') + ->live() + ->options(fn () => $allowedMounts->mapWithKeys(fn ($mount) => [$mount->id => $mount->name])) + ->descriptions(fn () => $allowedMounts->mapWithKeys(fn ($mount) => [$mount->id => "$mount->source -> $mount->target"])) + ->helperText(fn () => $allowedMounts->isEmpty() ? trans('admin/server.no_mounts') : null) + ->bulkToggleable() + ->columnSpanFull(); + } + + /** @return class-string[] */ + public static function getDefaultRelations(): array + { + return [ + RelationManagers\AllocationsRelationManager::class, + ]; + } + + /** @return array */ + public static function getDefaultPages(): array { return [ 'index' => Pages\ListServers::route('/'), @@ -47,4 +91,11 @@ class ServerResource extends Resource 'edit' => Pages\EditServer::route('/{record}/edit'), ]; } + + public static function getEloquentQuery(): Builder + { + $query = parent::getEloquentQuery(); + + return $query->whereIn('node_id', auth()->user()->accessibleNodes()->pluck('id')); + } } diff --git a/app/Filament/Admin/Resources/ServerResource/Pages/CreateServer.php b/app/Filament/Admin/Resources/ServerResource/Pages/CreateServer.php index 6675f105a..18351d1bd 100644 --- a/app/Filament/Admin/Resources/ServerResource/Pages/CreateServer.php +++ b/app/Filament/Admin/Resources/ServerResource/Pages/CreateServer.php @@ -11,10 +11,11 @@ use App\Services\Allocations\AssignmentService; use App\Services\Servers\RandomWordService; use App\Services\Servers\ServerCreationService; use App\Services\Users\UserCreationService; +use App\Traits\Filament\CanCustomizeHeaderActions; +use App\Traits\Filament\CanCustomizeHeaderWidgets; use Closure; use Exception; use Filament\Actions\Action; -use Filament\Forms\Components\CheckboxList; use Filament\Schemas\Components\Component; use Filament\Schemas\Components\Fieldset; use Filament\Forms\Components\Hidden; @@ -26,7 +27,6 @@ use Filament\Forms\Components\TagsInput; use Filament\Forms\Components\Textarea; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\ToggleButtons; -use Filament\Schemas\Components\Form; use Filament\Schemas\Components\Grid; use Filament\Schemas\Components\Section; use Filament\Schemas\Components\Utilities\Get; @@ -46,6 +46,9 @@ use Filament\Schemas\Schema; class CreateServer extends CreateRecord { + use CanCustomizeHeaderActions; + use CanCustomizeHeaderWidgets; + protected static string $resource = ServerResource::class; protected static bool $canCreateAnother = false; @@ -109,14 +112,20 @@ class CreateServer extends CreateRecord ->disabledOn('edit') ->prefixIcon('tabler-server-2') ->selectablePlaceholder(false) - ->default(fn () => ($this->node = Node::query()->latest()->first())?->id) + ->default(function () { + /** @var ?Node $latestNode */ + $latestNode = auth()->user()->accessibleNodes()->latest()->first(); + $this->node = $latestNode; + + return $this->node?->id; + }) ->columnSpan([ 'default' => 1, 'sm' => 2, 'md' => 2, ]) ->live() - ->relationship('node', 'name') + ->relationship('node', 'name', fn (Builder $query) => $query->whereIn('id', auth()->user()->accessibleNodes()->pluck('id'))) ->searchable() ->preload() ->afterStateUpdated(function (Set $set, $state) { @@ -139,6 +148,7 @@ class CreateServer extends CreateRecord ->relationship('user', 'username') ->searchable(['username', 'email']) ->getOptionLabelFromRecordUsing(fn (User $user) => "$user->username ($user->email)") + ->createOptionAction(fn (Action $action) => $action->authorize(fn () => auth()->user()->can('create', User::class))) ->createOptionForm([ TextInput::make('username') ->label(trans('admin/user.username')) @@ -183,10 +193,7 @@ class CreateServer extends CreateRecord $set('allocation_additional', null); $set('allocation_additional.needstobeastringhere.extra_allocations', null); }) - ->getOptionLabelFromRecordUsing( - fn (Allocation $allocation) => "$allocation->ip:$allocation->port" . - ($allocation->ip_alias ? " ($allocation->ip_alias)" : '') - ) + ->getOptionLabelFromRecordUsing(fn (Allocation $allocation) => $allocation->address) ->placeholder(function (Get $get) { $node = Node::find($get('node_id')); @@ -203,6 +210,7 @@ class CreateServer extends CreateRecord ->where('node_id', $get('node_id')) ->whereNull('server_id'), ) + ->createOptionAction(fn (Action $action) => $action->authorize(fn (Get $get) => auth()->user()->can('create', Node::find($get('node_id'))))) ->createOptionForm(function (Get $get) { $getPage = $get; @@ -212,7 +220,7 @@ class CreateServer extends CreateRecord ->label(trans('admin/server.ip_address'))->inlineLabel() ->helperText(trans('admin/server.ip_address_helper')) ->afterStateUpdated(fn (Set $set) => $set('allocation_ports', [])) - ->ipv4() + ->ip() ->live() ->required(), TextInput::make('allocation_alias') @@ -263,10 +271,7 @@ class CreateServer extends CreateRecord ->columnSpan(2) ->disabled(fn (Get $get) => $get('../../node_id') === null) ->searchable(['ip', 'port', 'ip_alias']) - ->getOptionLabelFromRecordUsing( - fn (Allocation $allocation) => "$allocation->ip:$allocation->port" . - ($allocation->ip_alias ? " ($allocation->ip_alias)" : '') - ) + ->getOptionLabelFromRecordUsing(fn (Allocation $allocation) => $allocation->address) ->placeholder(trans('admin/server.select_additional')) ->disableOptionsWhenSelectedInSiblingRepeaterItems() ->relationship( @@ -744,7 +749,7 @@ class CreateServer extends CreateRecord 'lg' => 4, ]) ->columnSpan(6) - ->schema([ + ->schema(fn (Get $get) => [ Select::make('select_image') ->label(trans('admin/server.image_name')) ->live() @@ -798,14 +803,7 @@ class CreateServer extends CreateRecord ->valueLabel(trans('admin/server.description')) ->columnSpanFull(), - CheckboxList::make('mounts') - ->label('Mounts') - ->live() - ->relationship('mounts') - ->options(fn () => $this->node?->mounts->mapWithKeys(fn ($mount) => [$mount->id => $mount->name]) ?? []) - ->descriptions(fn () => $this->node?->mounts->mapWithKeys(fn ($mount) => [$mount->id => "$mount->source -> $mount->target"]) ?? []) - ->helperText(fn () => $this->node?->mounts->isNotEmpty() ? '' : 'No Mounts exist for this Node') - ->columnSpanFull(), + ServerResource::getMountCheckboxList($get), ]), ]), ]) diff --git a/app/Filament/Admin/Resources/ServerResource/Pages/EditServer.php b/app/Filament/Admin/Resources/ServerResource/Pages/EditServer.php index 23f658868..13a247fa1 100644 --- a/app/Filament/Admin/Resources/ServerResource/Pages/EditServer.php +++ b/app/Filament/Admin/Resources/ServerResource/Pages/EditServer.php @@ -2,10 +2,9 @@ namespace App\Filament\Admin\Resources\ServerResource\Pages; -use App\Enums\ServerState; +use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor; use App\Enums\SuspendAction; use App\Filament\Admin\Resources\ServerResource; -use App\Filament\Admin\Resources\ServerResource\RelationManagers\AllocationsRelationManager; use App\Filament\Components\Forms\Actions\PreviewStartupAction; use App\Filament\Components\Forms\Actions\RotateDatabasePasswordAction; use App\Filament\Server\Pages\Console; @@ -13,7 +12,6 @@ use App\Models\Allocation; use App\Models\Database; use App\Models\DatabaseHost; use App\Models\Egg; -use App\Models\Mount; use App\Models\Node; use App\Models\Server; use App\Models\ServerVariable; @@ -27,10 +25,11 @@ use App\Services\Servers\ServerDeletionService; use App\Services\Servers\SuspensionService; use App\Services\Servers\ToggleInstallService; use App\Services\Servers\TransferServerService; +use App\Traits\Filament\CanCustomizeHeaderActions; +use App\Traits\Filament\CanCustomizeHeaderWidgets; use Closure; use Exception; use Filament\Actions\Action; -use Filament\Forms\Components\CheckboxList; use Filament\Schemas\Components\Actions; use Filament\Schemas\Components\Component; use Filament\Schemas\Components\Fieldset; @@ -51,6 +50,7 @@ use Filament\Schemas\Components\Utilities\Set; use Filament\Notifications\Notification; use Filament\Resources\Pages\EditRecord; use Filament\Schemas\Components\StateCasts\BooleanStateCast; +use Filament\Support\Enums\Alignment; use Illuminate\Database\Eloquent\Builder; use Illuminate\Http\Client\ConnectionException; use Illuminate\Support\Arr; @@ -62,6 +62,9 @@ use Random\RandomException; class EditServer extends EditRecord { + use CanCustomizeHeaderActions; + use CanCustomizeHeaderWidgets; + protected static string $resource = ServerResource::class; private DaemonServerRepository $daemonServerRepository; @@ -140,6 +143,51 @@ class EditServer extends EditRecord // 'lg' => 1, // ]), + ToggleButtons::make('condition') + ->label(trans('admin/server.server_status')) + ->formatStateUsing(fn (Server $server) => $server->condition) + ->options(fn ($state) => [$state->value => $state->getLabel()]) + ->colors(fn ($state) => [$state->value => $state->getColor()]) + ->icons(fn ($state) => [$state->value => $state->getIcon()]) + ->columnSpan([ + 'default' => 2, + 'sm' => 1, + 'md' => 1, + 'lg' => 1, + ]) + ->hintAction( + Action::make('view_install_log') + ->label(trans('admin/server.view_install_log')) + //->visible(fn (Server $server) => $server->isFailedInstall()) + ->modalHeading('') + ->modalSubmitAction(false) + ->modalFooterActionsAlignment(Alignment::Right) + ->modalCancelActionLabel(trans('filament::components/modal.actions.close.label')) + ->form([ + MonacoEditor::make('logs') + ->hiddenLabel() + ->placeholderText(trans('admin/server.no_log')) + ->formatStateUsing(function (Server $server, DaemonServerRepository $serverRepository) { + try { + return $serverRepository->setServer($server)->getInstallLogs(); + } catch (ConnectionException) { + Notification::make() + ->title(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name])) + ->body(trans('admin/server.notifications.log_failed')) + ->color('warning') + ->warning() + ->send(); + } catch (Exception) { + return ''; + } + + return ''; + }) + ->language('shell') + ->view('filament.plugins.monaco-editor-logs'), + ]) + ), + Textarea::make('description') ->label(trans('admin/server.description')) ->columnSpanFull(), @@ -178,7 +226,7 @@ class EditServer extends EditRecord ->maxLength(255), Select::make('node_id') ->label(trans('admin/server.node')) - ->relationship('node', 'name') + ->relationship('node', 'name', fn (Builder $query) => $query->whereIn('id', auth()->user()->accessibleNodes()->pluck('id'))) ->columnSpan([ 'default' => 2, 'sm' => 1, @@ -667,17 +715,11 @@ class EditServer extends EditRecord ]), Tab::make(trans('admin/server.mounts')) ->icon('tabler-layers-linked') - ->schema([ - CheckboxList::make('mounts') - ->label('') - ->relationship('mounts') - ->options(fn (Server $server) => $server->node->mounts->filter(fn (Mount $mount) => $mount->eggs->contains($server->egg))->mapWithKeys(fn (Mount $mount) => [$mount->id => $mount->name])) - ->descriptions(fn (Server $server) => $server->node->mounts->mapWithKeys(fn (Mount $mount) => [$mount->id => "$mount->source -> $mount->target"])) - ->helperText(fn (Server $server) => $server->node->mounts->isNotEmpty() ? '' : trans('admin/server.no_mounts')) - ->columnSpanFull(), + ->schema(fn (Get $get) => [ + ServerResource::getMountCheckboxList($get), ]), Tab::make(trans('admin/server.databases')) - ->hidden(fn () => !auth()->user()->can('viewList database')) + ->hidden(fn () => !auth()->user()->can('viewAny', Database::class)) ->icon('tabler-database') ->columns(4) ->schema([ @@ -707,8 +749,8 @@ class EditServer extends EditRecord ->requiresConfirmation() ->modalIcon('tabler-database-x') ->modalHeading(trans('admin/server.delete_db_heading')) - ->modalSubmitActionLabel(fn (Get $get) => 'Delete ' . $get('database') . '?') - ->modalDescription(fn (Get $get) => trans('admin/server.delete_db') . $get('database') . '?') + ->modalSubmitActionLabel(trans('filament-actions::delete.single.label')) + ->modalDescription(fn (Get $get) => trans('admin/server.delete_db', ['name' => $get('database')])) ->action(function (DatabaseManagementService $databaseManagementService, $record) { $databaseManagementService->delete($record); $this->fillForm(); @@ -754,7 +796,7 @@ class EditServer extends EditRecord ->columnSpan(4), Actions::make([ Action::make('createDatabase') - ->authorize(fn () => auth()->user()->can('create database')) + ->authorize(fn () => auth()->user()->can('create', Database::class)) ->disabled(fn () => DatabaseHost::query()->count() < 1) ->label(fn () => DatabaseHost::query()->count() < 1 ? trans('admin/server.no_db_hosts') : trans('admin/server.create_database')) ->color(fn () => DatabaseHost::query()->count() < 1 ? 'danger' : 'primary') @@ -825,12 +867,12 @@ class EditServer extends EditRecord Action::make('toggleInstall') ->label(trans('admin/server.toggle_install')) ->disabled(fn (Server $server) => $server->isSuspended()) - ->modal(fn (Server $server) => $server->status === ServerState::InstallFailed) + ->modal(fn (Server $server) => $server->isFailedInstall()) ->modalHeading(trans('admin/server.toggle_install_failed_header')) ->modalDescription(trans('admin/server.toggle_install_failed_desc')) ->modalSubmitActionLabel(trans('admin/server.reinstall')) ->action(function (ToggleInstallService $toggleService, ReinstallServerService $reinstallService, Server $server) { - if ($server->status === ServerState::InstallFailed) { + if ($server->isFailedInstall()) { try { $reinstallService->handle($server); @@ -842,7 +884,7 @@ class EditServer extends EditRecord } catch (Exception) { Notification::make() ->title(trans('admin/server.notifications.reinstall_failed')) - ->body(trans('admin/server.error_connecting', ['node' => $server->node->name])) + ->body(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name])) ->danger() ->send(); } @@ -889,7 +931,7 @@ class EditServer extends EditRecord Notification::make() ->warning() ->title(trans('admin/server.notifications.server_suspension')) - ->body(trans('admin/server.error_connecting', ['node' => $server->node->name])) + ->body(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name])) ->send(); } }), @@ -910,7 +952,7 @@ class EditServer extends EditRecord Notification::make() ->warning() ->title(trans('admin/server.notifications.server_suspension')) - ->body(trans('admin/server.error_connecting', ['node' => $server->node->name])) + ->body(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name])) ->send(); } }), @@ -973,7 +1015,7 @@ class EditServer extends EditRecord } catch (Exception) { Notification::make() ->title(trans('admin/server.notifications.reinstall_failed')) - ->body(trans('admin/server.error_connecting', ['node' => $server->node->name])) + ->body(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name])) ->danger() ->send(); } @@ -1021,7 +1063,8 @@ class EditServer extends EditRecord ]; } - protected function getHeaderActions(): array + /** @return array */ + protected function getDefaultHeaderActions(): array { /** @var Server $server */ $server = $this->getRecord(); @@ -1091,7 +1134,7 @@ class EditServer extends EditRecord $data['description'] = ''; } - unset($data['docker'], $data['condition']); + unset($data['docker'], $data['status'], $data['allocation_id']); return $data; } diff --git a/app/Filament/Admin/Resources/ServerResource/Pages/ListServers.php b/app/Filament/Admin/Resources/ServerResource/Pages/ListServers.php index 046fbc3ec..dfac34a55 100644 --- a/app/Filament/Admin/Resources/ServerResource/Pages/ListServers.php +++ b/app/Filament/Admin/Resources/ServerResource/Pages/ListServers.php @@ -5,6 +5,8 @@ namespace App\Filament\Admin\Resources\ServerResource\Pages; use App\Filament\Server\Pages\Console; use App\Filament\Admin\Resources\ServerResource; use App\Models\Server; +use App\Traits\Filament\CanCustomizeHeaderActions; +use App\Traits\Filament\CanCustomizeHeaderWidgets; use Filament\Actions; use Filament\Resources\Pages\ListRecords; use Filament\Actions\Action; @@ -17,6 +19,9 @@ use Filament\Tables\Table; class ListServers extends ListRecords { + use CanCustomizeHeaderActions; + use CanCustomizeHeaderWidgets; + protected static string $resource = ServerResource::class; public function table(Table $table): Table @@ -68,13 +73,13 @@ class ListServers extends ListRecords ->searchable(), SelectColumn::make('allocation_id') ->label(trans('admin/server.primary_allocation')) - ->hidden(!auth()->user()->can('update server')) + ->hidden(!auth()->user()->can('update server')) // TODO: update to policy check (fn (Server $server) --> $server is empty) ->options(fn (Server $server) => $server->allocations->mapWithKeys(fn ($allocation) => [$allocation->id => $allocation->address])) ->selectablePlaceholder(false) ->sortable(), TextColumn::make('allocation_id_readonly') ->label(trans('admin/server.primary_allocation')) - ->hidden(auth()->user()->can('update server')) + ->hidden(auth()->user()->can('update server')) // TODO: update to policy check (fn (Server $server) --> $server is empty) ->state(fn (Server $server) => $server->allocation->address), TextColumn::make('image')->hidden(), TextColumn::make('backups_count') @@ -101,7 +106,8 @@ class ListServers extends ListRecords ]); } - protected function getHeaderActions(): array + /** @return array */ + protected function getDefaultHeaderActions(): array { return [ Actions\CreateAction::make() diff --git a/app/Filament/Admin/Resources/ServerResource/RelationManagers/AllocationsRelationManager.php b/app/Filament/Admin/Resources/ServerResource/RelationManagers/AllocationsRelationManager.php index a1d824713..2fd236044 100644 --- a/app/Filament/Admin/Resources/ServerResource/RelationManagers/AllocationsRelationManager.php +++ b/app/Filament/Admin/Resources/ServerResource/RelationManagers/AllocationsRelationManager.php @@ -34,15 +34,18 @@ class AllocationsRelationManager extends RelationManager { return $table ->selectCurrentPageOnly() - ->recordTitleAttribute('ip') - ->recordTitle(fn (Allocation $allocation) => "$allocation->ip:$allocation->port") + ->recordTitleAttribute('address') + ->recordTitle(fn (Allocation $allocation) => $allocation->address) ->checkIfRecordIsSelectableUsing(fn (Allocation $record) => $record->id !== $this->getOwnerRecord()->allocation_id) ->inverseRelationship('server') ->heading(trans('admin/server.allocations')) ->columns([ - TextColumn::make('ip')->label(trans('admin/server.ip_address')), - TextColumn::make('port')->label(trans('admin/server.port')), - TextInputColumn::make('ip_alias')->label(trans('admin/server.alias')), + TextColumn::make('ip') + ->label(trans('admin/server.ip_address')), + TextColumn::make('port') + ->label(trans('admin/server.port')), + TextInputColumn::make('ip_alias') + ->label(trans('admin/server.alias')), IconColumn::make('primary') ->icon(fn ($state) => match ($state) { true => 'tabler-star-filled', @@ -58,8 +61,11 @@ class AllocationsRelationManager extends RelationManager ]) ->actions([ Action::make('make-primary') + ->label(trans('admin/server.make_primary')) ->action(fn (Allocation $allocation) => $this->getOwnerRecord()->update(['allocation_id' => $allocation->id]) && $this->deselectAllTableRecords()) - ->label(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id ? '' : trans('admin/server.make_primary')), + ->hidden(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id), + DissociateAction::make() + ->hidden(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id), ]) ->headerActions([ CreateAction::make()->label(trans('admin/server.create_allocation')) @@ -69,7 +75,7 @@ class AllocationsRelationManager extends RelationManager ->options(collect($this->getOwnerRecord()->node->ipAddresses())->mapWithKeys(fn (string $ip) => [$ip => $ip])) ->label(trans('admin/server.ip_address')) ->inlineLabel() - ->ipv4() + ->ip() ->live() ->afterStateUpdated(fn (Set $set) => $set('allocation_ports', [])) ->required(), @@ -85,9 +91,7 @@ class AllocationsRelationManager extends RelationManager ->inlineLabel() ->live() ->disabled(fn (Get $get) => empty($get('allocation_ip'))) - ->afterStateUpdated(fn ($state, Set $set, Get $get) => $set('allocation_ports', - CreateServer::retrieveValidPorts($this->getOwnerRecord()->node, $state, $get('allocation_ip'))) - ) + ->afterStateUpdated(fn ($state, Set $set, Get $get) => $set('allocation_ports', CreateServer::retrieveValidPorts($this->getOwnerRecord()->node, $state, $get('allocation_ip')))) ->splitKeys(['Tab', ' ', ',']) ->required(), ]) diff --git a/app/Filament/Admin/Resources/UserResource.php b/app/Filament/Admin/Resources/UserResource.php index 2ba833e2e..6d1cb3439 100644 --- a/app/Filament/Admin/Resources/UserResource.php +++ b/app/Filament/Admin/Resources/UserResource.php @@ -6,6 +6,10 @@ use App\Filament\Admin\Resources\UserResource\Pages; use App\Filament\Admin\Resources\UserResource\RelationManagers; use App\Models\Role; use App\Models\User; +use App\Traits\Filament\CanCustomizePages; +use App\Traits\Filament\CanCustomizeRelations; +use App\Traits\Filament\CanModifyForm; +use App\Traits\Filament\CanModifyTable; use Filament\Facades\Filament; use Filament\Forms\Components\CheckboxList; use Filament\Forms\Components\TextInput; @@ -22,6 +26,11 @@ use Filament\Schemas\Schema; class UserResource extends Resource { + use CanCustomizePages; + use CanCustomizeRelations; + use CanModifyForm; + use CanModifyTable; + protected static ?string $model = User::class; protected static string|\BackedEnum|null $navigationIcon = 'tabler-users'; @@ -53,7 +62,7 @@ class UserResource extends Resource return static::getModel()::count() ?: null; } - public static function table(Table $table): Table + public static function defaultTable(Table $table): Table { return $table ->columns([ @@ -101,9 +110,9 @@ class UserResource extends Resource ]); } - public static function form(Schema $schema): Schema + public static function form(Schema $form): Schema { - return $schema + return $form ->columns(['default' => 1, 'lg' => 3]) ->components([ TextInput::make('username') @@ -148,14 +157,16 @@ class UserResource extends Resource ]); } - public static function getRelations(): array + /** @return class-string[] */ + public static function getDefaultRelations(): array { return [ RelationManagers\ServersRelationManager::class, ]; } - public static function getPages(): array + /** @return array */ + public static function getDefaultPages(): array { return [ 'index' => Pages\ListUsers::route('/'), diff --git a/app/Filament/Admin/Resources/UserResource/Pages/CreateUser.php b/app/Filament/Admin/Resources/UserResource/Pages/CreateUser.php index 8666d31a2..6bac7c970 100644 --- a/app/Filament/Admin/Resources/UserResource/Pages/CreateUser.php +++ b/app/Filament/Admin/Resources/UserResource/Pages/CreateUser.php @@ -5,11 +5,18 @@ namespace App\Filament\Admin\Resources\UserResource\Pages; use App\Filament\Admin\Resources\UserResource; use App\Models\Role; use App\Services\Users\UserCreationService; +use App\Traits\Filament\CanCustomizeHeaderActions; +use App\Traits\Filament\CanCustomizeHeaderWidgets; +use Filament\Actions\Action; +use Filament\Actions\ActionGroup; use Filament\Resources\Pages\CreateRecord; use Illuminate\Database\Eloquent\Model; class CreateUser extends CreateRecord { + use CanCustomizeHeaderActions; + use CanCustomizeHeaderWidgets; + protected static string $resource = UserResource::class; protected static bool $canCreateAnother = false; @@ -21,7 +28,8 @@ class CreateUser extends CreateRecord $this->service = $service; } - protected function getHeaderActions(): array + /** @return array */ + protected function getDefaultHeaderActions(): array { return [ $this->getCreateFormAction()->formId('form'), diff --git a/app/Filament/Admin/Resources/UserResource/Pages/EditUser.php b/app/Filament/Admin/Resources/UserResource/Pages/EditUser.php index ea594ea55..f947be702 100644 --- a/app/Filament/Admin/Resources/UserResource/Pages/EditUser.php +++ b/app/Filament/Admin/Resources/UserResource/Pages/EditUser.php @@ -5,12 +5,19 @@ namespace App\Filament\Admin\Resources\UserResource\Pages; use App\Filament\Admin\Resources\UserResource; use App\Models\User; use App\Services\Users\UserUpdateService; +use App\Traits\Filament\CanCustomizeHeaderActions; +use App\Traits\Filament\CanCustomizeHeaderWidgets; +use Filament\Actions\Action; +use Filament\Actions\ActionGroup; use Filament\Actions\DeleteAction; use Filament\Resources\Pages\EditRecord; use Illuminate\Database\Eloquent\Model; class EditUser extends EditRecord { + use CanCustomizeHeaderActions; + use CanCustomizeHeaderWidgets; + protected static string $resource = UserResource::class; private UserUpdateService $service; @@ -20,7 +27,8 @@ class EditUser extends EditRecord $this->service = $service; } - protected function getHeaderActions(): array + /** @return array */ + protected function getDefaultHeaderActions(): array { return [ DeleteAction::make() diff --git a/app/Filament/Admin/Resources/UserResource/Pages/ListUsers.php b/app/Filament/Admin/Resources/UserResource/Pages/ListUsers.php index 2ef32f2ab..fada451e0 100644 --- a/app/Filament/Admin/Resources/UserResource/Pages/ListUsers.php +++ b/app/Filament/Admin/Resources/UserResource/Pages/ListUsers.php @@ -3,14 +3,22 @@ namespace App\Filament\Admin\Resources\UserResource\Pages; use App\Filament\Admin\Resources\UserResource; +use App\Traits\Filament\CanCustomizeHeaderActions; +use App\Traits\Filament\CanCustomizeHeaderWidgets; +use Filament\Actions\Action; +use Filament\Actions\ActionGroup; use Filament\Actions\CreateAction; use Filament\Resources\Pages\ListRecords; class ListUsers extends ListRecords { + use CanCustomizeHeaderActions; + use CanCustomizeHeaderWidgets; + protected static string $resource = UserResource::class; - protected function getHeaderActions(): array + /** @return array */ + protected function getDefaultHeaderActions(): array { return [ CreateAction::make(), diff --git a/app/Filament/Admin/Resources/UserResource/Pages/ViewUser.php b/app/Filament/Admin/Resources/UserResource/Pages/ViewUser.php index 0c5933cb9..73b8336b3 100644 --- a/app/Filament/Admin/Resources/UserResource/Pages/ViewUser.php +++ b/app/Filament/Admin/Resources/UserResource/Pages/ViewUser.php @@ -3,14 +3,22 @@ namespace App\Filament\Admin\Resources\UserResource\Pages; use App\Filament\Admin\Resources\UserResource; +use App\Traits\Filament\CanCustomizeHeaderActions; +use App\Traits\Filament\CanCustomizeHeaderWidgets; +use Filament\Actions\Action; +use Filament\Actions\ActionGroup; use Filament\Actions\EditAction; use Filament\Resources\Pages\ViewRecord; class ViewUser extends ViewRecord { + use CanCustomizeHeaderActions; + use CanCustomizeHeaderWidgets; + protected static string $resource = UserResource::class; - protected function getHeaderActions(): array + /** @return array */ + protected function getDefaultHeaderActions(): array { return [ EditAction::make(), diff --git a/app/Filament/Admin/Resources/WebhookResource.php b/app/Filament/Admin/Resources/WebhookResource.php index ce962371f..e70f09627 100644 --- a/app/Filament/Admin/Resources/WebhookResource.php +++ b/app/Filament/Admin/Resources/WebhookResource.php @@ -6,9 +6,14 @@ use App\Filament\Admin\Resources\WebhookResource\Pages; use App\Models\WebhookConfiguration; use Filament\Actions\CreateAction; use Filament\Actions\DeleteBulkAction; +use App\Traits\Filament\CanCustomizePages; +use App\Traits\Filament\CanCustomizeRelations; +use App\Traits\Filament\CanModifyForm; +use App\Traits\Filament\CanModifyTable; use Filament\Forms\Components\CheckboxList; use Filament\Forms\Components\TextInput; use Filament\Schemas\Components\Form; +use Filament\Resources\Pages\PageRegistration; use Filament\Resources\Resource; use Filament\Actions\DeleteAction; use Filament\Actions\EditAction; @@ -19,6 +24,11 @@ use Filament\Schemas\Schema; class WebhookResource extends Resource { + use CanCustomizePages; + use CanCustomizeRelations; + use CanModifyForm; + use CanModifyTable; + protected static ?string $model = WebhookConfiguration::class; protected static string|\BackedEnum|null $navigationIcon = 'tabler-webhook'; @@ -50,7 +60,7 @@ class WebhookResource extends Resource return trans('admin/dashboard.advanced'); } - public static function table(Table $table): Table + public static function defaultTable(Table $table): Table { return $table ->columns([ @@ -76,9 +86,9 @@ class WebhookResource extends Resource ]); } - public static function form(Schema $schema): Schema + public static function form(Schema $form): Schema { - return $schema + return $form ->schema([ TextInput::make('endpoint') ->label(trans('admin/webhook.endpoint')) @@ -99,7 +109,8 @@ class WebhookResource extends Resource ]); } - public static function getPages(): array + /** @return array */ + public static function getDefaultPages(): array { return [ 'index' => Pages\ListWebhookConfigurations::route('/'), diff --git a/app/Filament/Admin/Resources/WebhookResource/Pages/CreateWebhookConfiguration.php b/app/Filament/Admin/Resources/WebhookResource/Pages/CreateWebhookConfiguration.php index f38ebb7de..7eccf101b 100644 --- a/app/Filament/Admin/Resources/WebhookResource/Pages/CreateWebhookConfiguration.php +++ b/app/Filament/Admin/Resources/WebhookResource/Pages/CreateWebhookConfiguration.php @@ -3,15 +3,23 @@ namespace App\Filament\Admin\Resources\WebhookResource\Pages; use App\Filament\Admin\Resources\WebhookResource; +use App\Traits\Filament\CanCustomizeHeaderActions; +use App\Traits\Filament\CanCustomizeHeaderWidgets; +use Filament\Actions\Action; +use Filament\Actions\ActionGroup; use Filament\Resources\Pages\CreateRecord; class CreateWebhookConfiguration extends CreateRecord { + use CanCustomizeHeaderActions; + use CanCustomizeHeaderWidgets; + protected static string $resource = WebhookResource::class; protected static bool $canCreateAnother = false; - protected function getHeaderActions(): array + /** @return array */ + protected function getDefaultHeaderActions(): array { return [ $this->getCreateFormAction()->formId('form'), diff --git a/app/Filament/Admin/Resources/WebhookResource/Pages/EditWebhookConfiguration.php b/app/Filament/Admin/Resources/WebhookResource/Pages/EditWebhookConfiguration.php index bc9f69528..9e0313c5a 100644 --- a/app/Filament/Admin/Resources/WebhookResource/Pages/EditWebhookConfiguration.php +++ b/app/Filament/Admin/Resources/WebhookResource/Pages/EditWebhookConfiguration.php @@ -3,14 +3,22 @@ namespace App\Filament\Admin\Resources\WebhookResource\Pages; use App\Filament\Admin\Resources\WebhookResource; +use App\Traits\Filament\CanCustomizeHeaderActions; +use App\Traits\Filament\CanCustomizeHeaderWidgets; +use Filament\Actions\Action; +use Filament\Actions\ActionGroup; use Filament\Actions\DeleteAction; use Filament\Resources\Pages\EditRecord; class EditWebhookConfiguration extends EditRecord { + use CanCustomizeHeaderActions; + use CanCustomizeHeaderWidgets; + protected static string $resource = WebhookResource::class; - protected function getHeaderActions(): array + /** @return array */ + protected function getDefaultHeaderActions(): array { return [ DeleteAction::make(), diff --git a/app/Filament/Admin/Resources/WebhookResource/Pages/ListWebhookConfigurations.php b/app/Filament/Admin/Resources/WebhookResource/Pages/ListWebhookConfigurations.php index 722a0f167..ca418c886 100644 --- a/app/Filament/Admin/Resources/WebhookResource/Pages/ListWebhookConfigurations.php +++ b/app/Filament/Admin/Resources/WebhookResource/Pages/ListWebhookConfigurations.php @@ -4,14 +4,22 @@ namespace App\Filament\Admin\Resources\WebhookResource\Pages; use App\Filament\Admin\Resources\WebhookResource; use App\Models\WebhookConfiguration; +use App\Traits\Filament\CanCustomizeHeaderActions; +use App\Traits\Filament\CanCustomizeHeaderWidgets; +use Filament\Actions\Action; +use Filament\Actions\ActionGroup; use Filament\Actions\CreateAction; use Filament\Resources\Pages\ListRecords; class ListWebhookConfigurations extends ListRecords { + use CanCustomizeHeaderActions; + use CanCustomizeHeaderWidgets; + protected static string $resource = WebhookResource::class; - protected function getHeaderActions(): array + /** @return array */ + protected function getDefaultHeaderActions(): array { return [ CreateAction::make() diff --git a/app/Filament/Admin/Resources/WebhookResource/Pages/ViewWebhookConfiguration.php b/app/Filament/Admin/Resources/WebhookResource/Pages/ViewWebhookConfiguration.php index cd7d32712..be738cfd8 100644 --- a/app/Filament/Admin/Resources/WebhookResource/Pages/ViewWebhookConfiguration.php +++ b/app/Filament/Admin/Resources/WebhookResource/Pages/ViewWebhookConfiguration.php @@ -3,14 +3,22 @@ namespace App\Filament\Admin\Resources\WebhookResource\Pages; use App\Filament\Admin\Resources\WebhookResource; +use App\Traits\Filament\CanCustomizeHeaderActions; +use App\Traits\Filament\CanCustomizeHeaderWidgets; +use Filament\Actions\Action; +use Filament\Actions\ActionGroup; use Filament\Actions\EditAction; use Filament\Resources\Pages\ViewRecord; class ViewWebhookConfiguration extends ViewRecord { + use CanCustomizeHeaderActions; + use CanCustomizeHeaderWidgets; + protected static string $resource = WebhookResource::class; - protected function getHeaderActions(): array + /** @return array */ + protected function getDefaultHeaderActions(): array { return [ EditAction::make(), diff --git a/app/Filament/Admin/Widgets/CanaryWidget.php b/app/Filament/Admin/Widgets/CanaryWidget.php index 5d90a2a32..838a9f194 100644 --- a/app/Filament/Admin/Widgets/CanaryWidget.php +++ b/app/Filament/Admin/Widgets/CanaryWidget.php @@ -3,14 +3,13 @@ namespace App\Filament\Admin\Widgets; use Filament\Actions\Action; -use Filament\Widgets\Widget; +use Filament\Forms\Components\Actions\Action; +use Filament\Forms\Components\Placeholder; +use Filament\Forms\Components\Section; +use Filament\Forms\Form; -class CanaryWidget extends Widget +class CanaryWidget extends FormWidget { - protected string $view = 'filament.admin.widgets.canary-widget'; - - protected static bool $isLazy = false; - protected static ?int $sort = 1; public static function canView(): bool @@ -18,14 +17,28 @@ class CanaryWidget extends Widget return config('app.version') === 'canary'; } - public function getViewData(): array + public function form(Form $form): Form { - return [ - 'action' => Action::make('github') - ->label(trans('admin/dashboard.sections.intro-developers.button_issues')) - ->icon('tabler-brand-github') - ->url('https://github.com/pelican-dev/panel/issues', true) - ->toHtmlString(), - ]; + return $form + ->schema([ + Section::make(trans('admin/dashboard.sections.intro-developers.heading')) + ->icon('tabler-code') + ->iconColor('primary') + ->collapsible() + ->collapsed() + ->persistCollapsed() + ->schema([ + Placeholder::make('') + ->content(trans('admin/dashboard.sections.intro-developers.content')), + Placeholder::make('') + ->content(trans('admin/dashboard.sections.intro-developers.extra_note')), + ]) + ->headerActions([ + Action::make('issues') + ->label(trans('admin/dashboard.sections.intro-developers.button_issues')) + ->icon('tabler-brand-github') + ->url('https://github.com/pelican-dev/panel/issues', true), + ]), + ]); } } diff --git a/app/Filament/Admin/Widgets/FormWidget.php b/app/Filament/Admin/Widgets/FormWidget.php new file mode 100644 index 000000000..844d37918 --- /dev/null +++ b/app/Filament/Admin/Widgets/FormWidget.php @@ -0,0 +1,16 @@ + Action::make('docs') - ->label(trans('admin/dashboard.sections.intro-help.button_docs')) - ->icon('tabler-speedboat') - ->url('https://pelican.dev/docs', true) - ->toHtmlString(), - ]; + return $form + ->schema([ + Section::make(trans('admin/dashboard.sections.intro-help.heading')) + ->icon('tabler-question-mark') + ->iconColor('info') + ->collapsible() + ->persistCollapsed() + ->schema([ + Placeholder::make('') + ->content(trans('admin/dashboard.sections.intro-help.content')), + ]) + ->headerActions([ + Action::make('docs') + ->label(trans('admin/dashboard.sections.intro-help.button_docs')) + ->icon('tabler-speedboat') + ->url('https://pelican.dev/docs', true), + ]), + ]); } } diff --git a/app/Filament/Admin/Widgets/NoNodesWidget.php b/app/Filament/Admin/Widgets/NoNodesWidget.php index c14019774..40751870f 100644 --- a/app/Filament/Admin/Widgets/NoNodesWidget.php +++ b/app/Filament/Admin/Widgets/NoNodesWidget.php @@ -4,15 +4,13 @@ namespace App\Filament\Admin\Widgets; use App\Filament\Admin\Resources\NodeResource\Pages\CreateNode; use App\Models\Node; -use Filament\Actions\Action; -use Filament\Widgets\Widget; +use Filament\Forms\Components\Actions\Action; +use Filament\Forms\Components\Placeholder; +use Filament\Forms\Components\Section; +use Filament\Forms\Form; -class NoNodesWidget extends Widget +class NoNodesWidget extends FormWidget { - protected string $view = 'filament.admin.widgets.no-nodes-widget'; - - protected static bool $isLazy = false; - protected static ?int $sort = 2; public static function canView(): bool @@ -20,14 +18,25 @@ class NoNodesWidget extends Widget return Node::count() <= 0; } - public function getViewData(): array + public function form(Form $form): Form { - return [ - 'action' => Action::make('create-node') - ->label(trans('admin/dashboard.sections.intro-first-node.button_label')) - ->icon('tabler-server-2') - ->url(CreateNode::getUrl()) - ->toHtmlString(), - ]; + return $form + ->schema([ + Section::make(trans('admin/dashboard.sections.intro-first-node.heading')) + ->icon('tabler-server-2') + ->iconColor('primary') + ->collapsible() + ->persistCollapsed() + ->schema([ + Placeholder::make('') + ->content(trans('admin/dashboard.sections.intro-first-node.content')), + ]) + ->headerActions([ + Action::make('create-node') + ->label(trans('admin/dashboard.sections.intro-first-node.button_label')) + ->icon('tabler-server-2') + ->url(CreateNode::getUrl()), + ]), + ]); } } diff --git a/app/Filament/Admin/Widgets/SupportWidget.php b/app/Filament/Admin/Widgets/SupportWidget.php index dbe8ecbcc..ce42762f7 100644 --- a/app/Filament/Admin/Widgets/SupportWidget.php +++ b/app/Filament/Admin/Widgets/SupportWidget.php @@ -2,26 +2,37 @@ namespace App\Filament\Admin\Widgets; -use Filament\Actions\Action; -use Filament\Widgets\Widget; +use Filament\Forms\Components\Actions\Action; +use Filament\Forms\Components\Placeholder; +use Filament\Forms\Components\Section; +use Filament\Forms\Form; -class SupportWidget extends Widget +class SupportWidget extends FormWidget { - protected string $view = 'filament.admin.widgets.support-widget'; - - protected static bool $isLazy = false; - protected static ?int $sort = 3; - public function getViewData(): array + public function form(Form $form): Form { - return [ - 'action' => Action::make('donate') - ->label(trans('admin/dashboard.sections.intro-support.button_donate')) - ->icon('tabler-cash') - ->url('https://pelican.dev/donate', true) - ->color('success') - ->toHtmlString(), - ]; + return $form + ->schema([ + Section::make(trans('admin/dashboard.sections.intro-support.heading')) + ->icon('tabler-heart-filled') + ->iconColor('danger') + ->collapsible() + ->persistCollapsed() + ->schema([ + Placeholder::make('') + ->content(trans('admin/dashboard.sections.intro-support.content')), + Placeholder::make('') + ->content(trans('admin/dashboard.sections.intro-support.extra_note')), + ]) + ->headerActions([ + Action::make('donate') + ->label(trans('admin/dashboard.sections.intro-support.button_donate')) + ->icon('tabler-cash') + ->url('https://pelican.dev/donate', true) + ->color('success'), + ]), + ]); } } diff --git a/app/Filament/Admin/Widgets/UpdateWidget.php b/app/Filament/Admin/Widgets/UpdateWidget.php index 382b5abb5..95e7db4a9 100644 --- a/app/Filament/Admin/Widgets/UpdateWidget.php +++ b/app/Filament/Admin/Widgets/UpdateWidget.php @@ -3,15 +3,13 @@ namespace App\Filament\Admin\Widgets; use App\Services\Helpers\SoftwareVersionService; -use Filament\Actions\Action; -use Filament\Widgets\Widget; +use Filament\Forms\Components\Actions\Action; +use Filament\Forms\Components\Placeholder; +use Filament\Forms\Components\Section; +use Filament\Forms\Form; -class UpdateWidget extends Widget +class UpdateWidget extends FormWidget { - protected string $view = 'filament.admin.widgets.update-widget'; - - protected static bool $isLazy = false; - protected static ?int $sort = 0; private SoftwareVersionService $softwareVersionService; @@ -21,18 +19,34 @@ class UpdateWidget extends Widget $this->softwareVersionService = $softwareVersionService; } - public function getViewData(): array + public function form(Form $form): Form { - return [ - 'version' => $this->softwareVersionService->currentPanelVersion(), - 'latestVersion' => $this->softwareVersionService->latestPanelVersion(), - 'isLatest' => $this->softwareVersionService->isLatestPanel(), - 'action' => Action::make('update') - ->label(trans('admin/dashboard.sections.intro-update-available.heading')) - ->icon('tabler-clipboard-text') - ->url('https://pelican.dev/docs/panel/update', true) - ->color('warning') - ->toHtmlString(), - ]; + $isLatest = $this->softwareVersionService->isLatestPanel(); + + return $form + ->schema([ + $isLatest + ? Section::make(trans('admin/dashboard.sections.intro-no-update.heading')) + ->icon('tabler-checkbox') + ->iconColor('success') + ->schema([ + Placeholder::make('') + ->content(trans('admin/dashboard.sections.intro-no-update.content', ['version' => $this->softwareVersionService->currentPanelVersion()])), + ]) + : Section::make(trans('admin/dashboard.sections.intro-update-available.heading')) + ->icon('tabler-info-circle') + ->iconColor('warning') + ->schema([ + Placeholder::make('') + ->content(trans('admin/dashboard.sections.intro-update-available.content', ['latestVersion' => $this->softwareVersionService->latestPanelVersion()])), + ]) + ->headerActions([ + Action::make('update') + ->label(trans('admin/dashboard.sections.intro-update-available.heading')) + ->icon('tabler-clipboard-text') + ->url('https://pelican.dev/docs/panel/update', true) + ->color('warning'), + ]), + ]); } } diff --git a/app/Filament/App/Resources/ServerResource/Pages/ListServers.php b/app/Filament/App/Resources/ServerResource/Pages/ListServers.php index ec06307b2..aa80c3866 100644 --- a/app/Filament/App/Resources/ServerResource/Pages/ListServers.php +++ b/app/Filament/App/Resources/ServerResource/Pages/ListServers.php @@ -6,7 +6,13 @@ use App\Enums\ServerResourceType; use App\Filament\App\Resources\ServerResource; use App\Filament\Components\Tables\Columns\ServerEntryColumn; use App\Filament\Server\Pages\Console; +use App\Models\Permission; use App\Models\Server; +use App\Repositories\Daemon\DaemonPowerRepository; +use App\Traits\Filament\CanCustomizeHeaderActions; +use App\Traits\Filament\CanCustomizeHeaderWidgets; +use Filament\Notifications\Notification; +use Filament\Resources\Components\Tab; use Filament\Resources\Pages\ListRecords; use Filament\Schemas\Components\Tabs\Tab; use Filament\Support\Enums\TextSize; @@ -16,48 +22,64 @@ use Filament\Tables\Columns\TextColumn; use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Table; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Http\Client\ConnectionException; +use Livewire\Attributes\On; class ListServers extends ListRecords { + use CanCustomizeHeaderActions; + use CanCustomizeHeaderWidgets; + protected static string $resource = ServerResource::class; public const DANGER_THRESHOLD = 0.9; public const WARNING_THRESHOLD = 0.7; - public function table(Table $table): Table - { - $baseQuery = auth()->user()->accessibleServers(); + private DaemonPowerRepository $daemonPowerRepository; - $viewOne = [ + public function boot(): void + { + $this->daemonPowerRepository = new DaemonPowerRepository(); + } + + /** @return Stack[] */ + protected function gridColumns(): array + { + return [ + Stack::make([ + ServerEntryColumn::make('server_entry') + ->searchable(['name']), + ]), + ]; + } + + /** @return Column[] */ + protected function tableColumns(): array + { + return [ TextColumn::make('condition') ->label('') ->default('unknown') ->wrap() ->size(TextSize::Medium) ->badge() - ->alignCenter() ->tooltip(fn (Server $server) => $server->formatResource('uptime', type: ServerResourceType::Time)) ->icon(fn (Server $server) => $server->condition->getIcon()) ->color(fn (Server $server) => $server->condition->getColor()), - ]; - - $viewTwo = [ TextColumn::make('name') + ->description(fn (Server $server) => $server->description) + ->grow() ->label('') ->size(TextSize::Medium) ->searchable(), - TextColumn::make('iNeedAName') + TextColumn::make('allocation.address') ->label('') ->badge() - ->size(TextSize::Medium) - ->copyable(request()->isSecure()) - ->copyMessage(fn (Server $server, string $state) => 'Copied ' . $server->allocation->address) - ->state(fn (Server $server) => $server->allocation->address), - ]; - - $viewThree = [ + ->visibleFrom('md') + ->copyable(request()->isSecure()), TextColumn::make('cpuUsage') + ->label('Resources') ->label('') ->size(TextSize::Medium) ->icon('tabler-cpu') @@ -66,6 +88,7 @@ class ListServers extends ListRecords ->color(fn (Server $server) => $this->getResourceColor($server, 'cpu')), TextColumn::make('memoryUsage') ->label('') + ->icon('tabler-device-desktop-analytics') ->size(TextSize::Medium) ->icon('tabler-memory') ->tooltip(fn (Server $server) => 'Usage Limit: ' . $server->formatResource('memory', limit: true)) @@ -73,42 +96,30 @@ class ListServers extends ListRecords ->color(fn (Server $server) => $this->getResourceColor($server, 'memory')), TextColumn::make('diskUsage') ->label('') + ->icon('tabler-device-sd-card') ->size(TextSize::Medium) ->icon('tabler-device-floppy') ->tooltip(fn (Server $server) => 'Usage Limit: ' . $server->formatResource('disk', limit: true)) ->state(fn (Server $server) => $server->formatResource('disk_bytes')) ->color(fn (Server $server) => $this->getResourceColor($server, 'disk')), ]; + } + + public function table(Table $table): Table + { + $baseQuery = auth()->user()->accessibleServers(); + + $usingGrid = (auth()->user()->getCustomization()['dashboard_layout'] ?? 'grid') === 'grid'; return $table ->paginated(false) ->query(fn () => $baseQuery) ->poll('15s') - ->columns( - (auth()->user()->getCustomization()['dashboard_layout'] ?? 'grid') === 'grid' - ? [ - Stack::make([ - ServerEntryColumn::make('server_entry') - ->searchable(['name']), - ]), - ] - : [ - ColumnGroup::make('Status') - ->label('Status') - ->columns($viewOne), - ColumnGroup::make('Server') - ->label('Servers') - ->columns($viewTwo), - ColumnGroup::make('Resources') - ->label('Resources') - ->columns($viewThree), - ] - ) - ->recordUrl(fn (Server $server) => Console::getUrl(panel: 'server', tenant: $server)) - ->contentGrid([ - 'default' => 1, - 'md' => 2, - ]) + ->columns($usingGrid ? $this->gridColumns() : $this->tableColumns()) + ->recordUrl(!$usingGrid ? (fn (Server $server) => Console::getUrl(panel: 'server', tenant: $server)) : null) + ->actions(!$usingGrid ? ActionGroup::make(static::getPowerActions()) : []) + ->actionsAlignment(Alignment::Center->value) + ->contentGrid($usingGrid ? ['default' => 1, 'md' => 2] : null) ->emptyStateIcon('tabler-brand-docker') ->emptyStateDescription('') ->emptyStateHeading(fn () => $this->activeTab === 'my' ? 'You don\'t own any servers!' : 'You don\'t have access to any servers!') @@ -151,36 +162,33 @@ class ListServers extends ListRecords ]; } - public function getResourceColor(Server $server, string $resource): ?string + protected function getResourceColor(Server $server, string $resource): ?string { $current = null; $limit = null; switch ($resource) { case 'cpu': - $current = $server->resources()['cpu_absolute'] ?? 0; + $current = $server->retrieveResources()['cpu_absolute'] ?? 0; $limit = $server->cpu; if ($server->cpu === 0) { return null; } break; - case 'memory': - $current = $server->resources()['memory_bytes'] ?? 0; + $current = $server->retrieveResources()['memory_bytes'] ?? 0; $limit = $server->memory * 2 ** 20; if ($server->memory === 0) { return null; } break; - case 'disk': - $current = $server->resources()['disk_bytes'] ?? 0; + $current = $server->retrieveResources()['disk_bytes'] ?? 0; $limit = $server->disk * 2 ** 20; if ($server->disk === 0) { return null; } break; - default: return null; } @@ -194,6 +202,60 @@ class ListServers extends ListRecords } return null; + } + #[On('powerAction')] + public function powerAction(Server $server, string $action): void + { + try { + $this->daemonPowerRepository->setServer($server)->send($action); + + Notification::make() + ->title('Power Action') + ->body($action . ' sent to ' . $server->name) + ->success() + ->send(); + + cache()->forget("servers.$server->uuid.status"); + + $this->redirect(self::getUrl(['activeTab' => $this->activeTab])); + } catch (ConnectionException) { + Notification::make() + ->title(trans('exceptions.node.error_connecting', ['node' => $server->node->name])) + ->danger() + ->send(); + } + } + + /** @return Action[] */ + public static function getPowerActions(): array + { + return [ + Action::make('start') + ->color('primary') + ->icon('tabler-player-play-filled') + ->authorize(fn (Server $server) => auth()->user()->can(Permission::ACTION_CONTROL_START, $server)) + ->visible(fn (Server $server) => !$server->isInConflictState() & $server->retrieveStatus()->isStartable()) + ->dispatch('powerAction', fn (Server $server) => ['server' => $server, 'action' => 'start']), + Action::make('restart') + ->color('gray') + ->icon('tabler-reload') + ->authorize(fn (Server $server) => auth()->user()->can(Permission::ACTION_CONTROL_RESTART, $server)) + ->visible(fn (Server $server) => !$server->isInConflictState() & $server->retrieveStatus()->isRestartable()) + ->dispatch('powerAction', fn (Server $server) => ['server' => $server, 'action' => 'restart']), + Action::make('stop') + ->color('danger') + ->icon('tabler-player-stop-filled') + ->authorize(fn (Server $server) => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server)) + ->visible(fn (Server $server) => !$server->isInConflictState() & $server->retrieveStatus()->isStoppable()) + ->dispatch('powerAction', fn (Server $server) => ['server' => $server, 'action' => 'stop']), + Action::make('kill') + ->color('danger') + ->icon('tabler-alert-square') + ->tooltip('This can result in data corruption and/or data loss!') + ->authorize(fn (Server $server) => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server)) + ->visible(fn (Server $server) => !$server->isInConflictState() & $server->retrieveStatus()->isKillable()) + ->dispatch('powerAction', fn (Server $server) => ['server' => $server, 'action' => 'kill']), + ]; } } diff --git a/app/Filament/Components/Forms/Actions/RotateDatabasePasswordAction.php b/app/Filament/Components/Forms/Actions/RotateDatabasePasswordAction.php index 57d545a11..ffb0150e8 100644 --- a/app/Filament/Components/Forms/Actions/RotateDatabasePasswordAction.php +++ b/app/Filament/Components/Forms/Actions/RotateDatabasePasswordAction.php @@ -25,7 +25,7 @@ class RotateDatabasePasswordAction extends Action $this->icon('tabler-refresh'); - $this->authorize(fn (Database $database) => auth()->user()->can('update database', $database)); + $this->authorize(fn (Database $database) => auth()->user()->can('update', $database)); $this->modalHeading(trans('admin/databasehost.rotate_password')); diff --git a/app/Filament/Components/Tables/Columns/ServerEntryColumn.php b/app/Filament/Components/Tables/Columns/ServerEntryColumn.php index 3d02a8497..52e81c25e 100644 --- a/app/Filament/Components/Tables/Columns/ServerEntryColumn.php +++ b/app/Filament/Components/Tables/Columns/ServerEntryColumn.php @@ -6,5 +6,5 @@ use Filament\Tables\Columns\Column; class ServerEntryColumn extends Column { - protected string $view = 'tables.columns.server-entry-column'; + protected string $view = 'livewire.columns.server-entry-column'; } diff --git a/app/Filament/Pages/Auth/EditProfile.php b/app/Filament/Pages/Auth/EditProfile.php index 5b6af77fa..329c4949a 100644 --- a/app/Filament/Pages/Auth/EditProfile.php +++ b/app/Filament/Pages/Auth/EditProfile.php @@ -12,6 +12,8 @@ use App\Services\Helpers\LanguageService; use App\Services\Users\ToggleTwoFactorService; use App\Services\Users\TwoFactorSetupService; use App\Services\Users\UserUpdateService; +use App\Traits\Filament\CanCustomizeHeaderActions; +use App\Traits\Filament\CanCustomizeHeaderWidgets; use chillerlan\QRCode\Common\EccLevel; use chillerlan\QRCode\Common\Version; use chillerlan\QRCode\QRCode; @@ -41,6 +43,7 @@ use Filament\Support\Exceptions\Halt; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Facades\Storage; use Illuminate\Support\HtmlString; use Illuminate\Validation\Rules\Password; use Laravel\Socialite\Facades\Socialite; @@ -50,6 +53,9 @@ use Laravel\Socialite\Facades\Socialite; */ class EditProfile extends \Filament\Auth\Pages\EditProfile { + use CanCustomizeHeaderActions; + use CanCustomizeHeaderWidgets; + private ToggleTwoFactorService $toggleTwoFactorService; public function boot(ToggleTwoFactorService $toggleTwoFactorService): void @@ -288,10 +294,12 @@ class EditProfile extends \Filament\Auth\Pages\EditProfile $get('allowed_ips'), ); - Activity::event('user:api-key.create') - ->subject($token->accessToken) - ->property('identifier', $token->accessToken->identifier) - ->log(); + Activity::event('user:api-key.create') + ->actor($user) + ->subject($user) + ->subject($token->accessToken) + ->property('identifier', $token->accessToken->identifier) + ->log(); Notification::make() ->title(trans('profile.key_created')) @@ -363,6 +371,95 @@ class EditProfile extends \Filament\Auth\Pages\EditProfile 'grid' => trans('profile.grid'), 'table' => trans('profile.table'), ]), + Section::make(trans('profile.console')) + ->collapsible() + ->icon('tabler-brand-tabler') + ->columns(4) + ->schema([ + TextInput::make('console_font_size') + ->label(trans('profile.font_size')) + ->columnSpan(1) + ->minValue(1) + ->numeric() + ->required() + ->default(14), + Select::make('console_font') + ->label(trans('profile.font')) + ->required() + ->options(function () { + $fonts = [ + 'monospace' => 'monospace', //default + ]; + + if (!Storage::disk('public')->exists('fonts')) { + Storage::disk('public')->makeDirectory('fonts'); + $this->fillForm(); + } + + foreach (Storage::disk('public')->allFiles('fonts') as $file) { + $fileInfo = pathinfo($file); + + if ($fileInfo['extension'] === 'ttf') { + $fonts[$fileInfo['filename']] = $fileInfo['filename']; + } + } + + return $fonts; + }) + ->reactive() + ->default('monospace') + ->afterStateUpdated(fn ($state, Set $set) => $set('font_preview', $state)), + Placeholder::make('font_preview') + ->label(trans('profile.font_preview')) + ->columnSpan(2) + ->content(function (Get $get) { + $fontName = $get('console_font') ?? 'monospace'; + $fontSize = $get('console_font_size') . 'px'; + $style = << + {$style} + + The quick blue pelican jumps over the lazy pterodactyl. :) + HTML); + }), + TextInput::make('console_graph_period') + ->label(trans('profile.graph_period')) + ->suffix(trans('profile.seconds')) + ->hintIcon('tabler-question-mark') + ->hintIconTooltip(trans('profile.graph_period_helper')) + ->columnSpan(2) + ->numeric() + ->default(30) + ->minValue(10) + ->maxValue(120) + ->required(), + TextInput::make('console_rows') + ->label(trans('profile.rows')) + ->minValue(1) + ->numeric() + ->required() + ->columnSpan(2) + ->default(30), + ]), ]), Section::make(trans('profile.console')) ->collapsible() @@ -435,7 +532,8 @@ class EditProfile extends \Filament\Auth\Pages\EditProfile return []; } - protected function getHeaderActions(): array + /** @return array */ + protected function getDefaultHeaderActions(): array { return [ $this->getSaveFormAction()->formId('form'), @@ -446,12 +544,14 @@ class EditProfile extends \Filament\Auth\Pages\EditProfile protected function mutateFormDataBeforeSave(array $data): array { $moarbetterdata = [ + 'console_font' => $data['console_font'], 'console_font_size' => $data['console_font_size'], 'console_rows' => $data['console_rows'], + 'console_graph_period' => $data['console_graph_period'], 'dashboard_layout' => $data['dashboard_layout'], ]; - unset($data['dashboard_layout'], $data['console_font_size'], $data['console_rows']); + unset($data['console_font'],$data['console_font_size'], $data['console_rows'], $data['dashboard_layout']); $data['customization'] = json_encode($moarbetterdata); return $data; @@ -461,8 +561,10 @@ class EditProfile extends \Filament\Auth\Pages\EditProfile { $moarbetterdata = json_decode($data['customization'], true); + $data['console_font'] = $moarbetterdata['console_font'] ?? 'monospace'; $data['console_font_size'] = $moarbetterdata['console_font_size'] ?? 14; $data['console_rows'] = $moarbetterdata['console_rows'] ?? 30; + $data['console_graph_period'] = $moarbetterdata['console_graph_period'] ?? 30; $data['dashboard_layout'] = $moarbetterdata['dashboard_layout'] ?? 'grid'; return $data; diff --git a/app/Filament/Pages/Auth/Login.php b/app/Filament/Pages/Auth/Login.php index 907fccc4d..4afae9cf8 100644 --- a/app/Filament/Pages/Auth/Login.php +++ b/app/Filament/Pages/Auth/Login.php @@ -2,8 +2,10 @@ namespace App\Filament\Pages\Auth; +use App\Events\Auth\ProvidedAuthenticationToken; use App\Extensions\Captcha\Providers\CaptchaProvider; use App\Extensions\OAuth\Providers\OAuthProvider; +use App\Facades\Activity; use App\Models\User; use Filament\Auth\Http\Responses\LoginResponse; use Filament\Facades\Filament; @@ -54,14 +56,37 @@ class Login extends \Filament\Auth\Pages\Login if ($token === null) { $this->verifyTwoFactor = true; + Activity::event('auth:checkpoint') + ->withRequestMetadata() + ->subject($user) + ->log(); + return null; } - $isValidToken = $this->google2FA->verifyKey( - $user->totp_secret, - $token, - Config::integer('panel.auth.2fa.window'), - ); + $isValidToken = false; + if (strlen($token) === $this->google2FA->getOneTimePasswordLength()) { + $isValidToken = $this->google2FA->verifyKey( + $user->totp_secret, + $token, + Config::integer('panel.auth.2fa.window'), + ); + + if ($isValidToken) { + event(new ProvidedAuthenticationToken($user)); + } + } else { + foreach ($user->recoveryTokens as $recoveryToken) { + if (password_verify($token, $recoveryToken->token)) { + $isValidToken = true; + $recoveryToken->delete(); + + event(new ProvidedAuthenticationToken($user, true)); + + break; + } + } + } if (!$isValidToken) { // Buffer to prevent bruteforce @@ -103,7 +128,9 @@ class Login extends \Filament\Auth\Pages\Login { return TextInput::make('2fa') ->label(trans('auth.two-factor-code')) - ->hidden(fn () => !$this->verifyTwoFactor) + ->hintIcon('tabler-question-mark') + ->hintIconTooltip(trans('auth.two-factor-hint')) + ->visible(fn () => $this->verifyTwoFactor) ->required() ->live(); } diff --git a/app/Filament/Server/Components/SmallStatBlock.php b/app/Filament/Server/Components/SmallStatBlock.php index c8872ac12..f63114dbd 100644 --- a/app/Filament/Server/Components/SmallStatBlock.php +++ b/app/Filament/Server/Components/SmallStatBlock.php @@ -2,59 +2,29 @@ namespace App\Filament\Server\Components; -use BackedEnum; use Closure; -use Filament\Schemas\Components\Component; -use Filament\Schemas\Components\Concerns\CanOpenUrl; -use Filament\Schemas\Components\Concerns\HasDescription; -use Illuminate\Contracts\Support\Htmlable; +use Filament\Support\Concerns\EvaluatesClosures; +use Filament\Widgets\StatsOverviewWidget\Stat; +use Illuminate\Contracts\View\View; class SmallStatBlock extends Component { - use CanOpenUrl; - use HasDescription; + use EvaluatesClosures; - protected string $view = 'filament.components.server-small-data-block'; + protected bool|Closure $copyOnClick = false; - protected string|BackedEnum|null $icon = null; + public function copyOnClick(bool|Closure $copyOnClick = true): static + { + $this->copyOnClick = $copyOnClick; protected string $value; - final public function __construct(string $label, string $value) + public function shouldCopyOnClick(): bool { - $this->label($label); - $this->value($value); + return $this->evaluate($this->copyOnClick); } - /** - * @return SmallStatBlock - */ - public static function make(string $label, string $value): static - { - return app(static::class, ['label' => $label, 'value' => $value]); - } - - public function icon(string|BackedEnum|null $icon): static - { - $this->icon = $icon; - - return $this; - } - - /** - * @return SmallStatBlock - */ - private function value(string $value): static - { - $this->value = $value; - - return $this; - } - - /** - * @return scalar | Htmlable | Closure - */ - public function getValue(): mixed + public function render(): View { return value($this->value); } diff --git a/app/Filament/Server/Components/StatBlock.php b/app/Filament/Server/Components/StatBlock.php deleted file mode 100644 index 5b4ea04eb..000000000 --- a/app/Filament/Server/Components/StatBlock.php +++ /dev/null @@ -1,44 +0,0 @@ -label = $label; - - return $this; - } - - public function value($value): static - { - $this->value = $value; - - return $this; - } - - public function getLabel(): string - { - return $this->label; - } - - public function getValue(): mixed - { - return value($this->value); - } - - public function toHtml(): string - { - return $this->render()->render(); - } -} diff --git a/app/Filament/Server/Pages/Console.php b/app/Filament/Server/Pages/Console.php index 976362350..0bb1d376f 100644 --- a/app/Filament/Server/Pages/Console.php +++ b/app/Filament/Server/Pages/Console.php @@ -9,13 +9,16 @@ use App\Extensions\Features\FeatureProvider; use App\Filament\Server\Widgets\ServerConsole; use App\Filament\Server\Widgets\ServerCpuChart; use App\Filament\Server\Widgets\ServerMemoryChart; -// use App\Filament\Server\Widgets\ServerNetworkChart; +use App\Filament\Server\Widgets\ServerNetworkChart; use App\Filament\Server\Widgets\ServerOverview; use App\Livewire\AlertBanner; use App\Models\Permission; use App\Models\Server; +use App\Traits\Filament\CanCustomizeHeaderActions; +use Filament\Actions\Concerns\InteractsWithActions; use Filament\Facades\Filament; use Filament\Actions\Action; +use Filament\Actions\ActionGroup; use Filament\Pages\Page; use Filament\Support\Enums\Size; use Filament\Widgets\Widget; @@ -24,7 +27,8 @@ use Livewire\Attributes\On; class Console extends Page { - protected static string|\BackedEnum|null $navigationIcon = 'tabler-brand-tabler'; + use CanCustomizeHeaderActions; + use InteractsWithActions; protected static ?int $navigationSort = 1; @@ -109,7 +113,7 @@ class Console extends Page $allWidgets = array_merge($allWidgets, [ ServerCpuChart::class, ServerMemoryChart::class, - //ServerNetworkChart::class, TODO: convert units. + ServerNetworkChart::class, ]); $allWidgets = array_merge($allWidgets, static::$customWidgets[ConsoleWidgetPosition::Bottom->value] ?? []); @@ -142,7 +146,8 @@ class Console extends Page $this->cacheHeaderActions(); } - protected function getHeaderActions(): array + /** @return array */ + protected function getDefaultHeaderActions(): array { /** @var Server $server */ $server = Filament::getTenant(); diff --git a/app/Filament/Server/Pages/ServerFormPage.php b/app/Filament/Server/Pages/ServerFormPage.php index fe917bc81..771f73cdc 100644 --- a/app/Filament/Server/Pages/ServerFormPage.php +++ b/app/Filament/Server/Pages/ServerFormPage.php @@ -3,6 +3,9 @@ namespace App\Filament\Server\Pages; use App\Models\Server; +use App\Traits\Filament\BlockAccessInConflict; +use App\Traits\Filament\CanCustomizeHeaderActions; +use App\Traits\Filament\CanCustomizeHeaderWidgets; use Filament\Facades\Filament; use Filament\Forms\Concerns\InteractsWithForms; use Filament\Schemas\Components\Form; @@ -15,6 +18,9 @@ use Filament\Schemas\Schema; */ abstract class ServerFormPage extends Page { + use BlockAccessInConflict; + use CanCustomizeHeaderActions; + use CanCustomizeHeaderWidgets; use InteractsWithFormActions; use InteractsWithForms; @@ -52,17 +58,4 @@ abstract class ServerFormPage extends Page return $server; } - - // TODO: find better way handle server conflict state - public static function canAccess(): bool - { - /** @var Server $server */ - $server = Filament::getTenant(); - - if ($server->isInConflictState()) { - return false; - } - - return parent::canAccess(); - } } diff --git a/app/Filament/Server/Resources/ActivityResource.php b/app/Filament/Server/Resources/ActivityResource.php index 14a6b5f32..ad5f8c3f6 100644 --- a/app/Filament/Server/Resources/ActivityResource.php +++ b/app/Filament/Server/Resources/ActivityResource.php @@ -2,19 +2,40 @@ 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; use App\Models\Role; use App\Models\Server; use App\Models\User; +use App\Traits\Filament\CanCustomizePages; +use App\Traits\Filament\CanCustomizeRelations; +use App\Traits\Filament\CanModifyTable; 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\PageRegistration; 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 { + use CanCustomizePages; + use CanCustomizeRelations; + use CanModifyTable; + protected static ?string $model = ActivityLog::class; protected static ?string $modelLabel = 'Activity'; @@ -27,12 +48,96 @@ class ActivityResource extends Resource protected static bool $isScopedToTenant = false; + public static function defaultTable(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', $activityLog->actor) ? 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', $activityLog->actor)) + ->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 getEloquentQuery(): Builder { /** @var Server $server */ $server = Filament::getTenant(); - return ActivityLog::whereHas('subjects', fn (Builder $query) => $query->where('subject_id', $server->id)) + return ActivityLog::whereHas('subjects', fn (Builder $query) => $query->where('subject_id', $server->id)->where('subject_type', $server->getMorphClass())) ->whereNotIn('activity_logs.event', ActivityLog::DISABLED_EVENTS) ->when(config('activity.hide_admin_activity'), function (Builder $builder) use ($server) { // We could do this with a query and a lot of joins, but that gets pretty @@ -58,7 +163,8 @@ class ActivityResource extends Resource return auth()->user()->can(Permission::ACTION_ACTIVITY_READ, Filament::getTenant()); } - public static function getPages(): array + /** @return array */ + public static function getDefaultPages(): array { return [ 'index' => Pages\ListActivities::route('/'), diff --git a/app/Filament/Server/Resources/ActivityResource/Pages/ListActivities.php b/app/Filament/Server/Resources/ActivityResource/Pages/ListActivities.php index 4db7d86f5..e60a2127f 100644 --- a/app/Filament/Server/Resources/ActivityResource/Pages/ListActivities.php +++ b/app/Filament/Server/Resources/ActivityResource/Pages/ListActivities.php @@ -2,114 +2,18 @@ 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\Actions\Action; -use Filament\Forms\Components\DateTimePicker; -use Filament\Forms\Components\KeyValue; -use Filament\Infolists\Components\TextEntry; -use Filament\Forms\Components\TextInput; +use App\Traits\Filament\CanCustomizeHeaderActions; +use App\Traits\Filament\CanCustomizeHeaderWidgets; use Filament\Resources\Pages\ListRecords; -use Filament\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 { + use CanCustomizeHeaderActions; + use CanCustomizeHeaderWidgets; + 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()) - ->schema([ - TextEntry::make('event') - ->state(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 58177db25..49ccd6d05 100644 --- a/app/Filament/Server/Resources/AllocationResource.php +++ b/app/Filament/Server/Resources/AllocationResource.php @@ -2,16 +2,32 @@ 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 App\Traits\Filament\BlockAccessInConflict; +use App\Traits\Filament\CanCustomizePages; +use App\Traits\Filament\CanCustomizeRelations; +use App\Traits\Filament\CanModifyTable; use Filament\Facades\Filament; +use Filament\Resources\Pages\PageRegistration; 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 { + use BlockAccessInConflict; + use CanCustomizePages; + use CanCustomizeRelations; + use CanModifyTable; + protected static ?string $model = Allocation::class; protected static ?string $modelLabel = 'Network'; @@ -22,17 +38,59 @@ class AllocationResource extends Resource protected static string|\BackedEnum|null $navigationIcon = 'tabler-network'; - // TODO: find better way handle server conflict state - public static function canAccess(): bool + public static function defaultTable(Table $table): Table { /** @var Server $server */ $server = Filament::getTenant(); - if ($server->isInConflictState()) { - return false; - } + 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, + ]); - return parent::canAccess(); + Activity::event('server:allocation.delete') + ->subject($allocation) + ->property('allocation', $allocation->address) + ->log(); + }), + ]); } public static function canViewAny(): bool @@ -55,7 +113,8 @@ class AllocationResource extends Resource return auth()->user()->can(Permission::ACTION_ALLOCATION_DELETE, Filament::getTenant()); } - public static function getPages(): array + /** @return array */ + public static function getDefaultPages(): array { return [ 'index' => Pages\ListAllocations::route('/'), diff --git a/app/Filament/Server/Resources/AllocationResource/Pages/ListAllocations.php b/app/Filament/Server/Resources/AllocationResource/Pages/ListAllocations.php index 5f7caea07..83e5ab4c7 100644 --- a/app/Filament/Server/Resources/AllocationResource/Pages/ListAllocations.php +++ b/app/Filament/Server/Resources/AllocationResource/Pages/ListAllocations.php @@ -4,85 +4,31 @@ 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 App\Traits\Filament\CanCustomizeHeaderActions; +use App\Traits\Filament\CanCustomizeHeaderWidgets; +use Filament\Actions\Action; +use Filament\Actions\ActionGroup; use Filament\Facades\Filament; use Filament\Resources\Pages\ListRecords; -use Filament\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 { + use CanCustomizeHeaderActions; + use CanCustomizeHeaderWidgets; + 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 + /** @return array */ + protected function getDefaultHeaderActions(): 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')) @@ -93,7 +39,7 @@ class ListAllocations extends ListRecords Activity::event('server:allocation.create') ->subject($allocation) - ->property('allocation', $allocation->toString()) + ->property('allocation', $allocation->address) ->log(); }), ]; diff --git a/app/Filament/Server/Resources/BackupResource.php b/app/Filament/Server/Resources/BackupResource.php index 69fc30ff2..ed405a884 100644 --- a/app/Filament/Server/Resources/BackupResource.php +++ b/app/Filament/Server/Resources/BackupResource.php @@ -2,16 +2,54 @@ 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 App\Services\Backups\DeleteBackupService; +use App\Traits\Filament\BlockAccessInConflict; +use App\Traits\Filament\CanCustomizePages; +use App\Traits\Filament\CanCustomizeRelations; +use App\Traits\Filament\CanModifyForm; +use App\Traits\Filament\CanModifyTable; +use App\Traits\Filament\HasLimitBadge; 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\PageRegistration; 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\Client\ConnectionException; +use Illuminate\Http\Request; class BackupResource extends Resource { + use BlockAccessInConflict; + use CanCustomizePages; + use CanCustomizeRelations; + use CanModifyForm; + use CanModifyTable; + use HasLimitBadge; + protected static ?string $model = Backup::class; protected static ?int $navigationSort = 3; @@ -20,45 +58,151 @@ class BackupResource extends Resource protected static bool $canCreateAnother = false; - public const WARNING_THRESHOLD = 0.7; - - public static function getNavigationBadge(): string + protected static function getBadgeCount(): int { /** @var Server $server */ $server = Filament::getTenant(); - $limit = $server->backup_limit; - - return $server->backups->count() . ($limit === 0 ? '' : ' / ' . $limit); + return $server->backups->count(); } - public static function getNavigationBadgeColor(): ?string + protected static function getBadgeLimit(): int { /** @var Server $server */ $server = Filament::getTenant(); - $limit = $server->backup_limit; - $count = $server->backups->count(); - - if ($limit === 0) { - return null; - } - - return $count >= $limit ? 'danger' - : ($count >= $limit * self::WARNING_THRESHOLD ? 'warning' : 'success'); + return $server->backup_limit; } - // TODO: find better way handle server conflict state - public static function canAccess(): bool + public static function defaultForm(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 defaultTable(Table $table): Table { /** @var Server $server */ $server = Filament::getTenant(); - if ($server->isInConflictState()) { - return false; - } + 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(); + } - return parent::canAccess(); + if (!$backup->is_successful && is_null($backup->completed_at)) { + 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(function (Backup $backup, DeleteBackupService $deleteBackupService) { + try { + $deleteBackupService->handle($backup); + } catch (ConnectionException) { + Notification::make() + ->title('Could not delete backup') + ->body('Connection to node failed') + ->danger() + ->send(); + + return; + } + + Activity::event('server:backup.delete') + ->subject($backup) + ->property(['name' => $backup->name, 'failed' => !$backup->is_successful]) + ->log(); + }) + ->visible(fn (Backup $backup) => $backup->status !== BackupStatus::InProgress), + ]), + ]); } public static function canViewAny(): bool @@ -76,7 +220,8 @@ class BackupResource extends Resource return auth()->user()->can(Permission::ACTION_BACKUP_DELETE, Filament::getTenant()); } - public static function getPages(): array + /** @return array */ + public static function getDefaultPages(): array { return [ 'index' => Pages\ListBackups::route('/'), diff --git a/app/Filament/Server/Resources/BackupResource/Pages/ListBackups.php b/app/Filament/Server/Resources/BackupResource/Pages/ListBackups.php index 2407ca048..27c515cb7 100644 --- a/app/Filament/Server/Resources/BackupResource/Pages/ListBackups.php +++ b/app/Filament/Server/Resources/BackupResource/Pages/ListBackups.php @@ -2,166 +2,37 @@ 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\Facades\Filament; -use Filament\Forms\Components\Checkbox; -use Filament\Infolists\Components\TextEntry; -use Filament\Forms\Components\Textarea; -use Filament\Forms\Components\TextInput; -use Filament\Forms\Components\Toggle; -use Filament\Schemas\Components\Form; -use Filament\Notifications\Notification; -use Filament\Resources\Pages\ListRecords; +use App\Traits\Filament\CanCustomizeHeaderActions; +use App\Traits\Filament\CanCustomizeHeaderWidgets; use Filament\Actions\Action; use Filament\Actions\ActionGroup; -use Filament\Actions\DeleteAction; -use Filament\Tables\Columns\IconColumn; -use Filament\Tables\Columns\TextColumn; -use Filament\Tables\Table; -use Illuminate\Http\Request; +use Filament\Actions\CreateAction; +use Filament\Facades\Filament; +use Filament\Notifications\Notification; +use Filament\Resources\Pages\ListRecords; use Symfony\Component\HttpKernel\Exception\HttpException; use Filament\Schemas\Schema; class ListBackups extends ListRecords { + use CanCustomizeHeaderActions; + use CanCustomizeHeaderWidgets; + protected static string $resource = BackupResource::class; - protected static bool $canCreateAnother = false; - - public function form(Schema $schema): Schema - { - return $schema - ->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)) - ->schema([ - TextEntry::make('INeedAName') - ->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 + /** @return array */ + protected function getDefaultHeaderActions(): 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 6b0e95261..2fd38c3a2 100644 --- a/app/Filament/Server/Resources/DatabaseResource.php +++ b/app/Filament/Server/Resources/DatabaseResource.php @@ -2,62 +2,118 @@ 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 App\Traits\Filament\BlockAccessInConflict; +use App\Traits\Filament\CanCustomizePages; +use App\Traits\Filament\CanCustomizeRelations; +use App\Traits\Filament\CanModifyForm; +use App\Traits\Filament\CanModifyTable; +use App\Traits\Filament\HasLimitBadge; use Filament\Facades\Filament; +use Filament\Forms\Components\TextInput; +use Filament\Forms\Form; +use Filament\Resources\Pages\PageRegistration; 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 { + use BlockAccessInConflict; + use CanCustomizePages; + use CanCustomizeRelations; + use CanModifyForm; + use CanModifyTable; + use HasLimitBadge; + protected static ?string $model = Database::class; protected static ?int $navigationSort = 6; protected static string|\BackedEnum|null $navigationIcon = 'tabler-database'; - public const WARNING_THRESHOLD = 0.7; - - public static function getNavigationBadge(): string + protected static function getBadgeCount(): int { /** @var Server $server */ $server = Filament::getTenant(); - $limit = $server->database_limit; - - return $server->databases->count() . ($limit === 0 ? '' : ' / ' . $limit); + return $server->databases->count(); } - public static function getNavigationBadgeColor(): ?string + protected static function getBadgeLimit(): int { /** @var Server $server */ $server = Filament::getTenant(); - $limit = $server->database_limit; - $count = $server->databases->count(); - - if ($limit === 0) { - return null; - } - - return $count >= $limit - ? 'danger' - : ($count >= $limit * self::WARNING_THRESHOLD ? 'warning' : 'success'); + return $server->database_limit; } - // TODO: find better way handle server conflict state - public static function canAccess(): bool + public static function defaultForm(Form $form): Form { /** @var Server $server */ $server = Filament::getTenant(); - if ($server->isInConflictState()) { - return false; - } + 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), + ]); + } - return parent::canAccess(); + public static function defaultTable(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)), + ]); } public static function canViewAny(): bool @@ -85,7 +141,8 @@ class DatabaseResource extends Resource return auth()->user()->can(Permission::ACTION_DATABASE_DELETE, Filament::getTenant()); } - public static function getPages(): array + /** @return array */ + public static function getDefaultPages(): array { return [ 'index' => Pages\ListDatabases::route('/'), diff --git a/app/Filament/Server/Resources/DatabaseResource/Pages/ListDatabases.php b/app/Filament/Server/Resources/DatabaseResource/Pages/ListDatabases.php index 2ecba4deb..ce2527105 100644 --- a/app/Filament/Server/Resources/DatabaseResource/Pages/ListDatabases.php +++ b/app/Filament/Server/Resources/DatabaseResource/Pages/ListDatabases.php @@ -2,90 +2,30 @@ 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 App\Traits\Filament\CanCustomizeHeaderActions; +use App\Traits\Filament\CanCustomizeHeaderWidgets; +use Filament\Actions\Action; +use Filament\Actions\ActionGroup; use Filament\Actions\CreateAction; use Filament\Facades\Filament; use Filament\Schemas\Components\Grid; use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; -use Filament\Schemas\Components\Form; use Filament\Resources\Pages\ListRecords; -use Filament\Actions\DeleteAction; -use Filament\Actions\ViewAction; -use Filament\Tables\Columns\TextColumn; -use Filament\Tables\Table; -use Filament\Schemas\Schema; class ListDatabases extends ListRecords { + use CanCustomizeHeaderActions; + use CanCustomizeHeaderWidgets; + protected static string $resource = DatabaseResource::class; - public function form(Schema $schema): Schema - { - /** @var Server $server */ - $server = Filament::getTenant(); - - return $schema - ->schema([ - TextInput::make('host') - ->formatStateUsing(fn (Database $database) => $database->address()), - //TODO ->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null), - TextInput::make('database'), - //TODO ->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null), - TextInput::make('username'), - //TODO->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)) - ) - //TODO ->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)) - //TODO ->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 + /** @return array */ + protected function getDefaultHeaderActions(): array { /** @var Server $server */ $server = Filament::getTenant(); diff --git a/app/Filament/Server/Resources/FileResource.php b/app/Filament/Server/Resources/FileResource.php index 2ec39aecf..09d04a816 100644 --- a/app/Filament/Server/Resources/FileResource.php +++ b/app/Filament/Server/Resources/FileResource.php @@ -5,13 +5,20 @@ namespace App\Filament\Server\Resources; use App\Filament\Server\Resources\FileResource\Pages; use App\Models\File; use App\Models\Permission; -use App\Models\Server; +use App\Traits\Filament\BlockAccessInConflict; +use App\Traits\Filament\CanCustomizePages; +use App\Traits\Filament\CanCustomizeRelations; use Filament\Facades\Filament; +use Filament\Resources\Pages\PageRegistration; use Filament\Resources\Resource; use Illuminate\Database\Eloquent\Model; class FileResource extends Resource { + use BlockAccessInConflict; + use CanCustomizePages; + use CanCustomizeRelations; + protected static ?string $model = File::class; protected static ?int $navigationSort = 2; @@ -20,19 +27,6 @@ class FileResource extends Resource protected static bool $isScopedToTenant = false; - // TODO: find better way handle server conflict state - public static function canAccess(): bool - { - /** @var Server $server */ - $server = Filament::getTenant(); - - if ($server->isInConflictState()) { - return false; - } - - return parent::canAccess(); - } - public static function canViewAny(): bool { return auth()->user()->can(Permission::ACTION_FILE_READ, Filament::getTenant()); @@ -53,7 +47,8 @@ class FileResource extends Resource return auth()->user()->can(Permission::ACTION_FILE_DELETE, Filament::getTenant()); } - public static function getPages(): array + /** @return array */ + public static function getDefaultPages(): array { return [ 'edit' => Pages\EditFiles::route('/edit/{path}'), diff --git a/app/Filament/Server/Resources/FileResource/Pages/EditFiles.php b/app/Filament/Server/Resources/FileResource/Pages/EditFiles.php index 940ff218b..e5476f063 100644 --- a/app/Filament/Server/Resources/FileResource/Pages/EditFiles.php +++ b/app/Filament/Server/Resources/FileResource/Pages/EditFiles.php @@ -11,6 +11,8 @@ use App\Livewire\AlertBanner; use App\Models\Permission; use App\Models\Server; use App\Repositories\Daemon\DaemonFileRepository; +use App\Traits\Filament\CanCustomizeHeaderActions; +use App\Traits\Filament\CanCustomizeHeaderWidgets; use Filament\Facades\Filament; use Filament\Actions\Action; use Filament\Forms\Components\CodeEditor; @@ -26,6 +28,7 @@ use Filament\Schemas\Components\Section; use Filament\Schemas\Schema; use Filament\Support\Enums\Alignment; use Illuminate\Contracts\Filesystem\FileNotFoundException; +use Illuminate\Database\Eloquent\Model; use Illuminate\Http\Client\ConnectionException; use Illuminate\Routing\Route; use Illuminate\Support\Facades\Route as RouteFacade; @@ -36,6 +39,8 @@ use Livewire\Attributes\Locked; */ class EditFiles extends Page { + use CanCustomizeHeaderActions; + use CanCustomizeHeaderWidgets; use InteractsWithFormActions; use InteractsWithForms; @@ -178,6 +183,15 @@ class EditFiles extends Page ->info() ->closable() ->send(); + + try { + $this->getDaemonFileRepository()->getDirectory('/'); + } catch (ConnectionException) { + AlertBanner::make('node_connection_error') + ->title('Could not connect to the node!') + ->danger() + ->send(); + } } } @@ -232,6 +246,14 @@ class EditFiles extends Page return $this->fileRepository; } + /** + * @param array $parameters + */ + public static function getUrl(array $parameters = [], bool $isAbsolute = true, ?string $panel = null, ?Model $tenant = null): string + { + return parent::getUrl($parameters, $isAbsolute, $panel, $tenant) . '/'; + } + public static function route(string $path): PageRegistration { return new PageRegistration( diff --git a/app/Filament/Server/Resources/FileResource/Pages/ListFiles.php b/app/Filament/Server/Resources/FileResource/Pages/ListFiles.php index 6b460d862..aa2234fa5 100644 --- a/app/Filament/Server/Resources/FileResource/Pages/ListFiles.php +++ b/app/Filament/Server/Resources/FileResource/Pages/ListFiles.php @@ -40,20 +40,16 @@ use Livewire\Attributes\Locked; class ListFiles extends ListRecords { + use CanCustomizeHeaderActions; + use CanCustomizeHeaderWidgets; + protected static string $resource = FileResource::class; #[Locked] - public string $path; + public string $path = '/'; private DaemonFileRepository $fileRepository; - public function mount(?string $path = null): void - { - parent::mount(); - - $this->path = $path ?? '/'; - } - public function getBreadcrumbs(): array { $resource = static::getResource(); @@ -321,9 +317,9 @@ class ListFiles extends ListRecords ->label('') ->icon('tabler-trash') ->requiresConfirmation() - ->modalDescription(fn (File $file) => $file->name) - ->modalHeading('Delete file?') + ->modalHeading(fn (File $file) => trans('filament-actions::delete.single.modal.heading', ['label' => $file->name . ' ' . ($file->is_directory ? 'folder' : 'file')])) ->action(function (File $file) { + $this->deselectAllTableRecords(); $this->getDaemonFileRepository()->deleteFiles($this->path, [$file->name]); Activity::event('server:file.delete') @@ -406,10 +402,8 @@ class ListFiles extends ListRecords ]); } - /** - * @throws \Exception - */ - protected function getHeaderActions(): array + /** @return array */ + protected function getDefaultHeaderActions(): array { /** @var Server $server */ $server = Filament::getTenant(); @@ -507,8 +501,9 @@ class ListFiles extends ListRecords ->schema([ TextInput::make('searchTerm') ->placeholder('Enter a search term, e.g. *.txt') + ->required() ->regex('/^[^*]*\*?[^*]*$/') - ->minLength(3), + ->minValue(3), ]) ->action(fn ($data) => redirect(SearchFiles::getUrl([ 'searchTerm' => $data['searchTerm'], diff --git a/app/Filament/Server/Resources/FileResource/Pages/SearchFiles.php b/app/Filament/Server/Resources/FileResource/Pages/SearchFiles.php index e844dbc83..6b0d9aadf 100644 --- a/app/Filament/Server/Resources/FileResource/Pages/SearchFiles.php +++ b/app/Filament/Server/Resources/FileResource/Pages/SearchFiles.php @@ -7,14 +7,20 @@ use App\Models\File; use App\Models\Server; use App\Filament\Components\Tables\Columns\BytesColumn; use App\Filament\Components\Tables\Columns\DateTimeColumn; +use App\Traits\Filament\CanCustomizeHeaderActions; +use App\Traits\Filament\CanCustomizeHeaderWidgets; use Filament\Facades\Filament; use Filament\Resources\Pages\ListRecords; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; use Livewire\Attributes\Locked; +use Livewire\Attributes\Url; class SearchFiles extends ListRecords { + use CanCustomizeHeaderActions; + use CanCustomizeHeaderWidgets; + protected static string $resource = FileResource::class; protected static ?string $title = 'Global Search'; @@ -22,15 +28,8 @@ class SearchFiles extends ListRecords #[Locked] public string $searchTerm; - #[Locked] - public string $path; - - public function mount(?string $searchTerm = null, ?string $path = null): void - { - parent::mount(); - $this->searchTerm = $searchTerm; - $this->path = $path ?? '/'; - } + #[Url] + public string $path = '/'; public function getBreadcrumbs(): array { diff --git a/app/Filament/Server/Resources/ScheduleResource.php b/app/Filament/Server/Resources/ScheduleResource.php index 16d0b40f2..f3a92c339 100644 --- a/app/Filament/Server/Resources/ScheduleResource.php +++ b/app/Filament/Server/Resources/ScheduleResource.php @@ -2,12 +2,18 @@ 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; use App\Models\Permission; use App\Models\Schedule; -use App\Models\Server; +use App\Traits\Filament\BlockAccessInConflict; +use App\Traits\Filament\CanCustomizePages; +use App\Traits\Filament\CanCustomizeRelations; +use App\Traits\Filament\CanModifyForm; +use App\Traits\Filament\CanModifyTable; use Carbon\Carbon; use Exception; use Filament\Actions\Action; @@ -21,33 +27,34 @@ use Filament\Schemas\Components\Form; use Filament\Schemas\Components\Section; use Filament\Schemas\Components\Utilities\Set; use Filament\Notifications\Notification; +use Filament\Resources\Pages\PageRegistration; +use Filament\Resources\RelationManagers\RelationManager; use Filament\Resources\Resource; use Filament\Support\Enums\Operation; 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; use Filament\Schemas\Schema; class ScheduleResource extends Resource { + use BlockAccessInConflict; + use CanCustomizePages; + use CanCustomizeRelations; + use CanModifyForm; + use CanModifyTable; + protected static ?string $model = Schedule::class; protected static ?int $navigationSort = 4; protected static string|\BackedEnum|null $navigationIcon = 'tabler-clock'; - // TODO: find better way handle server conflict state - public static function canAccess(): bool - { - /** @var Server $server */ - $server = Filament::getTenant(); - - if ($server->isInConflictState()) { - return false; - } - - return parent::canAccess(); - } - public static function canViewAny(): bool { return auth()->user()->can(Permission::ACTION_SCHEDULE_READ, Filament::getTenant()); @@ -310,14 +317,54 @@ class ScheduleResource extends Resource ]); } - public static function getRelations(): array + public static function defaultTable(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(); + }), + ]); + } + + /** @return class-string[] */ + public static function getDefaultRelations(): array { return [ TasksRelationManager::class, ]; } - public static function getPages(): array + /** @return array */ + public static function getDefaultPages(): array { return [ 'index' => Pages\ListSchedules::route('/'), diff --git a/app/Filament/Server/Resources/ScheduleResource/Pages/CreateSchedule.php b/app/Filament/Server/Resources/ScheduleResource/Pages/CreateSchedule.php index b85fe213f..ba3b60a9d 100644 --- a/app/Filament/Server/Resources/ScheduleResource/Pages/CreateSchedule.php +++ b/app/Filament/Server/Resources/ScheduleResource/Pages/CreateSchedule.php @@ -6,11 +6,16 @@ use App\Facades\Activity; use App\Filament\Server\Resources\ScheduleResource; use App\Models\Schedule; use App\Models\Server; +use App\Traits\Filament\CanCustomizeHeaderActions; +use App\Traits\Filament\CanCustomizeHeaderWidgets; use Filament\Facades\Filament; use Filament\Resources\Pages\CreateRecord; class CreateSchedule extends CreateRecord { + use CanCustomizeHeaderActions; + use CanCustomizeHeaderWidgets; + protected static string $resource = ScheduleResource::class; protected static bool $canCreateAnother = false; diff --git a/app/Filament/Server/Resources/ScheduleResource/Pages/EditSchedule.php b/app/Filament/Server/Resources/ScheduleResource/Pages/EditSchedule.php index d6b10782e..9eefbaf54 100644 --- a/app/Filament/Server/Resources/ScheduleResource/Pages/EditSchedule.php +++ b/app/Filament/Server/Resources/ScheduleResource/Pages/EditSchedule.php @@ -6,10 +6,15 @@ use App\Facades\Activity; use App\Filament\Server\Resources\ScheduleResource; use App\Models\Schedule; use Filament\Actions\DeleteAction; +use App\Traits\Filament\CanCustomizeHeaderWidgets; +use App\Traits\Filament\CanCustomizeHeaderActions; use Filament\Resources\Pages\EditRecord; class EditSchedule extends EditRecord { + use CanCustomizeHeaderActions; + use CanCustomizeHeaderWidgets; + protected static string $resource = ScheduleResource::class; protected function afterSave(): void @@ -35,7 +40,8 @@ class EditSchedule extends EditRecord return $data; } - protected function getHeaderActions(): array + /** @return array */ + protected function getDefaultHeaderActions(): array { return [ DeleteAction::make() diff --git a/app/Filament/Server/Resources/ScheduleResource/Pages/ListSchedules.php b/app/Filament/Server/Resources/ScheduleResource/Pages/ListSchedules.php index da7d0911e..1d2f75f49 100644 --- a/app/Filament/Server/Resources/ScheduleResource/Pages/ListSchedules.php +++ b/app/Filament/Server/Resources/ScheduleResource/Pages/ListSchedules.php @@ -2,65 +2,27 @@ 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 App\Traits\Filament\CanCustomizeHeaderActions; +use App\Traits\Filament\CanCustomizeHeaderWidgets; +use Filament\Actions\Action; +use Filament\Actions\ActionGroup; +use Filament\Actions\CreateAction; use Filament\Resources\Pages\ListRecords; -use Filament\Actions\DeleteAction; -use Filament\Actions\EditAction; -use Filament\Actions\ViewAction; -use Filament\Tables\Columns\IconColumn; -use Filament\Tables\Columns\TextColumn; -use Filament\Tables\Table; class ListSchedules extends ListRecords { + use CanCustomizeHeaderActions; + use CanCustomizeHeaderWidgets; + 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 array */ + protected function getDefaultHeaderActions(): 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 2ce565da0..77f70845f 100644 --- a/app/Filament/Server/Resources/ScheduleResource/Pages/ViewSchedule.php +++ b/app/Filament/Server/Resources/ScheduleResource/Pages/ViewSchedule.php @@ -7,6 +7,8 @@ use App\Filament\Server\Resources\ScheduleResource; use App\Models\Permission; use App\Models\Schedule; use App\Services\Schedules\ProcessScheduleService; +use App\Traits\Filament\CanCustomizeHeaderActions; +use App\Traits\Filament\CanCustomizeHeaderWidgets; use Filament\Actions\Action; use Filament\Actions\EditAction; use Filament\Facades\Filament; @@ -14,9 +16,13 @@ use Filament\Resources\Pages\ViewRecord; class ViewSchedule extends ViewRecord { + use CanCustomizeHeaderActions; + use CanCustomizeHeaderWidgets; + protected static string $resource = ScheduleResource::class; - protected function getHeaderActions(): array + /** @return array */ + protected function getDefaultHeaderActions(): array { return [ Action::make('runNow') diff --git a/app/Filament/Server/Resources/UserResource.php b/app/Filament/Server/Resources/UserResource.php index c1a71fa51..aec96b650 100644 --- a/app/Filament/Server/Resources/UserResource.php +++ b/app/Filament/Server/Resources/UserResource.php @@ -8,6 +8,11 @@ use App\Models\Server; use App\Models\User; use App\Services\Subusers\SubuserDeletionService; use App\Services\Subusers\SubuserUpdateService; +use App\Traits\Filament\BlockAccessInConflict; +use App\Traits\Filament\CanCustomizePages; +use App\Traits\Filament\CanCustomizeRelations; +use App\Traits\Filament\CanModifyTable; +use App\Traits\Filament\HasLimitBadge; use Filament\Facades\Filament; use Filament\Actions\Action; use Filament\Forms\Components\CheckboxList; @@ -20,6 +25,7 @@ use Filament\Forms\Components\TextInput; use Filament\Schemas\Components\Utilities\Set; use Filament\Notifications\Notification; use Filament\Actions\DeleteAction; +use Filament\Resources\Pages\PageRegistration; use Filament\Resources\Resource; use Filament\Actions\EditAction; use Filament\Tables\Columns\ImageColumn; @@ -29,6 +35,12 @@ use Illuminate\Database\Eloquent\Model; class UserResource extends Resource { + use BlockAccessInConflict; + use CanCustomizePages; + use CanCustomizeRelations; + use CanModifyTable; + use HasLimitBadge; + protected static ?string $model = User::class; protected static ?int $navigationSort = 5; @@ -37,25 +49,12 @@ class UserResource extends Resource protected static ?string $tenantOwnershipRelationshipName = 'subServers'; - public static function getNavigationBadge(): string + protected static function getBadgeCount(): int { /** @var Server $server */ $server = Filament::getTenant(); - return (string) $server->subusers->count(); - } - - // TODO: find better way handle server conflict state - public static function canAccess(): bool - { - /** @var Server $server */ - $server = Filament::getTenant(); - - if ($server->isInConflictState()) { - return false; - } - - return parent::canAccess(); + return $server->subusers->count(); } public static function canViewAny(): bool @@ -78,11 +77,40 @@ class UserResource extends Resource return auth()->user()->can(Permission::ACTION_USER_DELETE, Filament::getTenant()); } - public static function table(Table $table): Table + public static function defaultTable(Table $table): Table { /** @var Server $server */ $server = Filament::getTenant(); + $tabs = []; + $permissionsArray = []; + + foreach (Permission::permissionData() as $data) { + $options = []; + $descriptions = []; + + foreach ($data['permissions'] as $permission) { + $options[$permission] = str($permission)->headline(); + $descriptions[$permission] = trans('server/users.permissions.' . $data['name'] . '_' . str($permission)->replace('-', '_')); + $permissionsArray[$data['name']][] = $permission; + } + + $tabs[] = Tab::make(str($data['name'])->headline()) + ->schema([ + Section::make() + ->description(trans('server/users.permissions.' . $data['name'] . '_desc')) + ->icon($data['icon']) + ->schema([ + CheckboxList::make($data['name']) + ->label('') + ->bulkToggleable() + ->columns(2) + ->options($options) + ->descriptions($descriptions), + ]), + ]); + } + return $table ->paginated(false) ->searchable(false) @@ -158,69 +186,8 @@ class UserResource extends Resource Actions::make([ Action::make('assignAll') ->label('Assign All') - ->action(function (Set $set) { - $permissions = [ - 'control' => [ - 'console', - 'start', - 'stop', - 'restart', - ], - 'user' => [ - 'read', - 'create', - 'update', - 'delete', - ], - 'file' => [ - 'read', - 'read-content', - 'create', - 'update', - 'delete', - 'archive', - 'sftp', - ], - 'backup' => [ - 'read', - 'create', - 'delete', - 'download', - 'restore', - ], - 'allocation' => [ - 'read', - 'create', - 'update', - 'delete', - ], - 'startup' => [ - 'read', - 'update', - 'docker-image', - ], - 'database' => [ - 'read', - 'create', - 'update', - 'delete', - 'view_password', - ], - 'schedule' => [ - 'read', - 'create', - 'update', - 'delete', - ], - 'settings' => [ - 'rename', - 'reinstall', - ], - 'activity' => [ - 'read', - ], - ]; - + ->action(function (Set $set) use ($permissionsArray) { + $permissions = $permissionsArray; foreach ($permissions as $key => $value) { $allValues = array_unique($value); $set($key, $allValues); @@ -235,268 +202,30 @@ class UserResource extends Resource ]), Tabs::make() ->columnSpanFull() - ->schema([ - Tab::make('Console') - ->schema([ - Section::make() - ->description(trans('server/users.permissions.control_desc')) - ->icon('tabler-terminal-2') - ->schema([ - CheckboxList::make('control') - ->formatStateUsing(function (User $user, Set $set) use ($server) { - $permissionsArray = $server->subusers->where('user_id', $user->id)->first()->permissions; - - $transformedPermissions = []; - - foreach ($permissionsArray as $permission) { - [$group, $action] = explode('.', $permission, 2); - $transformedPermissions[$group][] = $action; - } - - foreach ($transformedPermissions as $key => $value) { - $set($key, $value); - } - - return $transformedPermissions['control'] ?? []; - }) - ->bulkToggleable() - ->label('') - ->columns(2) - ->options([ - 'console' => 'Console', - 'start' => 'Start', - 'stop' => 'Stop', - 'restart' => 'Restart', - ]) - ->descriptions([ - 'console' => trans('server/users.permissions.control_console'), - 'start' => trans('server/users.permissions.control_start'), - 'stop' => trans('server/users.permissions.control_stop'), - 'restart' => trans('server/users.permissions.control_restart'), - ]), - ]), - ]), - Tab::make('User') - ->schema([ - Section::make() - ->description(trans('server/users.permissions.user_desc')) - ->icon('tabler-users') - ->schema([ - CheckboxList::make('user') - ->bulkToggleable() - ->label('') - ->columns(2) - ->options([ - 'read' => 'Read', - 'create' => 'Create', - 'update' => 'Update', - 'delete' => 'Delete', - ]) - ->descriptions([ - 'create' => trans('server/users.permissions.user_create'), - 'read' => trans('server/users.permissions.user_read'), - 'update' => trans('server/users.permissions.user_update'), - 'delete' => trans('server/users.permissions.user_delete'), - ]), - ]), - ]), - Tab::make('File') - ->schema([ - Section::make() - ->description(trans('server/users.permissions.file_desc')) - ->icon('tabler-folders') - ->schema([ - CheckboxList::make('file') - ->bulkToggleable() - ->label('') - ->columns(2) - ->options([ - 'read' => 'Read', - 'read-content' => 'Read Content', - 'create' => 'Create', - 'update' => 'Update', - 'delete' => 'Delete', - 'archive' => 'Archive', - 'sftp' => 'SFTP', - ]) - ->descriptions([ - 'create' => trans('server/users.permissions.file_create'), - 'read' => trans('server/users.permissions.file_read'), - 'read-content' => trans('server/users.permissions.file_read_content'), - 'update' => trans('server/users.permissions.file_update'), - 'delete' => trans('server/users.permissions.file_delete'), - 'archive' => trans('server/users.permissions.file_archive'), - 'sftp' => trans('server/users.permissions.file_sftp'), - ]), - ]), - ]), - Tab::make('Backup') - ->schema([ - Section::make() - ->description(trans('server/users.permissions.backup_desc')) - ->icon('tabler-download') - ->schema([ - CheckboxList::make('backup') - ->bulkToggleable() - ->label('') - ->columns(2) - ->options([ - 'read' => 'Read', - 'create' => 'Create', - 'delete' => 'Delete', - 'download' => 'Download', - 'restore' => 'Restore', - ]) - ->descriptions([ - 'create' => trans('server/users.permissions.backup_create'), - 'read' => trans('server/users.permissions.backup_read'), - 'delete' => trans('server/users.permissions.backup_delete'), - 'download' => trans('server/users.permissions.backup_download'), - 'restore' => trans('server/users.permissions.backup_restore'), - ]), - ]), - ]), - Tab::make('Allocation') - ->schema([ - Section::make() - ->description(trans('server/users.permissions.allocation_desc')) - ->icon('tabler-network') - ->schema([ - CheckboxList::make('allocation') - ->bulkToggleable() - ->label('') - ->columns(2) - ->options([ - 'read' => 'Read', - 'create' => 'Create', - 'update' => 'Update', - 'delete' => 'Delete', - ]) - ->descriptions([ - 'read' => trans('server/users.permissions.allocation_read'), - 'create' => trans('server/users.permissions.allocation_create'), - 'update' => trans('server/users.permissions.allocation_update'), - 'delete' => trans('server/users.permissions.allocation_delete'), - ]), - ]), - ]), - Tab::make('Startup') - ->schema([ - Section::make() - ->description(trans('server/users.permissions.startup_desc')) - ->icon('tabler-question-mark') - ->schema([ - CheckboxList::make('startup') - ->bulkToggleable() - ->label('') - ->columns(2) - ->options([ - 'read' => 'Read', - 'update' => 'Update', - 'docker-image' => 'Docker Image', - ]) - ->descriptions([ - 'read' => trans('server/users.permissions.startup_read'), - 'update' => trans('server/users.permissions.startup_update'), - 'docker-image' => trans('server/users.permissions.startup_docker_image'), - ]), - ]), - ]), - Tab::make('Database') - ->schema([ - Section::make() - ->description(trans('server/users.permissions.database_desc')) - ->icon('tabler-database') - ->schema([ - CheckboxList::make('database') - ->bulkToggleable() - ->label('') - ->columns(2) - ->options([ - 'read' => 'Read', - 'create' => 'Create', - 'update' => 'Update', - 'delete' => 'Delete', - 'view_password' => 'View Password', - ]) - ->descriptions([ - 'read' => trans('server/users.permissions.database_read'), - 'create' => trans('server/users.permissions.database_create'), - 'update' => trans('server/users.permissions.database_update'), - 'delete' => trans('server/users.permissions.database_delete'), - 'view_password' => trans('server/users.permissions.database_view_password'), - ]), - ]), - ]), - Tab::make('Schedule') - ->schema([ - Section::make() - ->description(trans('server/users.permissions.schedule_desc')) - ->icon('tabler-clock') - ->schema([ - CheckboxList::make('schedule') - ->bulkToggleable() - ->label('') - ->columns(2) - ->options([ - 'read' => 'Read', - 'create' => 'Create', - 'update' => 'Update', - 'delete' => 'Delete', - ]) - ->descriptions([ - 'read' => trans('server/users.permissions.schedule_read'), - 'create' => trans('server/users.permissions.schedule_create'), - 'update' => trans('server/users.permissions.schedule_update'), - 'delete' => trans('server/users.permissions.schedule_delete'), - ]), - ]), - ]), - Tab::make('Settings') - ->schema([ - Section::make() - ->description(trans('server/users.permissions.settings_desc')) - ->icon('tabler-settings') - ->schema([ - CheckboxList::make('settings') - ->bulkToggleable() - ->label('') - ->columns(2) - ->options([ - 'rename' => 'Rename', - 'reinstall' => 'Reinstall', - ]) - ->descriptions([ - 'rename' => trans('server/users.permissions.setting_rename'), - 'reinstall' => trans('server/users.permissions.setting_reinstall'), - ]), - ]), - ]), - Tab::make('Activity') - ->schema([ - Section::make() - ->description(trans('server/users.permissions.activity_desc')) - ->icon('tabler-stack') - ->schema([ - CheckboxList::make('activity') - ->bulkToggleable() - ->label('') - ->columns(2) - ->options([ - 'read' => 'Read', - ]) - ->descriptions([ - 'read' => trans('server/users.permissions.activity_read'), - ]), - ]), - ]), - ]), + ->schema($tabs), ]), - ]), + ]) + ->mutateRecordDataUsing(function ($data, User $user) use ($server) { + $permissionsArray = $server->subusers->where('user_id', $user->id)->first()->permissions; + + $transformedPermissions = []; + + foreach ($permissionsArray as $permission) { + [$group, $action] = explode('.', $permission, 2); + $transformedPermissions[$group][] = $action; + } + + foreach ($transformedPermissions as $key => $value) { + $data[$key] = $value; + } + + return $data; + }), ]); } - public static function getPages(): array + /** @return array */ + public static function getDefaultPages(): array { return [ 'index' => Pages\ListUsers::route('/'), diff --git a/app/Filament/Server/Resources/UserResource/Pages/ListUsers.php b/app/Filament/Server/Resources/UserResource/Pages/ListUsers.php index d752a1623..89d079cd8 100644 --- a/app/Filament/Server/Resources/UserResource/Pages/ListUsers.php +++ b/app/Filament/Server/Resources/UserResource/Pages/ListUsers.php @@ -7,6 +7,8 @@ use App\Filament\Server\Resources\UserResource; use App\Models\Permission; use App\Models\Server; use App\Services\Subusers\SubuserCreationService; +use App\Traits\Filament\CanCustomizeHeaderActions; +use App\Traits\Filament\CanCustomizeHeaderWidgets; use Exception; use Filament\Actions\CreateAction; use Filament\Facades\Filament; @@ -25,13 +27,46 @@ use Filament\Resources\Pages\ListRecords; class ListUsers extends ListRecords { + use CanCustomizeHeaderActions; + use CanCustomizeHeaderWidgets; + protected static string $resource = UserResource::class; - protected function getHeaderActions(): array + /** @return array */ + protected function getDefaultHeaderActions(): array { /** @var Server $server */ $server = Filament::getTenant(); + $tabs = []; + $permissionsArray = []; + + foreach (Permission::permissionData() as $data) { + $options = []; + $descriptions = []; + + foreach ($data['permissions'] as $permission) { + $options[$permission] = str($permission)->headline(); + $descriptions[$permission] = trans('server/users.permissions.' . $data['name'] . '_' . str($permission)->replace('-', '_')); + $permissionsArray[$data['name']][] = $permission; + } + + $tabs[] = Tab::make(str($data['name'])->headline()) + ->schema([ + Section::make() + ->description(trans('server/users.permissions.' . $data['name'] . '_desc')) + ->icon($data['icon']) + ->schema([ + CheckboxList::make($data['name']) + ->label('') + ->bulkToggleable() + ->columns(2) + ->options($options) + ->descriptions($descriptions), + ]), + ]); + } + return [ CreateAction::make('invite') ->label('Invite User') @@ -60,72 +95,10 @@ class ListUsers extends ListRecords Actions::make([ Action::make('assignAll') ->label('Assign All') - ->action(function (Set $set, Get $get) { - $permissions = [ - 'control' => [ - 'console', - 'start', - 'stop', - 'restart', - ], - 'user' => [ - 'read', - 'create', - 'update', - 'delete', - ], - 'file' => [ - 'read', - 'read-content', - 'create', - 'update', - 'delete', - 'archive', - 'sftp', - ], - 'backup' => [ - 'read', - 'create', - 'delete', - 'download', - 'restore', - ], - 'allocation' => [ - 'read', - 'create', - 'update', - 'delete', - ], - 'startup' => [ - 'read', - 'update', - 'docker-image', - ], - 'database' => [ - 'read', - 'create', - 'update', - 'delete', - 'view_password', - ], - 'schedule' => [ - 'read', - 'create', - 'update', - 'delete', - ], - 'settings' => [ - 'rename', - 'reinstall', - ], - 'activity' => [ - 'read', - ], - ]; - + ->action(function (Set $set, Get $get) use ($permissionsArray) { + $permissions = $permissionsArray; foreach ($permissions as $key => $value) { - $currentValues = $get($key) ?? []; - $allValues = array_unique(array_merge($currentValues, $value)); + $allValues = array_unique($value); $set($key, $allValues); } }), @@ -138,247 +111,7 @@ class ListUsers extends ListRecords ]), Tabs::make() ->columnSpanFull() - ->schema([ - Tab::make('Console') - ->schema([ - Section::make() - ->description(trans('server/users.permissions.control_desc')) - ->icon('tabler-terminal-2') - ->schema([ - CheckboxList::make('control') - ->bulkToggleable() - ->label('') - ->columns(2) - ->options([ - 'console' => 'Console', - 'start' => 'Start', - 'stop' => 'Stop', - 'restart' => 'Restart', - ]) - ->descriptions([ - 'console' => trans('server/users.permissions.control_console'), - 'start' => trans('server/users.permissions.control_start'), - 'stop' => trans('server/users.permissions.control_stop'), - 'restart' => trans('server/users.permissions.control_restart'), - ]), - ]), - ]), - Tab::make('User') - ->schema([ - Section::make() - ->description(trans('server/users.permissions.user_desc')) - ->icon('tabler-users') - ->schema([ - CheckboxList::make('user') - ->bulkToggleable() - ->label('') - ->columns(2) - ->options([ - 'read' => 'Read', - 'create' => 'Create', - 'update' => 'Update', - 'delete' => 'Delete', - ]) - ->descriptions([ - 'create' => trans('server/users.permissions.user_create'), - 'read' => trans('server/users.permissions.user_read'), - 'update' => trans('server/users.permissions.user_update'), - 'delete' => trans('server/users.permissions.user_delete'), - ]), - ]), - ]), - Tab::make('File') - ->schema([ - Section::make() - ->description(trans('server/users.permissions.file_desc')) - ->icon('tabler-folders') - ->schema([ - CheckboxList::make('file') - ->bulkToggleable() - ->label('') - ->columns(2) - ->options([ - 'read' => 'Read', - 'read-content' => 'Read Content', - 'create' => 'Create', - 'update' => 'Update', - 'delete' => 'Delete', - 'archive' => 'Archive', - 'sftp' => 'SFTP', - ]) - ->descriptions([ - 'create' => trans('server/users.permissions.file_create'), - 'read' => trans('server/users.permissions.file_read'), - 'read-content' => trans('server/users.permissions.file_read_content'), - 'update' => trans('server/users.permissions.file_update'), - 'delete' => trans('server/users.permissions.file_delete'), - 'archive' => trans('server/users.permissions.file_archive'), - 'sftp' => trans('server/users.permissions.file_sftp'), - ]), - ]), - ]), - Tab::make('Backup') - ->schema([ - Section::make() - ->description(trans('server/users.permissions.backup_desc')) - ->icon('tabler-download') - ->schema([ - CheckboxList::make('backup') - ->bulkToggleable() - ->label('') - ->columns(2) - ->options([ - 'read' => 'Read', - 'create' => 'Create', - 'delete' => 'Delete', - 'download' => 'Download', - 'restore' => 'Restore', - ]) - ->descriptions([ - 'create' => trans('server/users.permissions.backup_create'), - 'read' => trans('server/users.permissions.backup_read'), - 'delete' => trans('server/users.permissions.backup_delete'), - 'download' => trans('server/users.permissions.backup_download'), - 'restore' => trans('server/users.permissions.backup_restore'), - ]), - ]), - ]), - Tab::make('Allocation') - ->schema([ - Section::make() - ->description(trans('server/users.permissions.allocation_desc')) - ->icon('tabler-network') - ->schema([ - CheckboxList::make('allocation') - ->bulkToggleable() - ->label('') - ->columns(2) - ->options([ - 'read' => 'Read', - 'create' => 'Create', - 'update' => 'Update', - 'delete' => 'Delete', - ]) - ->descriptions([ - 'read' => trans('server/users.permissions.allocation_read'), - 'create' => trans('server/users.permissions.allocation_create'), - 'update' => trans('server/users.permissions.allocation_update'), - 'delete' => trans('server/users.permissions.allocation_delete'), - ]), - ]), - ]), - Tab::make('Startup') - ->schema([ - Section::make() - ->description(trans('server/users.permissions.startup_desc')) - ->icon('tabler-question-mark') - ->schema([ - CheckboxList::make('startup') - ->bulkToggleable() - ->label('') - ->columns(2) - ->options([ - 'read' => 'Read', - 'update' => 'Update', - 'docker-image' => 'Docker Image', - ]) - ->descriptions([ - 'read' => trans('server/users.permissions.startup_read'), - 'update' => trans('server/users.permissions.startup_update'), - 'docker-image' => trans('server/users.permissions.startup_docker_image'), - ]), - ]), - ]), - Tab::make('Database') - ->schema([ - Section::make() - ->description(trans('server/users.permissions.database_desc')) - ->icon('tabler-database') - ->schema([ - CheckboxList::make('database') - ->bulkToggleable() - ->label('') - ->columns(2) - ->options([ - 'read' => 'Read', - 'create' => 'Create', - 'update' => 'Update', - 'delete' => 'Delete', - 'view_password' => 'View Password', - ]) - ->descriptions([ - 'read' => trans('server/users.permissions.database_read'), - 'create' => trans('server/users.permissions.database_create'), - 'update' => trans('server/users.permissions.database_update'), - 'delete' => trans('server/users.permissions.database_delete'), - 'view_password' => trans('server/users.permissions.database_view_password'), - ]), - ]), - ]), - Tab::make('Schedule') - ->schema([ - Section::make() - ->description(trans('server/users.permissions.schedule_desc')) - ->icon('tabler-clock') - ->schema([ - CheckboxList::make('schedule') - ->bulkToggleable() - ->label('') - ->columns(2) - ->options([ - 'read' => 'Read', - 'create' => 'Create', - 'update' => 'Update', - 'delete' => 'Delete', - ]) - ->descriptions([ - 'read' => trans('server/users.permissions.schedule_read'), - 'create' => trans('server/users.permissions.schedule_create'), - 'update' => trans('server/users.permissions.schedule_update'), - 'delete' => trans('server/users.permissions.schedule_delete'), - ]), - ]), - ]), - Tab::make('Settings') - ->schema([ - Section::make() - ->description(trans('server/users.permissions.settings_desc')) - ->icon('tabler-settings') - ->schema([ - CheckboxList::make('settings') - ->bulkToggleable() - ->label('') - ->columns(2) - ->options([ - 'rename' => 'Rename', - 'reinstall' => 'Reinstall', - ]) - ->descriptions([ - 'rename' => trans('server/users.permissions.setting_rename'), - 'reinstall' => trans('server/users.permissions.setting_reinstall'), - ]), - ]), - ]), - Tab::make('Activity') - ->schema([ - Section::make() - ->description(trans('server/users.permissions.activity_desc')) - ->icon('tabler-stack') - ->schema([ - CheckboxList::make('activity') - ->bulkToggleable() - ->label('') - ->columns(2) - ->options([ - 'read' => 'Read', - ]) - ->descriptions([ - 'read' => trans('server/users.permissions.activity_read'), - ]), - ]), - ]), - ]), - + ->schema($tabs), ]), ]) ->modalHeading('Invite User') diff --git a/app/Filament/Server/Widgets/ServerCpuChart.php b/app/Filament/Server/Widgets/ServerCpuChart.php index 2b0fa4280..eeb05b48e 100644 --- a/app/Filament/Server/Widgets/ServerCpuChart.php +++ b/app/Filament/Server/Widgets/ServerCpuChart.php @@ -4,6 +4,7 @@ namespace App\Filament\Server\Widgets; use App\Models\Server; use Carbon\Carbon; +use Filament\Facades\Filament; use Filament\Support\RawJs; use Filament\Widgets\ChartWidget; use Illuminate\Support\Number; @@ -16,10 +17,19 @@ class ServerCpuChart extends ChartWidget public ?Server $server = null; + public static function canView(): bool + { + /** @var Server $server */ + $server = Filament::getTenant(); + + return !$server->isInConflictState() && !$server->retrieveStatus()->isOffline(); + } + protected function getData(): array { + $period = auth()->user()->getCustomization()['console_graph_period'] ?? 30; $cpu = collect(cache()->get("servers.{$this->server->id}.cpu_absolute")) - ->slice(-10) + ->slice(-$period) ->map(fn ($value, $key) => [ 'cpu' => Number::format($value, maxPrecision: 2), 'timestamp' => Carbon::createFromTimestamp($key, auth()->user()->timezone ?? 'UTC')->format('H:i:s'), diff --git a/app/Filament/Server/Widgets/ServerMemoryChart.php b/app/Filament/Server/Widgets/ServerMemoryChart.php index 5c323919b..8e568f35c 100644 --- a/app/Filament/Server/Widgets/ServerMemoryChart.php +++ b/app/Filament/Server/Widgets/ServerMemoryChart.php @@ -4,6 +4,7 @@ namespace App\Filament\Server\Widgets; use App\Models\Server; use Carbon\Carbon; +use Filament\Facades\Filament; use Filament\Support\RawJs; use Filament\Widgets\ChartWidget; use Illuminate\Support\Number; @@ -16,9 +17,19 @@ class ServerMemoryChart extends ChartWidget public ?Server $server = null; + public static function canView(): bool + { + /** @var Server $server */ + $server = Filament::getTenant(); + + return !$server->isInConflictState() && !$server->retrieveStatus()->isOffline(); + } + protected function getData(): array { - $memUsed = collect(cache()->get("servers.{$this->server->id}.memory_bytes"))->slice(-10) + $period = auth()->user()->getCustomization()['console_graph_period'] ?? 30; + $memUsed = collect(cache()->get("servers.{$this->server->id}.memory_bytes")) + ->slice(-$period) ->map(fn ($value, $key) => [ 'memory' => Number::format(config('panel.use_binary_prefix') ? $value / 1024 / 1024 / 1024 : $value / 1000 / 1000 / 1000, maxPrecision: 2), 'timestamp' => Carbon::createFromTimestamp($key, auth()->user()->timezone ?? 'UTC')->format('H:i:s'), diff --git a/app/Filament/Server/Widgets/ServerNetworkChart.php b/app/Filament/Server/Widgets/ServerNetworkChart.php index 7ca06d65e..79c9a2d15 100644 --- a/app/Filament/Server/Widgets/ServerNetworkChart.php +++ b/app/Filament/Server/Widgets/ServerNetworkChart.php @@ -4,61 +4,72 @@ namespace App\Filament\Server\Widgets; use App\Models\Server; use Carbon\Carbon; +use Filament\Facades\Filament; use Filament\Support\RawJs; use Filament\Widgets\ChartWidget; class ServerNetworkChart extends ChartWidget { - protected ?string $heading = 'Network'; + protected static ?string $pollingInterval = '1s'; - protected ?string $pollingInterval = '1s'; - - protected ?string $maxHeight = '300px'; + protected static ?string $maxHeight = '200px'; public ?Server $server = null; + public static function canView(): bool + { + /** @var Server $server */ + $server = Filament::getTenant(); + + return !$server->isInConflictState() && !$server->retrieveStatus()->isOffline(); + } + protected function getData(): array { - $data = cache()->get("servers.{$this->server->id}.network"); + $previous = null; - $rx = collect($data) - ->slice(-10) - ->map(fn ($value, $key) => [ - 'rx' => $value->rx_bytes, - 'timestamp' => Carbon::createFromTimestamp($key, (auth()->user()->timezone ?? 'UTC'))->format('H:i:s'), - ]) - ->all(); + $period = auth()->user()->getCustomization()['console_graph_period'] ?? 30; + $net = collect(cache()->get("servers.{$this->server->id}.network")) + ->slice(-$period) + ->map(function ($current, $timestamp) use (&$previous) { + $net = null; - $tx = collect($data) - ->slice(-10) - ->map(fn ($value, $key) => [ - 'tx' => $value->rx_bytes, - 'timestamp' => Carbon::createFromTimestamp($key, (auth()->user()->timezone ?? 'UTC'))->format('H:i:s'), - ]) + if ($previous !== null) { + $net = [ + 'rx' => max(0, $current->rx_bytes - $previous->rx_bytes), + 'tx' => max(0, $current->tx_bytes - $previous->tx_bytes), + 'timestamp' => Carbon::createFromTimestamp($timestamp, auth()->user()->timezone ?? 'UTC')->format('H:i:s'), + ]; + } + + $previous = $current; + + return $net; + }) ->all(); return [ 'datasets' => [ [ 'label' => 'Inbound', - 'data' => array_column($rx, 'rx'), + 'data' => array_column($net, 'rx'), 'backgroundColor' => [ - 'rgba(96, 165, 250, 0.3)', + 'rgba(100, 255, 105, 0.5)', ], 'tension' => '0.3', 'fill' => true, ], [ 'label' => 'Outbound', - 'data' => array_column($tx, 'tx'), + 'data' => array_column($net, 'tx'), 'backgroundColor' => [ - 'rgba(165, 96, 250, 0.3)', + 'rgba(96, 165, 250, 0.3)', ], 'tension' => '0.3', 'fill' => true, ], ], - 'labels' => array_column($rx, 'timestamp'), + 'labels' => array_column($net, 'timestamp'), ]; } @@ -69,25 +80,38 @@ class ServerNetworkChart extends ChartWidget protected function getOptions(): RawJs { + // TODO: use "panel.use_binary_prefix" config value return RawJs::make(<<<'JS' { scales: { x: { - grid: { - display: false, - }, - ticks: { - display: true, - }, - display: false, //debug + display: false, }, y: { + min: 0, ticks: { display: true, + callback(value) { + const bytes = typeof value === 'string' ? parseInt(value, 10) : value; + + if (bytes < 1) return '0 Bytes'; + + const i = Math.floor(Math.log(bytes) / Math.log(1024)); + const number = Number((bytes / Math.pow(1024, i)).toFixed(2)); + + return `${number} ${['Bytes', 'KiB', 'MiB', 'GiB', 'TiB'][i]}`; + }, }, }, } } JS); } + + public function getHeading(): string + { + $lastData = collect(cache()->get("servers.{$this->server->id}.network"))->last(); + + return 'Network - ↓' . convert_bytes_to_readable($lastData->rx_bytes ?? 0) . ' - ↑' . convert_bytes_to_readable($lastData->tx_bytes ?? 0); + } } diff --git a/app/Filament/Server/Widgets/ServerOverview.php b/app/Filament/Server/Widgets/ServerOverview.php index 1cdaebe28..8fa7fad3b 100644 --- a/app/Filament/Server/Widgets/ServerOverview.php +++ b/app/Filament/Server/Widgets/ServerOverview.php @@ -6,8 +6,10 @@ use App\Enums\ContainerStatus; use App\Filament\Server\Components\SmallStatBlock; use App\Models\Server; use Carbon\CarbonInterface; +use Filament\Notifications\Notification; use Filament\Widgets\StatsOverviewWidget; use Illuminate\Support\Number; +use Livewire\Attributes\On; class ServerOverview extends StatsOverviewWidget { @@ -19,14 +21,10 @@ class ServerOverview extends StatsOverviewWidget { return [ SmallStatBlock::make('Name', $this->server->name) - ->extraAttributes([ - 'class' => 'overflow-x-auto', - ]), + ->copyOnClick(fn () => request()->isSecure()), SmallStatBlock::make('Status', $this->status()), SmallStatBlock::make('Address', $this->server->allocation->address) - ->extraAttributes([ - 'class' => 'overflow-x-auto', - ]), + ->copyOnClick(fn () => request()->isSecure()), SmallStatBlock::make('CPU', $this->cpuUsage()), SmallStatBlock::make('Memory', $this->memoryUsage()), SmallStatBlock::make('Disk', $this->diskUsage()), @@ -93,4 +91,16 @@ class ServerOverview extends StatsOverviewWidget return $used . ($this->server->disk > 0 ? ' / ' . $total : ' / ∞'); } + + #[On('copyClick')] + public function copyClick(string $value): void + { + $this->js("window.navigator.clipboard.writeText('{$value}');"); + + Notification::make() + ->title('Copied to clipboard') + ->body($value) + ->success() + ->send(); + } } diff --git a/app/Http/Controllers/Api/Client/Servers/NetworkAllocationController.php b/app/Http/Controllers/Api/Client/Servers/NetworkAllocationController.php index 55e8023ac..c287addb6 100644 --- a/app/Http/Controllers/Api/Client/Servers/NetworkAllocationController.php +++ b/app/Http/Controllers/Api/Client/Servers/NetworkAllocationController.php @@ -62,7 +62,7 @@ class NetworkAllocationController extends ClientApiController if ($original !== $allocation->notes) { Activity::event('server:allocation.notes') ->subject($allocation) - ->property(['allocation' => $allocation->toString(), 'old' => $original, 'new' => $allocation->notes]) + ->property(['allocation' => $allocation->address, 'old' => $original, 'new' => $allocation->notes]) ->log(); } @@ -87,7 +87,7 @@ class NetworkAllocationController extends ClientApiController Activity::event('server:allocation.primary') ->subject($allocation) - ->property('allocation', $allocation->toString()) + ->property('allocation', $allocation->address) ->log(); return $this->fractal->item($allocation) @@ -114,7 +114,7 @@ class NetworkAllocationController extends ClientApiController Activity::event('server:allocation.create') ->subject($allocation) - ->property('allocation', $allocation->toString()) + ->property('allocation', $allocation->address) ->log(); return $this->fractal->item($allocation) @@ -148,7 +148,7 @@ class NetworkAllocationController extends ClientApiController Activity::event('server:allocation.delete') ->subject($allocation) - ->property('allocation', $allocation->toString()) + ->property('allocation', $allocation->address) ->log(); return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT); diff --git a/app/Http/Controllers/Api/Client/Servers/PowerController.php b/app/Http/Controllers/Api/Client/Servers/PowerController.php index b1843d192..dcf78e0a7 100644 --- a/app/Http/Controllers/Api/Client/Servers/PowerController.php +++ b/app/Http/Controllers/Api/Client/Servers/PowerController.php @@ -9,6 +9,7 @@ use App\Repositories\Daemon\DaemonPowerRepository; use App\Http\Controllers\Api\Client\ClientApiController; use App\Http\Requests\Api\Client\Servers\SendPowerRequest; use Dedoc\Scramble\Attributes\Group; +use Illuminate\Http\Client\ConnectionException; #[Group('Server', weight: 2)] class PowerController extends ClientApiController @@ -25,6 +26,8 @@ class PowerController extends ClientApiController * Send power action * * Send a power action to a server. + * + * @throws ConnectionException */ public function index(SendPowerRequest $request, Server $server): Response { diff --git a/app/Http/Controllers/Api/Client/Servers/SettingsController.php b/app/Http/Controllers/Api/Client/Servers/SettingsController.php index 169f825b5..2a2577c80 100644 --- a/app/Http/Controllers/Api/Client/Servers/SettingsController.php +++ b/app/Http/Controllers/Api/Client/Servers/SettingsController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers\Api\Client\Servers; use App\Facades\Activity; use App\Http\Controllers\Api\Client\ClientApiController; +use App\Http\Requests\Api\Client\Servers\Settings\DescriptionServerRequest; use App\Http\Requests\Api\Client\Servers\Settings\ReinstallServerRequest; use App\Http\Requests\Api\Client\Servers\Settings\RenameServerRequest; use App\Http\Requests\Api\Client\Servers\Settings\SetDockerImageRequest; @@ -33,25 +34,39 @@ class SettingsController extends ClientApiController */ public function rename(RenameServerRequest $request, Server $server): JsonResponse { + $originalName = $server->name; $name = $request->input('name'); - $description = $request->has('description') ? (string) $request->input('description') : $server->description; - if ($server->name !== $name) { + $server->update(['name' => $name]); + + if ($server->wasChanged('name')) { Activity::event('server:settings.rename') - ->property(['old' => $server->name, 'new' => $name]) + ->property(['old' => $originalName, 'new' => $name]) ->log(); - $server->name = $name; } - if ($server->description !== $description && config('panel.editable_server_descriptions')) { + return new JsonResponse([], Response::HTTP_NO_CONTENT); + } + + /** + * Update server description + */ + public function description(DescriptionServerRequest $request, Server $server): JsonResponse + { + if (!config('panel.editable_server_descriptions')) { + return new JsonResponse([], Response::HTTP_FORBIDDEN); + } + + $originalDescription = $server->description; + $description = $request->input('description'); + $server->update(['description' => $description ?? '']); + + if ($server->wasChanged('description')) { Activity::event('server:settings.description') - ->property(['old' => $server->description, 'new' => $description]) + ->property(['old' => $originalDescription, 'new' => $description]) ->log(); - $server->description = $description; } - $server->save(); - return new JsonResponse([], Response::HTTP_NO_CONTENT); } @@ -80,7 +95,7 @@ class SettingsController extends ClientApiController */ public function dockerImage(SetDockerImageRequest $request, Server $server): JsonResponse { - if (!in_array($server->image, array_values($server->egg->docker_images))) { + if (!in_array($server->image, $server->egg->docker_images)) { throw new BadRequestHttpException('This server\'s Docker image has been manually set by an administrator and cannot be updated.'); } diff --git a/app/Http/Requests/Api/Application/Nodes/StoreNodeRequest.php b/app/Http/Requests/Api/Application/Nodes/StoreNodeRequest.php index c24d5518a..2f5b67587 100644 --- a/app/Http/Requests/Api/Application/Nodes/StoreNodeRequest.php +++ b/app/Http/Requests/Api/Application/Nodes/StoreNodeRequest.php @@ -18,26 +18,7 @@ class StoreNodeRequest extends ApplicationApiRequest */ public function rules(?array $rules = null): array { - return collect($rules ?? Node::getRules())->only([ - 'public', - 'name', - 'description', - 'fqdn', - 'scheme', - 'behind_proxy', - 'maintenance_mode', - 'memory', - 'memory_overallocate', - 'disk', - 'disk_overallocate', - 'cpu', - 'cpu_overallocate', - 'upload_size', - 'daemon_listen', - 'daemon_sftp', - 'daemon_sftp_alias', - 'daemon_base', - ])->mapWithKeys(function ($value, $key) { + return collect($rules ?? Node::getRules())->mapWithKeys(function ($value, $key) { return [snake_case($key) => $value]; })->toArray(); } diff --git a/app/Http/Requests/Api/Client/Servers/Settings/DescriptionServerRequest.php b/app/Http/Requests/Api/Client/Servers/Settings/DescriptionServerRequest.php new file mode 100644 index 000000000..ba2fa2125 --- /dev/null +++ b/app/Http/Requests/Api/Client/Servers/Settings/DescriptionServerRequest.php @@ -0,0 +1,30 @@ + 'string|nullable', + ]; + } +} diff --git a/app/Http/Requests/Api/Client/Servers/Settings/RenameServerRequest.php b/app/Http/Requests/Api/Client/Servers/Settings/RenameServerRequest.php index c68e0d444..fb7d8e677 100644 --- a/app/Http/Requests/Api/Client/Servers/Settings/RenameServerRequest.php +++ b/app/Http/Requests/Api/Client/Servers/Settings/RenameServerRequest.php @@ -26,7 +26,6 @@ class RenameServerRequest extends ClientApiRequest implements ClientPermissionsR { return [ 'name' => Server::getRules()['name'], - 'description' => 'string|nullable', ]; } } diff --git a/app/Http/Requests/Api/Client/Servers/Subusers/SubuserRequest.php b/app/Http/Requests/Api/Client/Servers/Subusers/SubuserRequest.php index cb9933256..b2f0e201e 100644 --- a/app/Http/Requests/Api/Client/Servers/Subusers/SubuserRequest.php +++ b/app/Http/Requests/Api/Client/Servers/Subusers/SubuserRequest.php @@ -58,7 +58,7 @@ abstract class SubuserRequest extends ClientApiRequest $server = $this->route()->parameter('server'); // If we are an admin or the server owner, no need to perform these checks. - if ($user->can('update server', $server) || $user->id === $server->owner_id) { + if ($user->can('update', $server) || $user->id === $server->owner_id) { return; } diff --git a/app/Jobs/NodeStatistics.php b/app/Jobs/NodeStatistics.php deleted file mode 100644 index d6815ce6c..000000000 --- a/app/Jobs/NodeStatistics.php +++ /dev/null @@ -1,35 +0,0 @@ -statistics(); - $timestamp = now()->getTimestamp(); - - foreach ($stats as $key => $value) { - $cacheKey = "nodes.{$node->id}.$key"; - $data = cache()->get($cacheKey, []); - - // Add current timestamp and value to the data array - $data[$timestamp] = $value; - - // Update the cache with the new data, expires in 1 minute - cache()->put($cacheKey, $data, now()->addMinute()); - } - } - } -} diff --git a/app/Listeners/Auth/AuthenticationListener.php b/app/Listeners/Auth/AuthenticationListener.php index b06428bd2..5efaf0bff 100644 --- a/app/Listeners/Auth/AuthenticationListener.php +++ b/app/Listeners/Auth/AuthenticationListener.php @@ -4,7 +4,7 @@ namespace App\Listeners\Auth; use App\Facades\Activity; use Illuminate\Auth\Events\Failed; -use App\Events\Auth\DirectLogin; +use Illuminate\Auth\Events\Login; class AuthenticationListener { @@ -12,9 +12,10 @@ class AuthenticationListener * Handles an authentication event by logging the user and information about * the request. */ - public function handle(Failed|DirectLogin $event): void + public function handle(Failed|Login $event): void { $activity = Activity::withRequestMetadata(); + if ($event->user) { $activity = $activity->subject($event->user); } diff --git a/app/Listeners/Auth/PasswordResetListener.php b/app/Listeners/Auth/PasswordResetListener.php index 5daa84973..e64a96bee 100644 --- a/app/Listeners/Auth/PasswordResetListener.php +++ b/app/Listeners/Auth/PasswordResetListener.php @@ -2,22 +2,14 @@ namespace App\Listeners\Auth; -use Illuminate\Http\Request; use App\Facades\Activity; use Illuminate\Auth\Events\PasswordReset; class PasswordResetListener { - protected Request $request; - - public function __construct(Request $request) - { - $this->request = $request; - } - public function handle(PasswordReset $event): void { - Activity::event('event:password-reset') + Activity::event('auth:password-reset') ->withRequestMetadata() ->subject($event->user) ->log(); diff --git a/app/Livewire/ServerEntry.php b/app/Livewire/ServerEntry.php new file mode 100644 index 000000000..35b1a3095 --- /dev/null +++ b/app/Livewire/ServerEntry.php @@ -0,0 +1,64 @@ + +
+
+ +
+
+ +

+ {{ $server->name }} +

+
+ +
+
+

CPU

+

{{ Number::format(0, precision: 2, locale: auth()->user()->language ?? 'en') . '%' }}

+
+

{{ $server->formatResource('cpu', type: \App\Enums\ServerResourceType::Percentage, limit: true) }}

+
+
+

Memory

+

{{ convert_bytes_to_readable(0, decimals: 2) }}

+
+

{{ $server->formatResource('memory', limit: true) }}

+
+
+

Disk

+

{{ convert_bytes_to_readable(0, decimals: 2) }}

+
+

{{ $server->formatResource('disk', limit: true) }}

+
+ +
+
+ + HTML; + } +} diff --git a/app/Models/Allocation.php b/app/Models/Allocation.php index a63f57a50..bb642244f 100644 --- a/app/Models/Allocation.php +++ b/app/Models/Allocation.php @@ -117,15 +117,10 @@ class Allocation extends Model protected function address(): Attribute { return Attribute::make( - get: fn () => "$this->alias:$this->port", + get: fn () => (is_ipv6($this->alias) ? "[$this->alias]" : $this->alias) . ":$this->port", ); } - public function toString(): string - { - return $this->address; - } - /** * Gets information for the server associated with this allocation. */ diff --git a/app/Models/Egg.php b/app/Models/Egg.php index f3091016b..54883e4ba 100644 --- a/app/Models/Egg.php +++ b/app/Models/Egg.php @@ -11,6 +11,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\MorphToMany; use Illuminate\Support\Str; /** @@ -283,6 +284,11 @@ class Egg extends Model implements Validatable return $this->configFrom->file_denylist; } + public function mounts(): MorphToMany + { + return $this->morphToMany(Mount::class, 'mountable'); + } + /** * Gets all servers associated with this egg. */ diff --git a/app/Models/File.php b/app/Models/File.php index 90047faa3..9267fdd98 100644 --- a/app/Models/File.php +++ b/app/Models/File.php @@ -12,6 +12,9 @@ use Illuminate\Http\Client\ConnectionException; use Sushi\Sushi; /** + * \App\Models\File. + * + * @property int $id * @property string $name * @property Carbon $created_at * @property Carbon $modified_at @@ -27,12 +30,6 @@ class File extends Model { use Sushi; - protected $primaryKey = 'name'; - - public $incrementing = false; - - protected $keyType = 'string'; - protected int $sushiInsertChunkSize = 100; public const ARCHIVE_MIMES = [ diff --git a/app/Models/Mount.php b/app/Models/Mount.php index ebcd51e2d..b50d91bf2 100644 --- a/app/Models/Mount.php +++ b/app/Models/Mount.php @@ -5,7 +5,7 @@ namespace App\Models; use App\Contracts\Validatable; use App\Traits\HasValidation; use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Illuminate\Database\Eloquent\Relations\MorphToMany; /** * @property int $id @@ -102,24 +102,24 @@ class Mount extends Model implements Validatable /** * Returns all eggs that have this mount assigned. */ - public function eggs(): BelongsToMany + public function eggs(): MorphToMany { - return $this->belongsToMany(Egg::class); + return $this->morphedByMany(Egg::class, 'mountable'); } /** * Returns all nodes that have this mount assigned. */ - public function nodes(): BelongsToMany + public function nodes(): MorphToMany { - return $this->belongsToMany(Node::class); + return $this->morphedByMany(Node::class, 'mountable'); } /** * Returns all servers that have this mount assigned. */ - public function servers(): BelongsToMany + public function servers(): MorphToMany { - return $this->belongsToMany(Server::class); + return $this->morphedByMany(Server::class, 'mountable'); } } diff --git a/app/Models/MountNode.php b/app/Models/MountNode.php deleted file mode 100644 index 9c011a3b9..000000000 --- a/app/Models/MountNode.php +++ /dev/null @@ -1,14 +0,0 @@ - ['required', 'numeric', 'between:1,65535'], 'daemon_sftp_alias' => ['nullable', 'string'], 'daemon_listen' => ['required', 'numeric', 'between:1,65535'], + 'daemon_connect' => ['required', 'numeric', 'between:1,65535'], 'maintenance_mode' => ['boolean'], 'upload_size' => ['int', 'between:1,1024'], 'tags' => ['array'], @@ -122,6 +127,7 @@ class Node extends Model implements Validatable 'daemon_base' => '/var/lib/pelican/volumes', 'daemon_sftp' => 2022, 'daemon_listen' => 8080, + 'daemon_connect' => 8080, 'maintenance_mode' => false, 'tags' => '[]', ]; @@ -133,6 +139,7 @@ class Node extends Model implements Validatable 'disk' => 'integer', 'cpu' => 'integer', 'daemon_listen' => 'integer', + 'daemon_connect' => 'integer', 'daemon_sftp' => 'integer', 'daemon_token' => 'encrypted', 'behind_proxy' => 'boolean', @@ -168,7 +175,7 @@ class Node extends Model implements Validatable */ public function getConnectionAddress(): string { - return "$this->scheme://$this->fqdn:$this->daemon_listen"; + return "$this->scheme://$this->fqdn:$this->daemon_connect"; } /** @@ -214,7 +221,7 @@ class Node extends Model implements Validatable ], ], 'allowed_mounts' => $this->mounts->pluck('source')->toArray(), - 'remote' => route('filament.app.resources...index'), + 'remote' => config('app.url'), ]; } @@ -239,9 +246,9 @@ class Node extends Model implements Validatable return $this->maintenance_mode; } - public function mounts(): HasManyThrough + public function mounts(): MorphToMany { - return $this->hasManyThrough(Mount::class, MountNode::class, 'node_id', 'id', 'id', 'mount_id'); + return $this->morphToMany(Mount::class, 'mountable'); } /** @@ -268,6 +275,11 @@ class Node extends Model implements Validatable return $this->belongsToMany(DatabaseHost::class); } + public function roles(): HasManyThrough + { + return $this->hasManyThrough(Role::class, NodeRole::class, 'node_id', 'id', 'id', 'role_id'); + } + /** * Returns a boolean if the node is viable for an additional server to be placed on it. */ @@ -321,26 +333,6 @@ class Node extends Model implements Validatable }); } - /** - * @return array - */ - public function serverStatuses(): array - { - $statuses = []; - try { - $statuses = Http::daemon($this)->connectTimeout(1)->timeout(1)->get('/api/servers')->json() ?? []; - } catch (Exception $exception) { - report($exception); - } - - foreach ($statuses as $status) { - $uuid = fluent($status)->get('configuration.uuid'); - cache()->remember("servers.$uuid.container.status", now()->addMinute(), fn () => fluent($status)->get('state')); - } - - return $statuses; - } - /** @return array{ * memory_total: int, memory_used: int, * swap_total: int, swap_used: int, @@ -396,10 +388,11 @@ class Node extends Model implements Validatable } } - // Only IPV4 - $ips = $ips->filter(fn (string $ip) => filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false); + $ips = $ips->filter(fn (string $ip) => is_ip($ip)); + // TODO: remove later $ips->push('0.0.0.0'); + $ips->push('::'); return $ips->unique()->all(); }); diff --git a/app/Models/NodeRole.php b/app/Models/NodeRole.php new file mode 100644 index 000000000..f9fe1e85c --- /dev/null +++ b/app/Models/NodeRole.php @@ -0,0 +1,12 @@ + ['required', 'string'], ]; - /** - * All the permissions available on the system. You should use self::permissions() - * to retrieve them, and not directly access this array as it is subject to change. - * - * @see Permission::permissions() - * - * @var array, - * }> - */ - protected static array $permissions = [ - 'websocket' => [ - 'description' => 'Allows the user to connect to the server websocket, giving them access to view console output and realtime server stats.', - 'keys' => [ - 'connect' => 'Allows a user to connect to the websocket instance for a server to stream the console.', - ], - ], - - 'control' => [ - 'description' => 'Permissions that control a user\'s ability to control the power state of a server, or send commands.', - 'keys' => [ - 'console' => 'Allows a user to send commands to the server instance via the console.', - 'start' => 'Allows a user to start the server if it is stopped.', - 'stop' => 'Allows a user to stop a server if it is running.', - 'restart' => 'Allows a user to perform a server restart. This allows them to start the server if it is offline, but not put the server in a completely stopped state.', - ], - ], - - 'user' => [ - 'description' => 'Permissions that allow a user to manage other subusers on a server. They will never be able to edit their own account, or assign permissions they do not have themselves.', - 'keys' => [ - 'create' => 'Allows a user to create new subusers for the server.', - 'read' => 'Allows the user to view subusers and their permissions for the server.', - 'update' => 'Allows a user to modify other subusers.', - 'delete' => 'Allows a user to delete a subuser from the server.', - ], - ], - - 'file' => [ - 'description' => 'Permissions that control a user\'s ability to modify the filesystem for this server.', - 'keys' => [ - 'create' => 'Allows a user to create additional files and folders via the Panel or direct upload.', - 'read' => 'Allows a user to view the contents of a directory, but not view the contents of or download files.', - 'read-content' => 'Allows a user to view the contents of a given file. This will also allow the user to download files.', - 'update' => 'Allows a user to update the contents of an existing file or directory.', - 'delete' => 'Allows a user to delete files or directories.', - 'archive' => 'Allows a user to archive the contents of a directory as well as decompress existing archives on the system.', - 'sftp' => 'Allows a user to connect to SFTP and manage server files using the other assigned file permissions.', - ], - ], - - 'backup' => [ - 'description' => 'Permissions that control a user\'s ability to generate and manage server backups.', - 'keys' => [ - 'create' => 'Allows a user to create new backups for this server.', - 'read' => 'Allows a user to view all backups that exist for this server.', - 'delete' => 'Allows a user to remove backups from the system.', - 'download' => 'Allows a user to download a backup for the server. Danger: this allows a user to access all files for the server in the backup.', - 'restore' => 'Allows a user to restore a backup for the server. Danger: this allows the user to delete all the server files in the process.', - ], - ], - - // Controls permissions for editing or viewing a server's allocations. - 'allocation' => [ - 'description' => 'Permissions that control a user\'s ability to modify the port allocations for this server.', - 'keys' => [ - 'read' => 'Allows a user to view all allocations currently assigned to this server. Users with any level of access to this server can always view the primary allocation.', - 'create' => 'Allows a user to assign additional allocations to the server.', - 'update' => 'Allows a user to change the primary server allocation and attach notes to each allocation.', - 'delete' => 'Allows a user to delete an allocation from the server.', - ], - ], - - // Controls permissions for editing or viewing a server's startup parameters. - 'startup' => [ - 'description' => 'Permissions that control a user\'s ability to view this server\'s startup parameters.', - 'keys' => [ - 'read' => 'Allows a user to view the startup variables for a server.', - 'update' => 'Allows a user to modify the startup variables for the server.', - 'docker-image' => 'Allows a user to modify the Docker image used when running the server.', - ], - ], - - 'database' => [ - 'description' => 'Permissions that control a user\'s access to the database management for this server.', - 'keys' => [ - 'create' => 'Allows a user to create a new database for this server.', - 'read' => 'Allows a user to view the database associated with this server.', - 'update' => 'Allows a user to rotate the password on a database instance. If the user does not have the view_password permission they will not see the updated password.', - 'delete' => 'Allows a user to remove a database instance from this server.', - 'view_password' => 'Allows a user to view the password associated with a database instance for this server.', - ], - ], - - 'schedule' => [ - 'description' => 'Permissions that control a user\'s access to the schedule management for this server.', - 'keys' => [ - 'create' => 'Allows a user to create new schedules for this server.', // task.create-schedule - 'read' => 'Allows a user to view schedules and the tasks associated with them for this server.', // task.view-schedule, task.list-schedules - 'update' => 'Allows a user to update schedules and schedule tasks for this server.', // task.edit-schedule, task.queue-schedule, task.toggle-schedule - 'delete' => 'Allows a user to delete schedules for this server.', // task.delete-schedule - ], - ], - - 'settings' => [ - 'description' => 'Permissions that control a user\'s access to the settings for this server.', - 'keys' => [ - 'rename' => 'Allows a user to rename this server and change the description of it.', - 'reinstall' => 'Allows a user to trigger a reinstall of this server.', - ], - ], - - 'activity' => [ - 'description' => 'Permissions that control a user\'s access to the server activity logs.', - 'keys' => [ - 'read' => 'Allows a user to view the activity logs for the server.', - ], - ], - ]; - protected function casts(): array { return [ @@ -242,11 +123,92 @@ class Permission extends Model implements Validatable ]; } + /** + * All the permissions available on the system. + * + * @return array + */ + public static function permissionData(): array + { + return [ + [ + 'name' => 'control', + 'icon' => 'tabler-terminal-2', + 'permissions' => ['console', 'start', 'stop', 'restart'], + ], + [ + 'name' => 'user', + 'icon' => 'tabler-users', + 'permissions' => ['read', 'create', 'update', 'delete'], + ], + [ + 'name' => 'file', + 'icon' => 'tabler-files', + 'permissions' => ['read', 'read-content', 'create', 'update', 'delete', 'archive', 'sftp'], + ], + [ + 'name' => 'backup', + 'icon' => 'tabler-file-zip', + 'permissions' => ['read', 'create', 'delete', 'download', 'restore'], + ], + [ + 'name' => 'allocation', + 'icon' => 'tabler-network', + 'permissions' => ['read', 'create', 'update', 'delete'], + ], + [ + 'name' => 'startup', + 'icon' => 'tabler-player-play', + 'permissions' => ['read', 'update', 'docker-image'], + ], + [ + 'name' => 'database', + 'icon' => 'tabler-database', + 'permissions' => ['read', 'create', 'update', 'delete', 'view-password'], + ], + [ + 'name' => 'schedule', + 'icon' => 'tabler-clock', + 'permissions' => ['read', 'create', 'update', 'delete'], + ], + [ + 'name' => 'settings', + 'icon' => 'tabler-settings', + 'permissions' => ['rename', 'description', 'reinstall'], + ], + [ + 'name' => 'activity', + 'icon' => 'tabler-stack', + 'permissions' => ['read'], + ], + ]; + } + /** * Returns all the permissions available on the system for a user to have when controlling a server. */ public static function permissions(): Collection { - return Collection::make(self::$permissions); + $permissions = [ + 'websocket' => [ + 'description' => 'Allows the user to connect to the server websocket, giving them access to view console output and realtime server stats.', + 'keys' => [ + 'connect' => 'Allows a user to connect to the websocket instance for a server to stream the console.', + ], + ], + ]; + + foreach (static::permissionData() as $data) { + $permissions[$data['name']] = [ + 'description' => trans('server/users.permissions.' . $data['name'] . '_desc'), + 'keys' => collect($data['permissions'])->mapWithKeys(fn ($key) => [$key => trans('server/users.permissions.' . $data['name'] . '_' . str($key)->replace('-', '_'))])->toArray(), + ]; + } + + return collect($permissions); } } diff --git a/app/Models/Role.php b/app/Models/Role.php index 5f48c62a0..16cbf8402 100644 --- a/app/Models/Role.php +++ b/app/Models/Role.php @@ -5,6 +5,7 @@ namespace App\Models; use App\Enums\RolePermissionModels; use App\Enums\RolePermissionPrefixes; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Spatie\Permission\Models\Role as BaseRole; /** @@ -15,6 +16,8 @@ use Spatie\Permission\Models\Role as BaseRole; * @property int|null $permissions_count * @property \Illuminate\Database\Eloquent\Collection|\App\Models\User[] $users * @property int|null $users_count + * @property \Illuminate\Database\Eloquent\Collection|\App\Models\Node[] $nodes + * @property int|null $nodes_count */ class Role extends BaseRole { @@ -128,4 +131,9 @@ class Role extends BaseRole return $role; } + + public function nodes(): BelongsToMany + { + return $this->belongsToMany(Node::class, NodeRole::class); + } } diff --git a/app/Models/Server.php b/app/Models/Server.php index 6285079ed..5ba473418 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -12,7 +12,6 @@ use Carbon\CarbonInterface; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Http\Client\ConnectionException; use Illuminate\Notifications\Notifiable; use Illuminate\Database\Query\JoinClause; @@ -232,7 +231,12 @@ class Server extends Model implements Validatable public function isInstalled(): bool { - return $this->status !== ServerState::Installing && $this->status !== ServerState::InstallFailed; + return $this->status !== ServerState::Installing && !$this->isFailedInstall(); + } + + public function isFailedInstall(): bool + { + return $this->status === ServerState::InstallFailed || $this->status === ServerState::ReinstallFailed; } public function isSuspended(): bool @@ -350,9 +354,9 @@ class Server extends Model implements Validatable return $this->hasMany(Backup::class); } - public function mounts(): BelongsToMany + public function mounts(): MorphToMany { - return $this->belongsToMany(Mount::class); + return $this->morphToMany(Mount::class, 'mountable'); } /** @@ -431,25 +435,24 @@ class Server extends Model implements Validatable public function retrieveStatus(): ContainerStatus { - $status = cache()->get("servers.$this->uuid.container.status"); + return cache()->remember("servers.$this->uuid.status", now()->addSeconds(15), function () { + // @phpstan-ignore myCustomRules.forbiddenGlobalFunctions + $details = app(DaemonServerRepository::class)->setServer($this)->getDetails(); - if ($status === null) { - $this->node->serverStatuses(); - - $status = cache()->get("servers.$this->uuid.container.status"); - } - - return ContainerStatus::tryFrom($status) ?? ContainerStatus::Missing; + return ContainerStatus::tryFrom(Arr::get($details, 'state')) ?? ContainerStatus::Missing; + }); } /** * @return array */ - public function resources(): array + public function retrieveResources(): array { - return cache()->remember("resources:$this->uuid", now()->addSeconds(15), function () { + return cache()->remember("servers.$this->uuid.resources", now()->addSeconds(15), function () { // @phpstan-ignore myCustomRules.forbiddenGlobalFunctions - return Arr::get(app(DaemonServerRepository::class)->setServer($this)->getDetails(), 'utilization', []); + $details = app(DaemonServerRepository::class)->setServer($this)->getDetails(); + + return Arr::get($details, 'utilization', []); }); } @@ -457,7 +460,7 @@ class Server extends Model implements Validatable { $resourceAmount = $this->{$resourceKey} ?? 0; if (!$limit) { - $resourceAmount = $this->resources()[$resourceKey] ?? 0; + $resourceAmount = $this->retrieveResources()[$resourceKey] ?? 0; } if ($type === ServerResourceType::Time) { @@ -490,7 +493,7 @@ class Server extends Model implements Validatable public function condition(): Attribute { return Attribute::make( - get: fn () => $this->isSuspended() ? ServerState::Suspended : $this->status ?? $this->retrieveStatus(), + get: fn () => $this->status ?? $this->retrieveStatus(), ); } } diff --git a/app/Models/User.php b/app/Models/User.php index 10e56427f..a5bdb8305 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -265,8 +265,13 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac */ public function accessibleServers(): Builder { - if ($this->canned('viewList server')) { - return Server::query(); + if ($this->canned('viewAny', Server::class)) { + return Server::select('servers.*') + ->leftJoin('subusers', 'subusers.server_id', '=', 'servers.id') + ->where(function (Builder $builder) { + $builder->where('servers.owner_id', $this->id)->orWhere('subusers.user_id', $this->id)->orWhereIn('servers.node_id', $this->accessibleNodes()->pluck('id')); + }) + ->distinct('servers.id'); } return $this->directAccessibleServers(); @@ -278,14 +283,29 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac */ public function directAccessibleServers(): Builder { - return Server::query() - ->select('servers.*') + return Server::select('servers.*') ->leftJoin('subusers', 'subusers.server_id', '=', 'servers.id') ->where(function (Builder $builder) { $builder->where('servers.owner_id', $this->id)->orWhere('subusers.user_id', $this->id); }); } + public function accessibleNodes(): Builder + { + // Root admins can access all nodes + if ($this->isRootAdmin()) { + return Node::query(); + } + + // Check if there are no restrictions from any role + $roleIds = $this->roles()->pluck('id'); + if (!NodeRole::whereIn('role_id', $roleIds)->exists()) { + return Node::query(); + } + + return Node::whereHas('roles', fn (Builder $builder) => $builder->whereIn('roles.id', $roleIds)); + } + public function subusers(): HasMany { return $this->hasMany(Subuser::class); @@ -298,12 +318,12 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac protected function checkPermission(Server $server, string $permission = ''): bool { - if ($this->canned('update server', $server) || $server->owner_id === $this->id) { + if ($this->canned('update', $server) || $server->owner_id === $this->id) { return true; } // If the user only has "view" permissions allow viewing the console - if ($permission === Permission::ACTION_WEBSOCKET_CONNECT && $this->canned('view server', $server)) { + if ($permission === Permission::ACTION_WEBSOCKET_CONNECT && $this->canned('view', $server)) { return true; } @@ -390,13 +410,24 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac return $provider?->get($this); } - public function canTarget(Model $user): bool + public function canTarget(Model $model): bool { + // Root admins can target everyone and everything if ($this->isRootAdmin()) { return true; } - return $user instanceof User && !$user->isRootAdmin(); + // Make sure normal admins can't target root admins + if ($model instanceof User) { + return !$model->isRootAdmin(); + } + + // Make sure the user can only target accessible nodes + if ($model instanceof Node) { + return $this->accessibleNodes()->where('id', $model->id)->exists(); + } + + return false; } public function getTenants(Panel $panel): array|Collection @@ -407,7 +438,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac public function canAccessTenant(Model $tenant): bool { if ($tenant instanceof Server) { - if ($this->canned('view server', $tenant) || $tenant->owner_id === $this->id) { + if ($this->canned('view', $tenant) || $tenant->owner_id === $this->id) { return true; } diff --git a/app/Notifications/AccountCreated.php b/app/Notifications/AccountCreated.php index 4f3ba975d..00459842d 100644 --- a/app/Notifications/AccountCreated.php +++ b/app/Notifications/AccountCreated.php @@ -30,7 +30,7 @@ class AccountCreated extends Notification implements ShouldQueue ->line('Email: ' . $notifiable->email); if (!is_null($this->token)) { - return $message->action('Setup Your Account', Filament::getResetPasswordUrl($this->token, $notifiable)); + return $message->action('Setup Your Account', Filament::getPanel('app')->getResetPasswordUrl($this->token, $notifiable)); } return $message; diff --git a/app/Policies/DatabaseHostPolicy.php b/app/Policies/DatabaseHostPolicy.php index b93954064..75eb8386d 100644 --- a/app/Policies/DatabaseHostPolicy.php +++ b/app/Policies/DatabaseHostPolicy.php @@ -2,9 +2,28 @@ namespace App\Policies; +use App\Models\DatabaseHost; +use App\Models\User; + class DatabaseHostPolicy { use DefaultPolicies; protected string $modelName = 'databasehost'; + + public function before(User $user, string $ability, string|DatabaseHost $databaseHost): ?bool + { + // For "viewAny" the $databaseHost param is the class name + if (is_string($databaseHost)) { + return null; + } + + foreach ($databaseHost->nodes as $node) { + if (!$user->canTarget($node)) { + return false; + } + } + + return null; + } } diff --git a/app/Policies/MountPolicy.php b/app/Policies/MountPolicy.php index 4f9d58b63..9495dd08d 100644 --- a/app/Policies/MountPolicy.php +++ b/app/Policies/MountPolicy.php @@ -2,9 +2,28 @@ namespace App\Policies; +use App\Models\Mount; +use App\Models\User; + class MountPolicy { use DefaultPolicies; protected string $modelName = 'mount'; + + public function before(User $user, string $ability, string|Mount $mount): ?bool + { + // For "viewAny" the $mount param is the class name + if (is_string($mount)) { + return null; + } + + foreach ($mount->nodes as $node) { + if (!$user->canTarget($node)) { + return false; + } + } + + return null; + } } diff --git a/app/Policies/NodePolicy.php b/app/Policies/NodePolicy.php index 8f23cc666..6a459155c 100644 --- a/app/Policies/NodePolicy.php +++ b/app/Policies/NodePolicy.php @@ -2,9 +2,26 @@ namespace App\Policies; +use App\Models\Node; +use App\Models\User; + class NodePolicy { use DefaultPolicies; protected string $modelName = 'node'; + + public function before(User $user, string $ability, string|Node $node): ?bool + { + // For "viewAny" the $node param is the class name + if (is_string($node)) { + return null; + } + + if (!$user->canTarget($node)) { + return false; + } + + return null; + } } diff --git a/app/Policies/ServerPolicy.php b/app/Policies/ServerPolicy.php index 48d4e5cc3..539ae2712 100644 --- a/app/Policies/ServerPolicy.php +++ b/app/Policies/ServerPolicy.php @@ -32,6 +32,11 @@ class ServerPolicy return true; } + // Make sure user can target node of the server + if (!$user->canTarget($server->node)) { + return false; + } + // Return null to let default policies take over return null; } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 7ba52a820..4c3e7ea34 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -82,6 +82,7 @@ class AppServiceProvider extends ServiceProvider 'ssh_key' => Models\UserSSHKey::class, 'task' => Models\Task::class, 'user' => Models\User::class, + 'node' => Models\Node::class, ]); Http::macro( diff --git a/app/Providers/Filament/ServerPanelProvider.php b/app/Providers/Filament/ServerPanelProvider.php index 23cddb08d..f15451a0d 100644 --- a/app/Providers/Filament/ServerPanelProvider.php +++ b/app/Providers/Filament/ServerPanelProvider.php @@ -63,7 +63,7 @@ class ServerPanelProvider extends PanelProvider ->navigationItems([ NavigationItem::make('Open in Admin') ->url(fn () => EditServer::getUrl(['record' => Filament::getTenant()], panel: 'admin')) - ->visible(fn () => auth()->user()->can('view server', Filament::getTenant())) + ->visible(fn () => auth()->user()->canAccessPanel(Filament::getPanel('admin')) && auth()->user()->can('view server', Filament::getTenant())) ->icon('tabler-arrow-back') ->sort(99), ]) diff --git a/app/Repositories/Daemon/DaemonConfigurationRepository.php b/app/Repositories/Daemon/DaemonConfigurationRepository.php index 48d2b35c4..916e7d459 100644 --- a/app/Repositories/Daemon/DaemonConfigurationRepository.php +++ b/app/Repositories/Daemon/DaemonConfigurationRepository.php @@ -21,14 +21,7 @@ class DaemonConfigurationRepository extends DaemonRepository ->connectTimeout(3) ->get('/api/system') ->throwIf(function ($result) { - $header = $result->header('User-Agent'); - if ( - filled($header) && - preg_match('/^Pelican Wings\/v(?:\d+\.\d+\.\d+|develop) \(id:(\w*)\)$/', $header, $matches) && - array_get($matches, 1, '') !== $this->node->daemon_token_id - ) { - throw new ConnectionException($result->effectiveUri()->__toString() . ' does not match node token_id !'); - } + $this->enforceValidNodeToken($result); if (!$result->collect()->has(['architecture', 'cpu_count', 'kernel_version', 'os', 'version'])) { throw new ConnectionException($result->effectiveUri()->__toString() . ' is not Pelican Wings !'); } diff --git a/app/Repositories/Daemon/DaemonRepository.php b/app/Repositories/Daemon/DaemonRepository.php index ae7440a50..6b3b553d6 100644 --- a/app/Repositories/Daemon/DaemonRepository.php +++ b/app/Repositories/Daemon/DaemonRepository.php @@ -7,6 +7,8 @@ use Illuminate\Http\Client\PendingRequest; use Illuminate\Support\Facades\Http; use Webmozart\Assert\Assert; use App\Models\Server; +use Illuminate\Http\Client\ConnectionException; +use Illuminate\Http\Client\Response; abstract class DaemonRepository { @@ -47,6 +49,24 @@ abstract class DaemonRepository { Assert::isInstanceOf($this->node, Node::class); - return Http::daemon($this->node, $headers); + return Http::daemon($this->node, $headers)->throwIf(fn ($condition) => $this->enforceValidNodeToken($condition)); + } + + protected function enforceValidNodeToken(Response|bool $condition): bool + { + if (is_bool($condition)) { + return $condition; + } + + $header = $condition->header('User-Agent'); + if ( + empty($header) || + preg_match('/^Pelican Wings\/v(?:\d+\.\d+\.\d+|develop) \(id:(\w*)\)$/', $header, $matches) && + array_get($matches, 1, '') !== $this->node->daemon_token_id + ) { + throw new ConnectionException($condition->effectiveUri()->__toString() . ' does not match node token_id !'); + } + + return true; } } diff --git a/app/Repositories/Daemon/DaemonServerRepository.php b/app/Repositories/Daemon/DaemonServerRepository.php index 673673b98..ae88ceadd 100644 --- a/app/Repositories/Daemon/DaemonServerRepository.php +++ b/app/Repositories/Daemon/DaemonServerRepository.php @@ -19,7 +19,7 @@ class DaemonServerRepository extends DaemonRepository public function getDetails(): array { try { - return $this->getHttpClient()->get("/api/servers/{$this->server->uuid}")->throw()->json(); + return $this->getHttpClient()->connectTimeout(1)->timeout(1)->get("/api/servers/{$this->server->uuid}")->throw()->json(); } catch (RequestException $exception) { $cfId = $exception->response->header('Cf-Ray'); $cfCache = $exception->response->header('Cf-Cache-Status'); @@ -141,4 +141,12 @@ class DaemonServerRepository extends DaemonRepository 'jtis' => [md5($id . $this->server->uuid)], ]); } + + public function getInstallLogs(): string + { + return $this->getHttpClient() + ->get("/api/servers/{$this->server->uuid}/install-logs") + ->throw() + ->json('data'); + } } diff --git a/app/Services/Allocations/AssignmentService.php b/app/Services/Allocations/AssignmentService.php index 7f52713e0..39c67c37e 100644 --- a/app/Services/Allocations/AssignmentService.php +++ b/app/Services/Allocations/AssignmentService.php @@ -51,12 +51,7 @@ class AssignmentService } try { - // TODO: how should we approach supporting IPv6 with this? - // gethostbyname only supports IPv4, but the alternative (dns_get_record) returns - // an array of records, which is not ideal for this use case, we need a SINGLE - // IP to use, not multiple. - $underlying = gethostbyname($data['allocation_ip']); - $parsed = Network::parse($underlying); + $parsed = Network::parse($data['allocation_ip']); } catch (\Exception $exception) { throw new DisplayException("Could not parse provided allocation IP address ({$data['allocation_ip']}): {$exception->getMessage()}", $exception); } diff --git a/app/Services/Backups/DeleteBackupService.php b/app/Services/Backups/DeleteBackupService.php index a2402fdee..04cf9e548 100644 --- a/app/Services/Backups/DeleteBackupService.php +++ b/app/Services/Backups/DeleteBackupService.php @@ -6,12 +6,11 @@ use App\Extensions\Filesystem\S3Filesystem; use Aws\S3\S3Client; use Illuminate\Http\Response; use App\Models\Backup; -use GuzzleHttp\Exception\ClientException; use Illuminate\Database\ConnectionInterface; use App\Extensions\Backups\BackupManager; use App\Repositories\Daemon\DaemonBackupRepository; use App\Exceptions\Service\Backup\BackupLockedException; -use Illuminate\Http\Client\ConnectionException; +use Exception; class DeleteBackupService { @@ -48,12 +47,10 @@ class DeleteBackupService $this->connection->transaction(function () use ($backup) { try { $this->daemonBackupRepository->setServer($backup->server)->delete($backup); - } catch (ConnectionException $exception) { - $previous = $exception->getPrevious(); - + } catch (Exception $exception) { // Don't fail the request if the Daemon responds with a 404, just assume the backup // doesn't actually exist and remove its reference from the Panel as well. - if (!$previous instanceof ClientException || $previous->getResponse()->getStatusCode() !== Response::HTTP_NOT_FOUND) { + if ($exception->getCode() !== Response::HTTP_NOT_FOUND) { throw $exception; } } diff --git a/app/Services/Servers/GetUserPermissionsService.php b/app/Services/Servers/GetUserPermissionsService.php index f9d0e31fa..72c74bbf0 100644 --- a/app/Services/Servers/GetUserPermissionsService.php +++ b/app/Services/Servers/GetUserPermissionsService.php @@ -16,8 +16,8 @@ class GetUserPermissionsService */ public function handle(Server $server, User $user): array { - if ($user->isAdmin() && ($user->can('view server', $server) || $user->can('update server', $server))) { - $permissions = $user->can('update server', $server) ? ['*'] : ['websocket.connect', 'backup.read']; + if ($user->isAdmin() && ($user->can('view', $server) || $user->can('update', $server))) { + $permissions = $user->can('update', $server) ? ['*'] : ['websocket.connect', 'backup.read']; $permissions[] = 'admin.websocket.errors'; $permissions[] = 'admin.websocket.install'; diff --git a/app/Services/Servers/ToggleInstallService.php b/app/Services/Servers/ToggleInstallService.php index 01ee6de41..770db8443 100644 --- a/app/Services/Servers/ToggleInstallService.php +++ b/app/Services/Servers/ToggleInstallService.php @@ -9,7 +9,7 @@ class ToggleInstallService { public function handle(Server $server): void { - if ($server->status === ServerState::InstallFailed) { + if ($server->isFailedInstall()) { abort(500, trans('exceptions.server.marked_as_failed')); } diff --git a/app/Services/Servers/TransferServerService.php b/app/Services/Servers/TransferServerService.php index f394f4c7c..f9df5d647 100644 --- a/app/Services/Servers/TransferServerService.php +++ b/app/Services/Servers/TransferServerService.php @@ -47,7 +47,7 @@ class TransferServerService // Check if the node is viable for the transfer. $node = Node::query() - ->select(['nodes.id', 'nodes.fqdn', 'nodes.scheme', 'nodes.daemon_token', 'nodes.daemon_listen', 'nodes.memory', 'nodes.disk', 'nodes.cpu', 'nodes.memory_overallocate', 'nodes.disk_overallocate', 'nodes.cpu_overallocate']) + ->select(['nodes.id', 'nodes.fqdn', 'nodes.scheme', 'nodes.daemon_token', 'nodes.daemon_connect', 'nodes.memory', 'nodes.disk', 'nodes.cpu', 'nodes.memory_overallocate', 'nodes.disk_overallocate', 'nodes.cpu_overallocate']) ->withSum('servers', 'disk') ->withSum('servers', 'memory') ->withSum('servers', 'cpu') diff --git a/app/Traits/EnvironmentWriterTrait.php b/app/Traits/EnvironmentWriterTrait.php index 20a0c1680..5df4cdb1d 100644 --- a/app/Traits/EnvironmentWriterTrait.php +++ b/app/Traits/EnvironmentWriterTrait.php @@ -2,54 +2,20 @@ namespace App\Traits; -use Exception; +use Illuminate\Support\Env; +use RuntimeException; trait EnvironmentWriterTrait { - /** - * Escapes an environment value by looking for any characters that could - * reasonably cause environment parsing issues. Those values are then wrapped - * in quotes before being returned. - */ - public function escapeEnvironmentValue(string $value): string - { - if (!preg_match('/^\"(.*)\"$/', $value) && preg_match('/([^\w.\-+\/])+/', $value)) { - return sprintf('"%s"', addcslashes($value, '\\"')); - } - - return $value; - } - /** * Update the .env file for the application using the passed in values. * * @param array $values * - * @throws Exception + * @throws RuntimeException */ public function writeToEnvironment(array $values = []): void { - $path = base_path('.env'); - if (!file_exists($path)) { - throw new Exception('Cannot locate .env file, was this software installed correctly?'); - } - - $saveContents = file_get_contents($path); - if ($saveContents === false) { - $saveContents = ''; - } - - collect($values)->each(function ($value, $key) use (&$saveContents) { - $key = strtoupper($key); - $saveValue = sprintf('%s=%s', $key, $this->escapeEnvironmentValue($value ?? '')); - - if (preg_match_all('/^' . $key . '=(.*)$/m', $saveContents) < 1) { - $saveContents = $saveContents . PHP_EOL . $saveValue; - } else { - $saveContents = preg_replace('/^' . $key . '=(.*)$/m', $saveValue, $saveContents); - } - }); - - file_put_contents($path, $saveContents); + Env::writeVariables($values, base_path('.env'), true); } } diff --git a/app/Traits/Filament/BlockAccessInConflict.php b/app/Traits/Filament/BlockAccessInConflict.php new file mode 100644 index 000000000..a45f72fd2 --- /dev/null +++ b/app/Traits/Filament/BlockAccessInConflict.php @@ -0,0 +1,21 @@ +isInConflictState()) { + return false; + } + + return parent::canAccess(); + } +} diff --git a/app/Traits/Filament/CanCustomizeHeaderActions.php b/app/Traits/Filament/CanCustomizeHeaderActions.php new file mode 100644 index 000000000..a8dd5ecfc --- /dev/null +++ b/app/Traits/Filament/CanCustomizeHeaderActions.php @@ -0,0 +1,34 @@ + */ + protected static array $customHeaderActions = []; + + public static function registerCustomHeaderActions(HeaderActionPosition $position, Action|ActionGroup ...$customHeaderActions): void + { + static::$customHeaderActions[$position->value] = array_merge(static::$customHeaderActions[$position->value] ?? [], $customHeaderActions); + } + + /** @return array */ + protected function getDefaultHeaderActions(): array + { + return []; + } + + /** @return array */ + protected function getHeaderActions(): array + { + return array_merge( + static::$customHeaderActions[HeaderActionPosition::Before->value] ?? [], + $this->getDefaultHeaderActions(), + static::$customHeaderActions[HeaderActionPosition::After->value] ?? [] + ); + } +} diff --git a/app/Traits/Filament/CanCustomizeHeaderWidgets.php b/app/Traits/Filament/CanCustomizeHeaderWidgets.php new file mode 100644 index 000000000..6972b22e8 --- /dev/null +++ b/app/Traits/Filament/CanCustomizeHeaderWidgets.php @@ -0,0 +1,34 @@ +|WidgetConfiguration> */ + protected static array $customHeaderWidgets = []; + + public static function registerCustomHeaderWidgets(HeaderWidgetPosition $position, string|WidgetConfiguration ...$customHeaderWidgets): void + { + static::$customHeaderWidgets[$position->value] = array_merge(static::$customHeaderWidgets[$position->value] ?? [], $customHeaderWidgets); + } + + /** @return array|WidgetConfiguration> */ + protected function getDefaultHeaderWidgets(): array + { + return []; + } + + /** @return array|WidgetConfiguration> */ + protected function getHeaderWidgets(): array + { + return array_merge( + static::$customHeaderWidgets[HeaderWidgetPosition::Before->value] ?? [], + $this->getDefaultHeaderWidgets(), + static::$customHeaderWidgets[HeaderWidgetPosition::After->value] ?? [] + ); + } +} diff --git a/app/Traits/Filament/CanCustomizePages.php b/app/Traits/Filament/CanCustomizePages.php new file mode 100644 index 000000000..d962c51d8 --- /dev/null +++ b/app/Traits/Filament/CanCustomizePages.php @@ -0,0 +1,29 @@ + */ + protected static array $customPages = []; + + /** @param array $customPages */ + public static function registerCustomPages(array $customPages): void + { + static::$customPages = array_merge(static::$customPages, $customPages); + } + + /** @return array */ + public static function getDefaultPages(): array + { + return []; + } + + /** @return array */ + public static function getPages(): array + { + return array_unique(array_merge(static::getDefaultPages(), static::$customPages), SORT_REGULAR); + } +} diff --git a/app/Traits/Filament/CanCustomizeRelations.php b/app/Traits/Filament/CanCustomizeRelations.php new file mode 100644 index 000000000..13682a249 --- /dev/null +++ b/app/Traits/Filament/CanCustomizeRelations.php @@ -0,0 +1,28 @@ +> */ + protected static array $customRelations = []; + + public static function registerCustomRelations(string ...$customRelations): void + { + static::$customRelations = array_merge(static::$customRelations, $customRelations); + } + + /** @return class-string[] */ + public static function getDefaultRelations(): array + { + return []; + } + + /** @return class-string[] */ + public static function getRelations(): array + { + return array_unique(array_merge(static::getDefaultRelations(), static::$customRelations)); + } +} diff --git a/app/Traits/Filament/CanModifyForm.php b/app/Traits/Filament/CanModifyForm.php new file mode 100644 index 000000000..57f1a7db7 --- /dev/null +++ b/app/Traits/Filament/CanModifyForm.php @@ -0,0 +1,33 @@ + */ + protected static array $customFormModifications = []; + + public static function modifyForm(Closure $closure): void + { + static::$customFormModifications[] = $closure; + } + + public static function defaultForm(Form $form): Form + { + return $form; + } + + public static function form(Form $form): Form + { + $form = static::defaultForm($form); + + foreach (static::$customFormModifications as $closure) { + $form = $closure($form); + } + + return $form; + } +} diff --git a/app/Traits/Filament/CanModifyTable.php b/app/Traits/Filament/CanModifyTable.php new file mode 100644 index 000000000..96968b13c --- /dev/null +++ b/app/Traits/Filament/CanModifyTable.php @@ -0,0 +1,33 @@ + */ + protected static array $customTableModifications = []; + + public static function modifyTable(Closure $closure): void + { + static::$customTableModifications[] = $closure; + } + + public static function defaultTable(Table $table): Table + { + return $table; + } + + public static function table(Table $table): Table + { + $table = static::defaultTable($table); + + foreach (static::$customTableModifications as $closure) { + $table = $closure($table); + } + + return $table; + } +} diff --git a/app/Traits/Filament/HasLimitBadge.php b/app/Traits/Filament/HasLimitBadge.php new file mode 100644 index 000000000..c7d41a243 --- /dev/null +++ b/app/Traits/Filament/HasLimitBadge.php @@ -0,0 +1,38 @@ += $limit ? 'danger' : ($count >= $limit * self::WARNING_THRESHOLD ? 'warning' : 'success'); + } +} diff --git a/app/helpers.php b/app/helpers.php index 0fe63c7a5..6098eff07 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -18,6 +18,20 @@ if (!function_exists('is_ip')) { } } +if (!function_exists('is_ipv4')) { + function is_ipv4(?string $address): bool + { + return $address !== null && filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false; + } +} + +if (!function_exists('is_ipv6')) { + function is_ipv6(?string $address): bool + { + return $address !== null && filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false; + } +} + if (!function_exists('convert_bytes_to_readable')) { function convert_bytes_to_readable(int $bytes, int $decimals = 2, ?int $base = null): string { diff --git a/composer.json b/composer.json index 7c57ff592..3229c9341 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,7 @@ { "name": "pelican-dev/panel", "description": "The free, open-source game management panel. Supporting Minecraft, Spigot, BungeeCord, and SRCDS servers.", + "license": "AGPL-3.0-only", "require": { "php": "^8.2 || ^8.3 || ^8.4", "ext-intl": "*", @@ -8,17 +9,17 @@ "ext-mbstring": "*", "ext-pdo": "*", "ext-zip": "*", - "aws/aws-sdk-php": "^3.342", + "aws/aws-sdk-php": "^3.344", "calebporzio/sushi": "^2.5", "chillerlan/php-qrcode": "^5.0.2", "dedoc/scramble": "^0.12.10", "doctrine/dbal": "~3.6.0", "filament/filament": "v4.0.0-beta7", "guzzlehttp/guzzle": "^7.9", - "laravel/framework": "^12.10", + "laravel/framework": "^12.19", "laravel/helpers": "^1.7", - "laravel/sanctum": "^4.0.2", - "laravel/socialite": "^5.19", + "laravel/sanctum": "^4.1", + "laravel/socialite": "^5.21", "laravel/tinker": "^2.10.1", "laravel/ui": "^4.6", "lcobucci/jwt": "~4.3.0", @@ -32,10 +33,10 @@ "socialiteproviders/authentik": "^5.2", "socialiteproviders/discord": "^4.2", "socialiteproviders/steam": "^4.3", - "spatie/laravel-data": "^4.14", + "spatie/laravel-data": "^4.15", "spatie/laravel-fractal": "^6.3", "spatie/laravel-health": "^1.34", - "spatie/laravel-permission": "^6.16", + "spatie/laravel-permission": "^6.19", "spatie/laravel-query-builder": "^6.3", "spatie/temporary-directory": "^2.3", "symfony/http-client": "^7.2", @@ -47,7 +48,7 @@ "require-dev": { "barryvdh/laravel-ide-helper": "^3.5", "fakerphp/faker": "^1.23.1", - "larastan/larastan": "3.x-dev#5bd1c40edb43a727584081e74e9a1a2a201ea2ee", + "larastan/larastan": "^3.4", "laravel/pail": "^1.2.2", "laravel/pint": "^1.15.3", "laravel/sail": "^1.41", diff --git a/composer.lock b/composer.lock index 24bc79180..a1da10dd1 100644 --- a/composer.lock +++ b/composer.lock @@ -1674,16 +1674,16 @@ }, { "name": "dedoc/scramble", - "version": "v0.12.19", + "version": "v0.12.23", "source": { "type": "git", "url": "https://github.com/dedoc/scramble.git", - "reference": "f6516eb82c7c86e57e2cfaead4a05b900e4b5ecc" + "reference": "5b650167c81c59138e844c2ae550c14dc1a249d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dedoc/scramble/zipball/f6516eb82c7c86e57e2cfaead4a05b900e4b5ecc", - "reference": "f6516eb82c7c86e57e2cfaead4a05b900e4b5ecc", + "url": "https://api.github.com/repos/dedoc/scramble/zipball/5b650167c81c59138e844c2ae550c14dc1a249d0", + "reference": "5b650167c81c59138e844c2ae550c14dc1a249d0", "shasum": "" }, "require": { @@ -1695,11 +1695,15 @@ "spatie/laravel-package-tools": "^1.9.2" }, "require-dev": { + "larastan/larastan": "^3.3", "laravel/pint": "^v1.1.0", "nunomaduro/collision": "^7.0|^8.0", "orchestra/testbench": "^8.0|^9.0|^10.0", "pestphp/pest": "^2.34|^3.7", "pestphp/pest-plugin-laravel": "^2.3|^3.1", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", "phpunit/phpunit": "^10.5|^11.5.3", "spatie/laravel-permission": "^6.10", "spatie/pest-plugin-snapshots": "^2.1" @@ -1738,7 +1742,7 @@ ], "support": { "issues": "https://github.com/dedoc/scramble/issues", - "source": "https://github.com/dedoc/scramble/tree/v0.12.19" + "source": "https://github.com/dedoc/scramble/tree/v0.12.23" }, "funding": [ { @@ -1746,7 +1750,7 @@ "type": "github" } ], - "time": "2025-04-23T09:41:53+00:00" + "time": "2025-06-15T09:04:49+00:00" }, { "name": "dflydev/dot-access-data", @@ -4095,16 +4099,16 @@ }, { "name": "laravel/socialite", - "version": "v5.20.0", + "version": "v5.21.0", "source": { "type": "git", "url": "https://github.com/laravel/socialite.git", - "reference": "30972c12a41f71abeb418bc9ff157da8d9231519" + "reference": "d83639499ad14985c9a6a9713b70073300ce998d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/socialite/zipball/30972c12a41f71abeb418bc9ff157da8d9231519", - "reference": "30972c12a41f71abeb418bc9ff157da8d9231519", + "url": "https://api.github.com/repos/laravel/socialite/zipball/d83639499ad14985c9a6a9713b70073300ce998d", + "reference": "d83639499ad14985c9a6a9713b70073300ce998d", "shasum": "" }, "require": { @@ -4163,7 +4167,7 @@ "issues": "https://github.com/laravel/socialite/issues", "source": "https://github.com/laravel/socialite" }, - "time": "2025-04-21T14:21:34+00:00" + "time": "2025-05-19T12:56:37+00:00" }, { "name": "laravel/tinker", @@ -6101,16 +6105,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.4.0", + "version": "v5.5.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "447a020a1f875a434d62f2a401f53b82a396e494" + "reference": "ae59794362fe85e051a58ad36b289443f57be7a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", - "reference": "447a020a1f875a434d62f2a401f53b82a396e494", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/ae59794362fe85e051a58ad36b289443f57be7a9", + "reference": "ae59794362fe85e051a58ad36b289443f57be7a9", "shasum": "" }, "require": { @@ -6153,9 +6157,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.5.0" }, - "time": "2024-12-30T11:07:19+00:00" + "time": "2025-05-31T08:24:38+00:00" }, { "name": "nunomaduro/termwind", @@ -6510,19 +6514,20 @@ }, { "name": "phpdocumentor/reflection", - "version": "6.1.0", + "version": "6.3.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/Reflection.git", - "reference": "bb4dea805a645553d6d989b23dad9f8041f39502" + "reference": "d91b3270832785602adcc24ae2d0974ba99a8ff8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/Reflection/zipball/bb4dea805a645553d6d989b23dad9f8041f39502", - "reference": "bb4dea805a645553d6d989b23dad9f8041f39502", + "url": "https://api.github.com/repos/phpDocumentor/Reflection/zipball/d91b3270832785602adcc24ae2d0974ba99a8ff8", + "reference": "d91b3270832785602adcc24ae2d0974ba99a8ff8", "shasum": "" }, "require": { + "composer-runtime-api": "^2", "nikic/php-parser": "~4.18 || ^5.0", "php": "8.1.*|8.2.*|8.3.*|8.4.*", "phpdocumentor/reflection-common": "^2.1", @@ -6533,7 +6538,8 @@ }, "require-dev": { "dealerdirect/phpcodesniffer-composer-installer": "^1.0", - "doctrine/coding-standard": "^12.0", + "doctrine/coding-standard": "^13.0", + "eliashaeussler/phpunit-attributes": "^1.7", "mikey179/vfsstream": "~1.2", "mockery/mockery": "~1.6.0", "phpspec/prophecy-phpunit": "^2.0", @@ -6541,7 +6547,7 @@ "phpstan/phpstan": "^1.8", "phpstan/phpstan-webmozart-assert": "^1.2", "phpunit/phpunit": "^10.0", - "psalm/phar": "^5.24", + "psalm/phar": "^6.0", "rector/rector": "^1.0.0", "squizlabs/php_codesniffer": "^3.8" }, @@ -6553,6 +6559,9 @@ } }, "autoload": { + "files": [ + "src/php-parser/Modifiers.php" + ], "psr-4": { "phpDocumentor\\": "src/phpDocumentor" } @@ -6571,9 +6580,9 @@ ], "support": { "issues": "https://github.com/phpDocumentor/Reflection/issues", - "source": "https://github.com/phpDocumentor/Reflection/tree/6.1.0" + "source": "https://github.com/phpDocumentor/Reflection/tree/6.3.0" }, - "time": "2024-11-22T15:11:54+00:00" + "time": "2025-06-06T13:39:18+00:00" }, { "name": "phpdocumentor/reflection-common", @@ -6827,16 +6836,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "3.0.43", + "version": "3.0.44", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "709ec107af3cb2f385b9617be72af8cf62441d02" + "reference": "1d0b5e7e1434678411787c5a0535e68907cf82d9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/709ec107af3cb2f385b9617be72af8cf62441d02", - "reference": "709ec107af3cb2f385b9617be72af8cf62441d02", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/1d0b5e7e1434678411787c5a0535e68907cf82d9", + "reference": "1d0b5e7e1434678411787c5a0535e68907cf82d9", "shasum": "" }, "require": { @@ -6917,7 +6926,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.43" + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.44" }, "funding": [ { @@ -6933,7 +6942,7 @@ "type": "tidelift" } ], - "time": "2024-12-14T21:12:59+00:00" + "time": "2025-06-15T09:59:26+00:00" }, { "name": "phpstan/phpdoc-parser", @@ -8187,16 +8196,16 @@ }, { "name": "secondnetwork/blade-tabler-icons", - "version": "v3.31.0", + "version": "v3.34.0", "source": { "type": "git", "url": "https://github.com/secondnetwork/blade-tabler-icons.git", - "reference": "bf069bbdb2a63aa8eaeb10e6fd993464e9340a31" + "reference": "e48c0a5a53798d42c7beff760de8cbc7dbbccff4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/secondnetwork/blade-tabler-icons/zipball/bf069bbdb2a63aa8eaeb10e6fd993464e9340a31", - "reference": "bf069bbdb2a63aa8eaeb10e6fd993464e9340a31", + "url": "https://api.github.com/repos/secondnetwork/blade-tabler-icons/zipball/e48c0a5a53798d42c7beff760de8cbc7dbbccff4", + "reference": "e48c0a5a53798d42c7beff760de8cbc7dbbccff4", "shasum": "" }, "require": { @@ -8239,9 +8248,9 @@ ], "support": { "issues": "https://github.com/secondnetwork/blade-tabler-icons/issues", - "source": "https://github.com/secondnetwork/blade-tabler-icons/tree/v3.31.0" + "source": "https://github.com/secondnetwork/blade-tabler-icons/tree/v3.34.0" }, - "time": "2025-03-06T09:24:43+00:00" + "time": "2025-06-09T08:41:55+00:00" }, { "name": "socialiteproviders/authentik", @@ -8667,16 +8676,16 @@ }, { "name": "spatie/laravel-data", - "version": "4.15.1", + "version": "4.15.2", "source": { "type": "git", "url": "https://github.com/spatie/laravel-data.git", - "reference": "cb97afe6c0dadeb2e76ea1b7220cd04ed33dcca9" + "reference": "50f5abe716ff1ad9a3e96dcfdeb4ad00f014bf8d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-data/zipball/cb97afe6c0dadeb2e76ea1b7220cd04ed33dcca9", - "reference": "cb97afe6c0dadeb2e76ea1b7220cd04ed33dcca9", + "url": "https://api.github.com/repos/spatie/laravel-data/zipball/50f5abe716ff1ad9a3e96dcfdeb4ad00f014bf8d", + "reference": "50f5abe716ff1ad9a3e96dcfdeb4ad00f014bf8d", "shasum": "" }, "require": { @@ -8738,7 +8747,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-data/issues", - "source": "https://github.com/spatie/laravel-data/tree/4.15.1" + "source": "https://github.com/spatie/laravel-data/tree/4.15.2" }, "funding": [ { @@ -8746,7 +8755,7 @@ "type": "github" } ], - "time": "2025-04-10T06:06:27+00:00" + "time": "2025-06-12T09:42:08+00:00" }, { "name": "spatie/laravel-fractal", @@ -8831,16 +8840,16 @@ }, { "name": "spatie/laravel-health", - "version": "1.34.1", + "version": "1.34.3", "source": { "type": "git", "url": "https://github.com/spatie/laravel-health.git", - "reference": "283d7d5dffeeff6452c9a182f466c327fec6db3b" + "reference": "d421bc223c7a8c872ad944706d98a74b1056f761" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-health/zipball/283d7d5dffeeff6452c9a182f466c327fec6db3b", - "reference": "283d7d5dffeeff6452c9a182f466c327fec6db3b", + "url": "https://api.github.com/repos/spatie/laravel-health/zipball/d421bc223c7a8c872ad944706d98a74b1056f761", + "reference": "d421bc223c7a8c872ad944706d98a74b1056f761", "shasum": "" }, "require": { @@ -8912,7 +8921,7 @@ "spatie" ], "support": { - "source": "https://github.com/spatie/laravel-health/tree/1.34.1" + "source": "https://github.com/spatie/laravel-health/tree/1.34.3" }, "funding": [ { @@ -8920,7 +8929,7 @@ "type": "github" } ], - "time": "2025-04-17T06:34:01+00:00" + "time": "2025-06-04T22:04:19+00:00" }, { "name": "spatie/laravel-package-tools", @@ -8985,16 +8994,16 @@ }, { "name": "spatie/laravel-permission", - "version": "6.17.0", + "version": "6.20.0", "source": { "type": "git", "url": "https://github.com/spatie/laravel-permission.git", - "reference": "02ada8f638b643713fa2fb543384738e27346ddb" + "reference": "31c05679102c73f3b0d05790d2400182745a5615" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-permission/zipball/02ada8f638b643713fa2fb543384738e27346ddb", - "reference": "02ada8f638b643713fa2fb543384738e27346ddb", + "url": "https://api.github.com/repos/spatie/laravel-permission/zipball/31c05679102c73f3b0d05790d2400182745a5615", + "reference": "31c05679102c73f3b0d05790d2400182745a5615", "shasum": "" }, "require": { @@ -9056,7 +9065,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-permission/issues", - "source": "https://github.com/spatie/laravel-permission/tree/6.17.0" + "source": "https://github.com/spatie/laravel-permission/tree/6.20.0" }, "funding": [ { @@ -9064,7 +9073,7 @@ "type": "github" } ], - "time": "2025-04-08T15:06:14+00:00" + "time": "2025-06-05T07:33:07+00:00" }, { "name": "spatie/laravel-query-builder", @@ -10075,16 +10084,16 @@ }, { "name": "symfony/http-client", - "version": "v7.2.4", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "78981a2ffef6437ed92d4d7e2a86a82f256c6dc6" + "reference": "57e4fb86314015a695a750ace358d07a7e37b8a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/78981a2ffef6437ed92d4d7e2a86a82f256c6dc6", - "reference": "78981a2ffef6437ed92d4d7e2a86a82f256c6dc6", + "url": "https://api.github.com/repos/symfony/http-client/zipball/57e4fb86314015a695a750ace358d07a7e37b8a9", + "reference": "57e4fb86314015a695a750ace358d07a7e37b8a9", "shasum": "" }, "require": { @@ -10150,7 +10159,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.2.4" + "source": "https://github.com/symfony/http-client/tree/v7.3.0" }, "funding": [ { @@ -10166,7 +10175,7 @@ "type": "tidelift" } ], - "time": "2025-02-13T10:27:23+00:00" + "time": "2025-05-02T08:23:16+00:00" }, { "name": "symfony/http-client-contracts", @@ -10521,7 +10530,7 @@ }, { "name": "symfony/mailgun-mailer", - "version": "v7.2.0", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/mailgun-mailer.git", @@ -10570,7 +10579,7 @@ "description": "Symfony Mailgun Mailer Bridge", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailgun-mailer/tree/v7.2.0" + "source": "https://github.com/symfony/mailgun-mailer/tree/v7.3.0" }, "funding": [ { @@ -11311,16 +11320,16 @@ }, { "name": "symfony/postmark-mailer", - "version": "v7.2.4", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/postmark-mailer.git", - "reference": "d11c8ce0ff5974a2ee4a9a3297ba89ecdf6d1952" + "reference": "71ac001f8bc2ac36cc0bbea3fd6f4a4087d0cec0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/postmark-mailer/zipball/d11c8ce0ff5974a2ee4a9a3297ba89ecdf6d1952", - "reference": "d11c8ce0ff5974a2ee4a9a3297ba89ecdf6d1952", + "url": "https://api.github.com/repos/symfony/postmark-mailer/zipball/71ac001f8bc2ac36cc0bbea3fd6f4a4087d0cec0", + "reference": "71ac001f8bc2ac36cc0bbea3fd6f4a4087d0cec0", "shasum": "" }, "require": { @@ -11361,7 +11370,7 @@ "description": "Symfony Postmark Mailer Bridge", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/postmark-mailer/tree/v7.2.4" + "source": "https://github.com/symfony/postmark-mailer/tree/v7.3.0" }, "funding": [ { @@ -11377,7 +11386,7 @@ "type": "tidelift" } ], - "time": "2025-02-26T08:19:39+00:00" + "time": "2025-04-30T07:52:47+00:00" }, { "name": "symfony/process", @@ -12025,16 +12034,16 @@ }, { "name": "symfony/yaml", - "version": "v7.2.5", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "4c4b6f4cfcd7e52053f0c8bfad0f7f30fb924912" + "reference": "cea40a48279d58dc3efee8112634cb90141156c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/4c4b6f4cfcd7e52053f0c8bfad0f7f30fb924912", - "reference": "4c4b6f4cfcd7e52053f0c8bfad0f7f30fb924912", + "url": "https://api.github.com/repos/symfony/yaml/zipball/cea40a48279d58dc3efee8112634cb90141156c2", + "reference": "cea40a48279d58dc3efee8112634cb90141156c2", "shasum": "" }, "require": { @@ -12077,7 +12086,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.2.5" + "source": "https://github.com/symfony/yaml/tree/v7.3.0" }, "funding": [ { @@ -12093,7 +12102,7 @@ "type": "tidelift" } ], - "time": "2025-03-03T07:12:39+00:00" + "time": "2025-04-04T10:10:33+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -12954,16 +12963,16 @@ }, { "name": "filp/whoops", - "version": "2.18.0", + "version": "2.18.3", "source": { "type": "git", "url": "https://github.com/filp/whoops.git", - "reference": "a7de6c3c6c3c022f5cfc337f8ede6a14460cf77e" + "reference": "59a123a3d459c5a23055802237cb317f609867e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filp/whoops/zipball/a7de6c3c6c3c022f5cfc337f8ede6a14460cf77e", - "reference": "a7de6c3c6c3c022f5cfc337f8ede6a14460cf77e", + "url": "https://api.github.com/repos/filp/whoops/zipball/59a123a3d459c5a23055802237cb317f609867e5", + "reference": "59a123a3d459c5a23055802237cb317f609867e5", "shasum": "" }, "require": { @@ -13013,7 +13022,7 @@ ], "support": { "issues": "https://github.com/filp/whoops/issues", - "source": "https://github.com/filp/whoops/tree/2.18.0" + "source": "https://github.com/filp/whoops/tree/2.18.3" }, "funding": [ { @@ -13021,7 +13030,7 @@ "type": "github" } ], - "time": "2025-03-15T12:00:00+00:00" + "time": "2025-06-16T00:02:10+00:00" }, { "name": "hamcrest/hamcrest-php", @@ -13177,16 +13186,16 @@ }, { "name": "larastan/larastan", - "version": "3.x-dev", + "version": "v3.4.2", "source": { "type": "git", "url": "https://github.com/larastan/larastan.git", - "reference": "5bd1c40edb43a727584081e74e9a1a2a201ea2ee" + "reference": "36706736a0c51d3337478fab9c919d78d2e03404" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/larastan/larastan/zipball/5bd1c40edb43a727584081e74e9a1a2a201ea2ee", - "reference": "5bd1c40edb43a727584081e74e9a1a2a201ea2ee", + "url": "https://api.github.com/repos/larastan/larastan/zipball/36706736a0c51d3337478fab9c919d78d2e03404", + "reference": "36706736a0c51d3337478fab9c919d78d2e03404", "shasum": "" }, "require": { @@ -13215,7 +13224,6 @@ "suggest": { "orchestra/testbench": "Using Larastan for analysing a package needs Testbench" }, - "default-branch": true, "type": "phpstan-extension", "extra": { "phpstan": { @@ -13255,7 +13263,7 @@ ], "support": { "issues": "https://github.com/larastan/larastan/issues", - "source": "https://github.com/larastan/larastan/tree/3.x" + "source": "https://github.com/larastan/larastan/tree/v3.4.2" }, "funding": [ { @@ -13263,20 +13271,20 @@ "type": "github" } ], - "time": "2025-04-24T07:26:41+00:00" + "time": "2025-06-10T09:34:58+00:00" }, { "name": "laravel/pail", - "version": "v1.2.2", + "version": "v1.2.3", "source": { "type": "git", "url": "https://github.com/laravel/pail.git", - "reference": "f31f4980f52be17c4667f3eafe034e6826787db2" + "reference": "8cc3d575c1f0e57eeb923f366a37528c50d2385a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pail/zipball/f31f4980f52be17c4667f3eafe034e6826787db2", - "reference": "f31f4980f52be17c4667f3eafe034e6826787db2", + "url": "https://api.github.com/repos/laravel/pail/zipball/8cc3d575c1f0e57eeb923f366a37528c50d2385a", + "reference": "8cc3d575c1f0e57eeb923f366a37528c50d2385a", "shasum": "" }, "require": { @@ -13296,7 +13304,7 @@ "orchestra/testbench-core": "^8.13|^9.0|^10.0", "pestphp/pest": "^2.20|^3.0", "pestphp/pest-plugin-type-coverage": "^2.3|^3.0", - "phpstan/phpstan": "^1.10", + "phpstan/phpstan": "^1.12.27", "symfony/var-dumper": "^6.3|^7.0" }, "type": "library", @@ -13332,6 +13340,7 @@ "description": "Easily delve into your Laravel application's log files directly from the command line.", "homepage": "https://github.com/laravel/pail", "keywords": [ + "dev", "laravel", "logs", "php", @@ -13341,20 +13350,20 @@ "issues": "https://github.com/laravel/pail/issues", "source": "https://github.com/laravel/pail" }, - "time": "2025-01-28T15:15:15+00:00" + "time": "2025-06-05T13:55:57+00:00" }, { "name": "laravel/pint", - "version": "v1.22.0", + "version": "v1.22.1", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "7ddfaa6523a675fae5c4123ee38fc6bfb8ee4f36" + "reference": "941d1927c5ca420c22710e98420287169c7bcaf7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/7ddfaa6523a675fae5c4123ee38fc6bfb8ee4f36", - "reference": "7ddfaa6523a675fae5c4123ee38fc6bfb8ee4f36", + "url": "https://api.github.com/repos/laravel/pint/zipball/941d1927c5ca420c22710e98420287169c7bcaf7", + "reference": "941d1927c5ca420c22710e98420287169c7bcaf7", "shasum": "" }, "require": { @@ -13366,11 +13375,11 @@ }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.75.0", - "illuminate/view": "^11.44.2", - "larastan/larastan": "^3.3.1", + "illuminate/view": "^11.44.7", + "larastan/larastan": "^3.4.0", "laravel-zero/framework": "^11.36.1", "mockery/mockery": "^1.6.12", - "nunomaduro/termwind": "^2.3", + "nunomaduro/termwind": "^2.3.1", "pestphp/pest": "^2.36.0" }, "bin": [ @@ -13407,7 +13416,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2025-04-08T22:11:45+00:00" + "time": "2025-05-08T08:38:12+00:00" }, { "name": "laravel/sail", @@ -13557,23 +13566,23 @@ }, { "name": "nunomaduro/collision", - "version": "v8.8.0", + "version": "v8.8.1", "source": { "type": "git", "url": "https://github.com/nunomaduro/collision.git", - "reference": "4cf9f3b47afff38b139fb79ce54fc71799022ce8" + "reference": "44ccb82e3e21efb5446748d2a3c81a030ac22bd5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/collision/zipball/4cf9f3b47afff38b139fb79ce54fc71799022ce8", - "reference": "4cf9f3b47afff38b139fb79ce54fc71799022ce8", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/44ccb82e3e21efb5446748d2a3c81a030ac22bd5", + "reference": "44ccb82e3e21efb5446748d2a3c81a030ac22bd5", "shasum": "" }, "require": { - "filp/whoops": "^2.18.0", - "nunomaduro/termwind": "^2.3.0", + "filp/whoops": "^2.18.1", + "nunomaduro/termwind": "^2.3.1", "php": "^8.2.0", - "symfony/console": "^7.2.5" + "symfony/console": "^7.3.0" }, "conflict": { "laravel/framework": "<11.44.2 || >=13.0.0", @@ -13581,15 +13590,15 @@ }, "require-dev": { "brianium/paratest": "^7.8.3", - "larastan/larastan": "^3.2", - "laravel/framework": "^11.44.2 || ^12.6", - "laravel/pint": "^1.21.2", - "laravel/sail": "^1.41.0", - "laravel/sanctum": "^4.0.8", + "larastan/larastan": "^3.4.2", + "laravel/framework": "^11.44.2 || ^12.18", + "laravel/pint": "^1.22.1", + "laravel/sail": "^1.43.1", + "laravel/sanctum": "^4.1.1", "laravel/tinker": "^2.10.1", - "orchestra/testbench-core": "^9.12.0 || ^10.1", - "pestphp/pest": "^3.8.0", - "sebastian/environment": "^7.2.0 || ^8.0" + "orchestra/testbench-core": "^9.12.0 || ^10.4", + "pestphp/pest": "^3.8.2", + "sebastian/environment": "^7.2.1 || ^8.0" }, "type": "library", "extra": { @@ -13652,7 +13661,7 @@ "type": "patreon" } ], - "time": "2025-04-03T14:33:09+00:00" + "time": "2025-06-11T01:04:21+00:00" }, { "name": "pestphp/pest", @@ -14287,16 +14296,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "11.0.9", + "version": "11.0.10", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "14d63fbcca18457e49c6f8bebaa91a87e8e188d7" + "reference": "1a800a7446add2d79cc6b3c01c45381810367d76" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/14d63fbcca18457e49c6f8bebaa91a87e8e188d7", - "reference": "14d63fbcca18457e49c6f8bebaa91a87e8e188d7", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/1a800a7446add2d79cc6b3c01c45381810367d76", + "reference": "1a800a7446add2d79cc6b3c01c45381810367d76", "shasum": "" }, "require": { @@ -14353,15 +14362,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.9" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/show" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" } ], - "time": "2025-02-25T13:26:39+00:00" + "time": "2025-06-18T08:56:18+00:00" }, { "name": "phpunit/php-file-iterator", @@ -15086,23 +15107,23 @@ }, { "name": "sebastian/environment", - "version": "7.2.0", + "version": "7.2.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5" + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5", - "reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4", "shasum": "" }, "require": { "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^11.3" }, "suggest": { "ext-posix": "*" @@ -15138,15 +15159,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", "security": "https://github.com/sebastianbergmann/environment/security/policy", - "source": "https://github.com/sebastianbergmann/environment/tree/7.2.0" + "source": "https://github.com/sebastianbergmann/environment/tree/7.2.1" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" } ], - "time": "2024-07-03T04:54:44+00:00" + "time": "2025-05-21T11:55:47+00:00" }, { "name": "sebastian/exporter", diff --git a/database/Factories/NodeFactory.php b/database/Factories/NodeFactory.php index 030bd3e38..295c312ae 100644 --- a/database/Factories/NodeFactory.php +++ b/database/Factories/NodeFactory.php @@ -38,6 +38,7 @@ class NodeFactory extends Factory 'daemon_token_id' => Str::random(Node::DAEMON_TOKEN_ID_LENGTH), 'daemon_token' => Str::random(Node::DAEMON_TOKEN_LENGTH), 'daemon_listen' => 8080, + 'daemon_connect' => 8080, 'daemon_sftp' => 2022, 'daemon_base' => '/var/lib/panel/volumes', 'maintenance_mode' => false, diff --git a/database/Seeders/eggs/rust/egg-rust.json b/database/Seeders/eggs/rust/egg-rust.json index b38868888..ebe84d132 100644 --- a/database/Seeders/eggs/rust/egg-rust.json +++ b/database/Seeders/eggs/rust/egg-rust.json @@ -4,7 +4,7 @@ "version": "PLCN_v1", "update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/rust\/egg-rust.json" }, - "exported_at": "2025-03-18T12:36:48+00:00", + "exported_at": "2025-06-06T11:57:17+00:00", "name": "Rust", "author": "panel@example.com", "uuid": "bace2dfb-209c-452a-9459-7d6f340b07ae", @@ -20,7 +20,7 @@ "ghcr.io\/parkervcp\/games:rust": "ghcr.io\/parkervcp\/games:rust" }, "file_denylist": [], - "startup": ".\/RustDedicated -batchmode +server.port {{SERVER_PORT}} +server.queryport {{QUERY_PORT}} +server.identity \"rust\" +rcon.port {{RCON_PORT}} +rcon.web true +server.hostname \\\"{{HOSTNAME}}\\\" +server.level \\\"{{LEVEL}}\\\" +server.description \\\"{{DESCRIPTION}}\\\" +server.url \\\"{{SERVER_URL}}\\\" +server.headerimage \\\"{{SERVER_IMG}}\\\" +server.logoimage \\\"{{SERVER_LOGO}}\\\" +server.maxplayers {{MAX_PLAYERS}} +rcon.password \\\"{{RCON_PASS}}\\\" +server.saveinterval {{SAVEINTERVAL}} +app.port {{APP_PORT}} $( [ -z ${MAP_URL} ] && printf %s \"+server.worldsize \\\"{{WORLD_SIZE}}\\\" +server.seed \\\"{{WORLD_SEED}}\\\"\" || printf %s \"+server.levelurl {{MAP_URL}}\" ) {{ADDITIONAL_ARGS}}", + "startup": ".\/RustDedicated -batchmode +server.port {{SERVER_PORT}} +server.queryport {{QUERY_PORT}} +server.identity \"rust\" +rcon.port {{RCON_PORT}} +rcon.web true +server.hostname \\\"{{SERVER_HOSTNAME}}\\\" +server.level \\\"{{LEVEL}}\\\" +server.description \\\"{{DESCRIPTION}}\\\" +server.url \\\"{{SERVER_URL}}\\\" +server.headerimage \\\"{{SERVER_IMG}}\\\" +server.logoimage \\\"{{SERVER_LOGO}}\\\" +server.maxplayers {{MAX_PLAYERS}} +rcon.password \\\"{{RCON_PASS}}\\\" +server.saveinterval {{SAVEINTERVAL}} +app.port {{APP_PORT}} $( [ -z ${MAP_URL} ] && printf %s \"+server.worldsize \\\"{{WORLD_SIZE}}\\\" +server.seed \\\"{{WORLD_SEED}}\\\"\" || printf %s \"+server.levelurl {{MAP_URL}}\" ) {{ADDITIONAL_ARGS}}", "config": { "files": "{}", "startup": "{\r\n \"done\": \"Server startup complete\"\r\n}", @@ -38,7 +38,7 @@ { "name": "Server Name", "description": "The name of your server in the public server list.", - "env_variable": "HOSTNAME", + "env_variable": "SERVER_HOSTNAME", "default_value": "A Rust Server", "user_viewable": true, "user_editable": true, diff --git a/database/Seeders/eggs/source-engine/egg-counter--strike--global-offensive.json b/database/Seeders/eggs/source-engine/egg-counter--strike--global-offensive.json deleted file mode 100644 index f8c0fd0ad..000000000 --- a/database/Seeders/eggs/source-engine/egg-counter--strike--global-offensive.json +++ /dev/null @@ -1,83 +0,0 @@ -{ - "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL", - "meta": { - "version": "PLCN_v1", - "update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/source-engine\/egg-counter--strike--global-offensive.json" - }, - "exported_at": "2025-03-18T12:36:01+00:00", - "name": "Counter-Strike: Global Offensive", - "author": "panel@example.com", - "uuid": "437c367d-06be-498f-a604-fdad135504d7", - "description": "Counter-Strike: Global Offensive is a multiplayer first-person shooter video game developed by Hidden Path Entertainment and Valve Corporation.", - "tags": [ - "source", - "steamcmd" - ], - "features": [ - "gsl_token", - "steam_disk_space" - ], - "docker_images": { - "ghcr.io\/parkervcp\/games:source": "ghcr.io\/parkervcp\/games:source" - }, - "file_denylist": [], - "startup": ".\/srcds_run -game csgo -console -port {{SERVER_PORT}} +ip 0.0.0.0 +map {{SRCDS_MAP}} -strictportbind -norestart +sv_setsteamaccount {{STEAM_ACC}}", - "config": { - "files": "{}", - "startup": "{\r\n \"done\": \"Connection to Steam servers successful\"\r\n}", - "logs": "{}", - "stop": "quit" - }, - "scripts": { - "installation": { - "script": "#!\/bin\/bash\r\n# steamcmd Base Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\n\r\n## just in case someone removed the defaults.\r\nif [ \"${STEAM_USER}\" == \"\" ]; then\r\n STEAM_USER=anonymous\r\n STEAM_PASS=\"\"\r\n STEAM_AUTH=\"\"\r\nfi\r\n\r\n## download and install steamcmd\r\ncd \/tmp\r\nmkdir -p \/mnt\/server\/steamcmd\r\ncurl -sSL -o steamcmd.tar.gz https:\/\/steamcdn-a.akamaihd.net\/client\/installer\/steamcmd_linux.tar.gz\r\ntar -xzvf steamcmd.tar.gz -C \/mnt\/server\/steamcmd\r\nmkdir -p \/mnt\/server\/steamapps # Fix steamcmd disk write error when this folder is missing\r\ncd \/mnt\/server\/steamcmd\r\n\r\n# SteamCMD fails otherwise for some reason, even running as root.\r\n# This is changed at the end of the install process anyways.\r\nchown -R root:root \/mnt\r\nexport HOME=\/mnt\/server\r\n\r\n## install game using steamcmd\r\n.\/steamcmd.sh +force_install_dir \/mnt\/server +login ${STEAM_USER} ${STEAM_PASS} ${STEAM_AUTH} +app_update ${SRCDS_APPID} ${EXTRA_FLAGS} +quit ## other flags may be needed depending on install. looking at you cs 1.6\r\n\r\n## set up 32 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk32\r\ncp -v linux32\/steamclient.so ..\/.steam\/sdk32\/steamclient.so\r\n\r\n## set up 64 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk64\r\ncp -v linux64\/steamclient.so ..\/.steam\/sdk64\/steamclient.so", - "container": "ghcr.io\/parkervcp\/installers:debian", - "entrypoint": "bash" - } - }, - "variables": [ - { - "name": "Map", - "description": "The default map for the server.", - "env_variable": "SRCDS_MAP", - "default_value": "de_dust2", - "user_viewable": true, - "user_editable": true, - "rules": [ - "required", - "string", - "alpha_dash" - ], - "sort": 1 - }, - { - "name": "Steam Account Token", - "description": "The Steam Account Token required for the server to be displayed publicly.", - "env_variable": "STEAM_ACC", - "default_value": "", - "user_viewable": true, - "user_editable": true, - "rules": [ - "required", - "string", - "alpha_num", - "size:32" - ], - "sort": 2 - }, - { - "name": "Source AppID", - "description": "Required for game to update on server restart. Do not modify this.", - "env_variable": "SRCDS_APPID", - "default_value": "740", - "user_viewable": false, - "user_editable": false, - "rules": [ - "required", - "string", - "max:20" - ], - "sort": 3 - } - ] -} \ No newline at end of file diff --git a/database/migrations/2025_04_08_113556_create_node_role_table.php b/database/migrations/2025_04_08_113556_create_node_role_table.php new file mode 100644 index 000000000..42f51c73a --- /dev/null +++ b/database/migrations/2025_04_08_113556_create_node_role_table.php @@ -0,0 +1,32 @@ +unsignedInteger('node_id'); + $table->unsignedBigInteger('role_id'); + + $table->unique(['node_id', 'role_id']); + + $table->foreign('node_id')->references('id')->on('nodes')->cascadeOnDelete(); + $table->foreign('role_id')->references('id')->on('roles')->cascadeOnDelete(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('node_role'); + } +}; diff --git a/database/migrations/2025_05_01_193002_move_to_mountables.php b/database/migrations/2025_05_01_193002_move_to_mountables.php new file mode 100644 index 000000000..36bf52bfb --- /dev/null +++ b/database/migrations/2025_05_01_193002_move_to_mountables.php @@ -0,0 +1,93 @@ +unsignedInteger('mount_id'); + + $table->string('mountable_type'); + $table->unsignedBigInteger('mountable_id'); + $table->index(['mountable_id', 'mountable_type'], 'mountables_mountable_id_mountable_type_index'); + + $table->foreign('mount_id') + ->references('id') // mount id + ->on('mounts') + ->onDelete('cascade'); + + $table->primary(['mount_id', 'mountable_id', 'mountable_type'], + 'mountables_mountable_type_primary'); + }); + + Schema::table('mount_node', function (Blueprint $table) { + $table->dropForeign(['node_id']); + $table->dropForeign(['mount_id']); + $table->dropUnique(['node_id', 'mount_id']); + }); + + $inserts = []; + $nodeMounts = DB::table('mount_node')->get(); + $nodeMounts->each(function ($mount) use (&$inserts) { + $inserts[] = [ + 'mount_id' => $mount->mount_id, + 'mountable_type' => 'node', + 'mountable_id' => $mount->node_id, + ]; + }); + + Schema::table('mount_server', function (Blueprint $table) { + $table->dropForeign(['server_id']); + $table->dropForeign(['mount_id']); + $table->dropUnique(['server_id', 'mount_id']); + }); + + $serverMounts = DB::table('mount_server')->get(); + $serverMounts->each(function ($mount) use (&$inserts) { + $inserts[] = [ + 'mount_id' => $mount->mount_id, + 'mountable_type' => 'server', + 'mountable_id' => $mount->server_id, + ]; + }); + + Schema::table('egg_mount', function (Blueprint $table) { + $table->dropForeign(['egg_id']); + $table->dropForeign(['mount_id']); + $table->dropUnique(['egg_id', 'mount_id']); + }); + + $eggMounts = DB::table('egg_mount')->get(); + $eggMounts->each(function ($mount) use (&$inserts) { + $inserts[] = [ + 'mount_id' => $mount->mount_id, + 'mountable_type' => 'egg', + 'mountable_id' => $mount->egg_id, + ]; + }); + + DB::transaction(function () use ($inserts) { + DB::table('mountables')->insert($inserts); + }); + + Schema::drop('mount_node'); + Schema::drop('mount_server'); + Schema::drop('egg_mount'); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // Not needed + } +}; diff --git a/database/migrations/2025_06_02_073349_add_daemon_connect_to_nodes.php b/database/migrations/2025_06_02_073349_add_daemon_connect_to_nodes.php new file mode 100644 index 000000000..90f01b0bc --- /dev/null +++ b/database/migrations/2025_06_02_073349_add_daemon_connect_to_nodes.php @@ -0,0 +1,28 @@ +smallInteger('daemon_connect')->unsigned()->default(8080); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('nodes', function (Blueprint $table) { + $table->dropColumn('daemon_connect'); + }); + } +}; diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 0dd8d2e29..1089ea539 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -23,7 +23,7 @@ else echo -e "APP_INSTALLED=false" >> /pelican-data/.env fi -mkdir -p /pelican-data/database /pelican-data/storage/avatars /var/www/html/storage/logs/supervisord 2>/dev/null +mkdir -p /pelican-data/database /pelican-data/storage/avatars /pelican-data/storage/fonts /var/www/html/storage/logs/supervisord 2>/dev/null if ! grep -q "APP_KEY=" .env || grep -q "APP_KEY=$" .env; then echo "Generating APP_KEY..." diff --git a/lang/en/admin/node.php b/lang/en/admin/node.php index 582d3a808..6aed06229 100644 --- a/lang/en/admin/node.php +++ b/lang/en/admin/node.php @@ -45,6 +45,10 @@ return [ 'port' => 'Port', 'ports' => 'Ports', 'port_help' => 'If you are running the daemon behind Cloudflare you should set the daemon port to 8443 to allow websocket proxying over SSL.', + 'connect_port' => 'Connection Port', + 'connect_port_help' => 'Connections to wings will use this port. If you are using a reverse proxy this can differ from the listen port. When using Cloudflare proxy you should use 8443.', + 'listen_port' => 'Listening Port', + 'listen_port_help' => 'Wings will listen on this port.', 'display_name' => 'Display Name', 'ssl' => 'Communicate over SSL', 'panel_on_ssl' => 'Your Panel is using a secure SSL connection,
so your Daemon must too.', diff --git a/lang/en/admin/role.php b/lang/en/admin/role.php index 1b99c0625..c4871ac7a 100644 --- a/lang/en/admin/role.php +++ b/lang/en/admin/role.php @@ -12,4 +12,6 @@ return [ 'root_admin' => 'The :role has all permissions.', 'root_admin_delete' => 'Can\'t delete Root Admin', 'users' => 'Users', + 'nodes' => 'Nodes', + 'nodes_hint' => 'Leave empty to allow access to all nodes.', ]; diff --git a/lang/en/admin/server.php b/lang/en/admin/server.php index 97340b67a..4b64320f9 100644 --- a/lang/en/admin/server.php +++ b/lang/en/admin/server.php @@ -64,6 +64,7 @@ return [ 'reinstall_modal_heading' => 'Are you sure you want to reinstall this server?', 'reinstall_modal_description' => '!! This can result in unrecoverable data loss !!', 'server_status' => 'Server Status', + 'view_install_log' => 'View install log', 'uuid' => 'UUID', 'node' => 'Node', 'short_uuid' => 'Short UUID', @@ -85,7 +86,7 @@ return [ 'allocations' => 'Allocations', 'databases' => 'Databases', 'no_databases' => 'No Databases exist for this Server', - 'delete_db' => 'Are you sure you want to delete', + 'delete_db' => 'Are you sure you want to delete :name ?', 'delete_db_heading' => 'Delete Database?', 'backups' => 'Backups', 'egg' => 'Egg', @@ -100,6 +101,7 @@ return [ 'create_allocation' => 'Create Allocation', 'add_allocation' => 'Add Allocation', 'view' => 'View', + 'no_log' => 'No Log Available', 'tabs' => [ 'information' => 'Information', 'egg_configuration' => 'Egg Configuration', @@ -129,5 +131,6 @@ return [ 'install_toggle_failed' => 'Could not toggle install status', 'reinstall_started' => 'Reinstall started', 'reinstall_failed' => 'Could not start reinstall', + 'log_failed' => 'Could not connect to Wings to retrieve server install log.', ], ]; diff --git a/lang/en/admin/setting.php b/lang/en/admin/setting.php index 969dab597..9f0ee01be 100644 --- a/lang/en/admin/setting.php +++ b/lang/en/admin/setting.php @@ -137,6 +137,8 @@ return [ 'title' => 'Servers', 'helper' => 'Settings for Servers', 'edit_server_desc' => 'Allow Users to edit Descriptions?', + 'console_font_upload' => 'Console Font Upload', + 'console_font_hint' => 'Only *.ttf fonts are supported. Mono fonts strongly recommended!', ], 'webhook' => [ 'title' => 'Webhooks', diff --git a/lang/en/auth.php b/lang/en/auth.php index b995376f7..d88b09397 100644 --- a/lang/en/auth.php +++ b/lang/en/auth.php @@ -16,6 +16,7 @@ return [ 'failed' => 'These credentials do not match our records.', 'failed-two-factor' => 'Incorrect 2FA Code', 'two-factor-code' => 'Two Factor Code', + 'two-factor-hint' => 'You may use backup codes if you lost access to your device.', 'password' => 'The provided password is incorrect.', 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', '2fa_must_be_enabled' => 'The administrator has required that 2-Factor Authentication must be enabled for your account in order to use the Panel.', diff --git a/lang/en/commands.php b/lang/en/commands.php index b6fa6c9ca..659a7aa9b 100644 --- a/lang/en/commands.php +++ b/lang/en/commands.php @@ -36,6 +36,7 @@ return [ 'cpu_overallocate' => 'Enter the amount of cpu to over allocate by, -1 will disable checking and 0 will prevent creating new server', 'upload_size' => "'Enter the maximum filesize upload", 'daemonListen' => 'Enter the daemon listening port', + 'daemonConnect' => 'Enter the daemon connecting port (can be same as listen port)', 'daemonSFTP' => 'Enter the daemon SFTP listening port', 'daemonSFTPAlias' => 'Enter the daemon SFTP alias (can be empty)', 'daemonBase' => 'Enter the base folder', diff --git a/lang/en/exceptions.php b/lang/en/exceptions.php index 711ce7ac9..ed17349d2 100644 --- a/lang/en/exceptions.php +++ b/lang/en/exceptions.php @@ -4,6 +4,7 @@ return [ 'daemon_connection_failed' => 'There was an exception while attempting to communicate with the daemon resulting in a HTTP/:code response code. This exception has been logged.', 'node' => [ 'servers_attached' => 'A node must have no servers linked to it in order to be deleted.', + 'error_connecting' => 'Error connecting to :node', 'daemon_off_config_updated' => 'The daemon configuration has been updated, however there was an error encountered while attempting to automatically update the configuration file on the Daemon. You will need to manually update the configuration file (config.yml) for the daemon to apply these changes.', ], 'allocations' => [ diff --git a/lang/en/profile.php b/lang/en/profile.php index e6354118c..525f6f235 100644 --- a/lang/en/profile.php +++ b/lang/en/profile.php @@ -47,4 +47,8 @@ return [ 'rows' => 'Rows', 'font_size' => 'Font Size', 'font' => 'Font', + 'font_preview' => 'Font Preview', + 'seconds' => 'Seconds', + 'graph_period' => 'Graph Period', + 'graph_period_helper' => 'The amount of data points, seconds, shown on the console graphs.', ]; diff --git a/lang/en/server/users.php b/lang/en/server/users.php index 77d90b370..99486bd48 100644 --- a/lang/en/server/users.php +++ b/lang/en/server/users.php @@ -15,8 +15,9 @@ return [ 'startup_read' => 'Allows a user to view the startup variables for a server.', 'startup_update' => 'Allows a user to modify the startup variables for the server.', 'startup_docker_image' => 'Allows a user to modify the Docker image used when running the server.', - 'setting_reinstall' => 'Allows a user to trigger a reinstall of this server.', - 'setting_rename' => 'Allows a user to rename this server and change the description of it.', + 'settings_reinstall' => 'Allows a user to trigger a reinstall of this server.', + 'settings_rename' => 'Allows a user to rename this server.', + 'settings_description' => 'Allows a user to change the description of this server.', 'activity_read' => 'Allows a user to view the activity logs for the server.', 'websocket_*' => 'Allows a user access to the websocket for this server.', 'control_console' => 'Allows a user to send data to the server console.', diff --git a/public/css/filament/server/console.css b/public/css/filament/server/console.css index a95de40e5..51c2d8957 100644 --- a/public/css/filament/server/console.css +++ b/public/css/filament/server/console.css @@ -48,15 +48,3 @@ ::-webkit-scrollbar-corner { background: transparent; } - -@font-face { - font-family: Comic Mono; - font-weight: normal; - src: url('/fonts/ComicMono.ttf'); -} - -@font-face { - font-family: Comic Mono; - font-weight: bold; - src: url('/fonts/ComicMono-bold.ttf'); -} diff --git a/public/fonts/ComicMono-Bold.ttf b/public/fonts/ComicMono-Bold.ttf deleted file mode 100644 index e03f41e73..000000000 Binary files a/public/fonts/ComicMono-Bold.ttf and /dev/null differ diff --git a/public/fonts/ComicMono.ttf b/public/fonts/ComicMono.ttf deleted file mode 100644 index 9bc7354e3..000000000 Binary files a/public/fonts/ComicMono.ttf and /dev/null differ diff --git a/resources/views/filament/admin/widgets/canary-widget.blade.php b/resources/views/filament/admin/widgets/canary-widget.blade.php deleted file mode 100644 index 2e6a4aded..000000000 --- a/resources/views/filament/admin/widgets/canary-widget.blade.php +++ /dev/null @@ -1,19 +0,0 @@ - - - {{ trans('admin/dashboard.sections.intro-developers.heading') }} - -

{{ trans('admin/dashboard.sections.intro-developers.content') }}

- -


- -

{{ trans('admin/dashboard.sections.intro-developers.extra_note') }}

-
-
diff --git a/resources/views/filament/admin/widgets/form-widget.blade.php b/resources/views/filament/admin/widgets/form-widget.blade.php new file mode 100644 index 000000000..642f88749 --- /dev/null +++ b/resources/views/filament/admin/widgets/form-widget.blade.php @@ -0,0 +1,3 @@ + + {{ $this->form }} + diff --git a/resources/views/filament/admin/widgets/help-widget.blade.php b/resources/views/filament/admin/widgets/help-widget.blade.php deleted file mode 100644 index ee3d94394..000000000 --- a/resources/views/filament/admin/widgets/help-widget.blade.php +++ /dev/null @@ -1,14 +0,0 @@ - - - {{ trans('admin/dashboard.sections.intro-help.heading') }} - -

{{ trans('admin/dashboard.sections.intro-help.content') }}

-
-
diff --git a/resources/views/filament/admin/widgets/no-nodes-widget.blade.php b/resources/views/filament/admin/widgets/no-nodes-widget.blade.php deleted file mode 100644 index 79f083286..000000000 --- a/resources/views/filament/admin/widgets/no-nodes-widget.blade.php +++ /dev/null @@ -1,14 +0,0 @@ - - - {{ trans('admin/dashboard.sections.intro-first-node.heading') }} - -

{{ trans('admin/dashboard.sections.intro-first-node.content') }}

-
-
diff --git a/resources/views/filament/admin/widgets/support-widget.blade.php b/resources/views/filament/admin/widgets/support-widget.blade.php deleted file mode 100644 index 2bf7f2ac6..000000000 --- a/resources/views/filament/admin/widgets/support-widget.blade.php +++ /dev/null @@ -1,18 +0,0 @@ - - - {{ trans('admin/dashboard.sections.intro-support.heading') }} - -

{{ trans('admin/dashboard.sections.intro-support.content') }}

- -


- -

{{ trans('admin/dashboard.sections.intro-support.extra_note') }}

-
-
diff --git a/resources/views/filament/admin/widgets/update-widget.blade.php b/resources/views/filament/admin/widgets/update-widget.blade.php deleted file mode 100644 index 122a25f82..000000000 --- a/resources/views/filament/admin/widgets/update-widget.blade.php +++ /dev/null @@ -1,25 +0,0 @@ - - @if (!$isLatest) - - {{ trans('admin/dashboard.sections.intro-update-available.heading') }} - -

{{ trans('admin/dashboard.sections.intro-update-available.content', ['latestVersion' => $latestVersion]) }}

- -
- @else - - {{ trans('admin/dashboard.sections.intro-no-update.heading') }} - -

{{ trans('admin/dashboard.sections.intro-no-update.content', ['version' => $version]) }}

-
- @endif -
diff --git a/resources/views/filament/components/server-console.blade.php b/resources/views/filament/components/server-console.blade.php index 4ce34a40d..b7ddf0537 100644 --- a/resources/views/filament/components/server-console.blade.php +++ b/resources/views/filament/components/server-console.blade.php @@ -1,11 +1,25 @@ @assets - + @php + $userFont = auth()->user()->getCustomization()['console_font'] ?? 'monospace'; + $userFontSize = auth()->user()->getCustomization()['console_font_size'] ?? 14; + $userRows = auth()->user()->getCustomization()['console_rows'] ?? 30; + @endphp + @if($userFont !== "monospace") + + + @endif + @endassets @@ -57,14 +71,14 @@ }; let options = { - fontSize: {{ auth()->user()->getCustomization()['console_font_size'] ?? 14 }}, - fontFamily: 'Comic Mono, monospace', + fontSize: {{ $userFontSize }}, + fontFamily: '{{ $userFont }}, monospace', lineHeight: 1.2, disableStdin: true, cursorStyle: 'underline', cursorInactiveStyle: 'underline', allowTransparency: true, - rows: {{ auth()->user()->getCustomization()['console_rows'] ?? 30 }}, + rows: {{ $userRows }}, theme: theme }; diff --git a/resources/views/filament/components/server-data-block.blade.php b/resources/views/filament/components/server-data-block.blade.php deleted file mode 100644 index f640d582d..000000000 --- a/resources/views/filament/components/server-data-block.blade.php +++ /dev/null @@ -1,10 +0,0 @@ -
-
- - {{ $getLabel() }} - -
- {{ $getValue() }} -
-
-
diff --git a/resources/views/filament/components/server-small-data-block.blade.php b/resources/views/filament/components/server-small-data-block.blade.php index ff9770fa5..3be63ee71 100644 --- a/resources/views/filament/components/server-small-data-block.blade.php +++ b/resources/views/filament/components/server-small-data-block.blade.php @@ -1,9 +1,14 @@ -
+
+@if ($shouldCopyOnClick()) + +@else +@endif {{ $getLabel() }} - {{ $getValue() }} + + {{ $getValue() }} +
diff --git a/resources/views/filament/plugins/monaco-editor-logs.blade.php b/resources/views/filament/plugins/monaco-editor-logs.blade.php new file mode 100644 index 000000000..cb5766359 --- /dev/null +++ b/resources/views/filament/plugins/monaco-editor-logs.blade.php @@ -0,0 +1,184 @@ +@script + +@endscript + + + +
+
+ @if($getShowFullScreenToggle()) + + @endif +
+
+ +
+
+
+
+
+ + diff --git a/resources/views/livewire/columns/server-entry-column.blade.php b/resources/views/livewire/columns/server-entry-column.blade.php new file mode 100644 index 000000000..332b1a988 --- /dev/null +++ b/resources/views/livewire/columns/server-entry-column.blade.php @@ -0,0 +1,8 @@ +@php + /** @var \App\Models\Server $server */ + $server = $getRecord(); +@endphp + +
+ @livewire('server-entry', ['server' => $server, 'lazy' => true], key($server->id)) +
\ No newline at end of file diff --git a/resources/views/tables/columns/server-entry-column.blade.php b/resources/views/livewire/server-entry.blade.php similarity index 73% rename from resources/views/tables/columns/server-entry-column.blade.php rename to resources/views/livewire/server-entry.blade.php index 152db35b6..6f4b25775 100644 --- a/resources/views/tables/columns/server-entry-column.blade.php +++ b/resources/views/livewire/server-entry.blade.php @@ -1,25 +1,11 @@ -@php - use App\Enums\ServerResourceType; - - /** @var \App\Models\Server $server */ - $server = $getRecord(); -@endphp - - - - -
- \ No newline at end of file diff --git a/routes/api-client.php b/routes/api-client.php index 0feceeb78..c5ab1d3ba 100644 --- a/routes/api-client.php +++ b/routes/api-client.php @@ -129,6 +129,7 @@ Route::prefix('/servers/{server:uuid}')->middleware([ServerSubject::class, Authe Route::prefix('/settings')->group(function () { Route::post('/rename', [Client\Servers\SettingsController::class, 'rename']); + Route::post('/description', [Client\Servers\SettingsController::class, 'description']); Route::post('/reinstall', [Client\Servers\SettingsController::class, 'reinstall']); Route::put('/docker-image', [Client\Servers\SettingsController::class, 'dockerImage']); }); diff --git a/storage/app/packs/.githold b/storage/app/packs/.githold deleted file mode 100755 index e69de29bb..000000000 diff --git a/tests/Feature/SettingsControllerTest.php b/tests/Feature/SettingsControllerTest.php index c1850489e..769ffa18f 100644 --- a/tests/Feature/SettingsControllerTest.php +++ b/tests/Feature/SettingsControllerTest.php @@ -41,7 +41,7 @@ it('server description can be changed', function () { expect()->toLogActivities(1) ->and($logged->properties['old'])->toBe($originalDescription) ->and($logged->properties['new'])->toBe($newDescription) - ->and($server->description)->not()->toBe($originalDescription); + ->and($server->description)->toBe($newDescription); }); it('server description cannot be changed', function () { @@ -53,7 +53,7 @@ it('server description cannot be changed', function () { ->post("/api/client/servers/$server->uuid/settings/description", [ 'description' => 'Test Description', ]) - ->assertStatus(Response::HTTP_NO_CONTENT); + ->assertStatus(Response::HTTP_FORBIDDEN); $server = $server->refresh(); expect()->toLogActivities(0) diff --git a/tests/Integration/Api/Client/Server/SettingsControllerTest.php b/tests/Integration/Api/Client/Server/SettingsControllerTest.php index a0a722651..ded84ce1d 100644 --- a/tests/Integration/Api/Client/Server/SettingsControllerTest.php +++ b/tests/Integration/Api/Client/Server/SettingsControllerTest.php @@ -21,11 +21,9 @@ class SettingsControllerTest extends ClientApiIntegrationTestCase /** @var \App\Models\Server $server */ [$user, $server] = $this->generateTestAccount($permissions); $originalName = $server->name; - $originalDescription = $server->description; $response = $this->actingAs($user)->postJson("/api/client/servers/$server->uuid/settings/rename", [ 'name' => '', - 'description' => '', ]); $response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY); @@ -33,18 +31,15 @@ class SettingsControllerTest extends ClientApiIntegrationTestCase $server = $server->refresh(); $this->assertSame($originalName, $server->name); - $this->assertSame($originalDescription, $server->description); $this->actingAs($user) ->postJson("/api/client/servers/$server->uuid/settings/rename", [ 'name' => 'Test Server Name', - 'description' => 'This is a test server.', ]) ->assertStatus(Response::HTTP_NO_CONTENT); $server = $server->refresh(); $this->assertSame('Test Server Name', $server->name); - $this->assertSame('This is a test server.', $server->description); } /** diff --git a/tests/Integration/Services/Backups/DeleteBackupServiceTest.php b/tests/Integration/Services/Backups/DeleteBackupServiceTest.php index d280a95c7..8c0ac8ad2 100644 --- a/tests/Integration/Services/Backups/DeleteBackupServiceTest.php +++ b/tests/Integration/Services/Backups/DeleteBackupServiceTest.php @@ -2,10 +2,8 @@ namespace App\Tests\Integration\Services\Backups; -use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Response; use App\Models\Backup; -use GuzzleHttp\Exception\ClientException; use App\Extensions\Backups\BackupManager; use App\Extensions\Filesystem\S3Filesystem; use App\Services\Backups\DeleteBackupService; @@ -54,7 +52,7 @@ class DeleteBackupServiceTest extends IntegrationTestCase $backup = Backup::factory()->create(['server_id' => $server->id]); $mock = $this->mock(DaemonBackupRepository::class); - $mock->expects('setServer->delete')->with($backup)->andThrow(new ConnectionException(previous: new ClientException('', new Request('DELETE', '/'), new Response(404)))); + $mock->expects('setServer->delete')->with($backup)->andThrow(new ConnectionException(code: 404)); $this->app->make(DeleteBackupService::class)->handle($backup); @@ -69,7 +67,7 @@ class DeleteBackupServiceTest extends IntegrationTestCase $backup = Backup::factory()->create(['server_id' => $server->id]); $mock = $this->mock(DaemonBackupRepository::class); - $mock->expects('setServer->delete')->with($backup)->andThrow(new ConnectionException(previous: new ClientException('', new Request('DELETE', '/'), new Response(500)))); + $mock->expects('setServer->delete')->with($backup)->andThrow(new ConnectionException(code: 500)); $this->expectException(ConnectionException::class); diff --git a/tests/Unit/Helpers/EnvironmentWriterTraitTest.php b/tests/Unit/Helpers/EnvironmentWriterTraitTest.php deleted file mode 100644 index 4678c0579..000000000 --- a/tests/Unit/Helpers/EnvironmentWriterTraitTest.php +++ /dev/null @@ -1,43 +0,0 @@ -escapeEnvironmentValue($input); - - $this->assertSame($expected, $output); - } - - public static function variableDataProvider(): array - { - return [ - ['foo', 'foo'], - ['abc123', 'abc123'], - ['val"ue', '"val\"ue"'], - ['val\'ue', '"val\'ue"'], - ['my test value', '"my test value"'], - ['mysql_p@assword', '"mysql_p@assword"'], - ['mysql_p#assword', '"mysql_p#assword"'], - ['mysql p@$$word', '"mysql p@$$word"'], - ['mysql p%word', '"mysql p%word"'], - ['mysql p#word', '"mysql p#word"'], - ['abc_@#test', '"abc_@#test"'], - ['test 123 $$$', '"test 123 $$$"'], - ['#password%', '"#password%"'], - ['$pass ', '"$pass "'], - ]; - } -} - -class FooClass -{ - use EnvironmentWriterTrait; -}