From ec3f5004e70cae0e5ad8afabf1820545fec151f0 Mon Sep 17 00:00:00 2001 From: JoanFo1456 <161775222+JoanFo1456@users.noreply.github.com> Date: Sat, 13 Dec 2025 14:37:24 +0000 Subject: [PATCH 01/10] Server view page --- .../Resources/Servers/Pages/EditServer.php | 1101 +-------------- .../Resources/Servers/Pages/ListServers.php | 18 +- .../Resources/Servers/Pages/ViewServer.php | 120 ++ .../AllocationsRelationManager.php | 3 + .../Resources/Servers/ServerResource.php | 1182 ++++++++++++++++- 5 files changed, 1328 insertions(+), 1096 deletions(-) create mode 100644 app/Filament/Admin/Resources/Servers/Pages/ViewServer.php diff --git a/app/Filament/Admin/Resources/Servers/Pages/EditServer.php b/app/Filament/Admin/Resources/Servers/Pages/EditServer.php index 4db27aa798..8236282067 100644 --- a/app/Filament/Admin/Resources/Servers/Pages/EditServer.php +++ b/app/Filament/Admin/Resources/Servers/Pages/EditServer.php @@ -2,70 +2,30 @@ namespace App\Filament\Admin\Resources\Servers\Pages; -use App\Enums\SuspendAction; +use App\Filament\Admin\Resources\Servers\Pages\ListServers; use App\Filament\Admin\Resources\Servers\RelationManagers\AllocationsRelationManager; use App\Filament\Admin\Resources\Servers\RelationManagers\DatabasesRelationManager; use App\Filament\Admin\Resources\Servers\ServerResource; -use App\Filament\Components\Actions\PreviewStartupAction; -use App\Filament\Components\Forms\Fields\StartupVariable; -use App\Filament\Components\StateCasts\ServerConditionStateCast; use App\Filament\Server\Pages\Console; -use App\Models\Allocation; -use App\Models\Egg; use App\Models\Server; -use App\Models\User; use App\Repositories\Daemon\DaemonServerRepository; -use App\Services\Eggs\EggChangerService; -use App\Services\Servers\RandomWordService; -use App\Services\Servers\ReinstallServerService; 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 Exception; use Filament\Actions\Action; use Filament\Actions\ActionGroup; -use Filament\Forms\Components\CodeEditor; -use Filament\Forms\Components\FileUpload; -use Filament\Forms\Components\Hidden; -use Filament\Forms\Components\KeyValue; -use Filament\Forms\Components\Repeater; -use Filament\Forms\Components\Select; -use Filament\Forms\Components\TagsInput; -use Filament\Forms\Components\Textarea; -use Filament\Forms\Components\TextInput; -use Filament\Forms\Components\Toggle; -use Filament\Forms\Components\ToggleButtons; -use Filament\Infolists\Components\TextEntry; use Filament\Notifications\Notification; use Filament\Resources\Pages\EditRecord; -use Filament\Schemas\Components\Actions; -use Filament\Schemas\Components\Component; -use Filament\Schemas\Components\Fieldset; -use Filament\Schemas\Components\Grid; -use Filament\Schemas\Components\Image; -use Filament\Schemas\Components\StateCasts\BooleanStateCast; -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\Schemas\Schema; -use Filament\Support\Enums\Alignment; use Filament\Support\Enums\IconSize; -use Illuminate\Database\Eloquent\Builder; use Illuminate\Http\Client\ConnectionException; -use Illuminate\Support\Arr; -use Illuminate\Support\HtmlString; -use LogicException; use Random\RandomException; class EditServer extends EditRecord { use CanCustomizeHeaderActions; use CanCustomizeHeaderWidgets; - protected static string $resource = ServerResource::class; private DaemonServerRepository $daemonServerRepository; @@ -81,1008 +41,7 @@ public function boot(DaemonServerRepository $daemonServerRepository): void */ public function form(Schema $schema): Schema { - return $schema - ->components([ - Tabs::make('Tabs') - ->persistTabInQueryString() - ->columns([ - 'default' => 2, - 'sm' => 2, - 'md' => 4, - 'lg' => 6, - ]) - ->columnSpanFull() - ->tabs([ - Tab::make('information') - ->label(trans('admin/server.tabs.information')) - ->icon('tabler-info-circle') - ->schema([ - Grid::make() - ->columns(2) - ->columnStart(1) - ->schema([ - Image::make('', 'icon') - ->hidden(fn ($record) => !$record->icon && !$record->egg->image) - ->url(fn ($record) => $record->icon ?: $record->egg->image) - ->tooltip(fn ($record) => $record->icon ? '' : trans('server/setting.server_info.icon.tooltip')) - ->columnSpan(2) - ->alignJustify(), - Action::make('uploadIcon') - ->iconButton()->iconSize(IconSize::Large) - ->icon('tabler-photo-up') - ->modal() - ->modalSubmitActionLabel(trans('server/setting.server_info.icon.upload')) - ->schema([ - Tabs::make()->tabs([ - Tab::make(trans('admin/egg.import.url')) - ->schema([ - Hidden::make('base64Image'), - TextInput::make('image_url') - ->label(trans('admin/egg.import.image_url')) - ->reactive() - ->autocomplete(false) - ->debounce(500) - ->afterStateUpdated(function ($state, Set $set) { - if (!$state) { - $set('image_url_error', null); - - return; - } - - try { - if (!in_array(parse_url($state, PHP_URL_SCHEME), ['http', 'https'], true)) { - throw new \Exception(trans('admin/egg.import.invalid_url')); - } - - if (!filter_var($state, FILTER_VALIDATE_URL)) { - throw new \Exception(trans('admin/egg.import.invalid_url')); - } - - $allowedExtensions = [ - 'png' => 'image/png', - 'jpg' => 'image/jpeg', - 'jpeg' => 'image/jpeg', - 'gif' => 'image/gif', - 'webp' => 'image/webp', - 'svg' => 'image/svg+xml', - ]; - - $extension = strtolower(pathinfo(parse_url($state, PHP_URL_PATH), PATHINFO_EXTENSION)); - - if (!array_key_exists($extension, $allowedExtensions)) { - throw new \Exception(trans('admin/egg.import.unsupported_format', ['format' => implode(', ', array_keys($allowedExtensions))])); - } - - $host = parse_url($state, PHP_URL_HOST); - $ip = gethostbyname($host); - - if ( - filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false - ) { - throw new \Exception(trans('admin/egg.import.no_local_ip')); - } - - $context = stream_context_create([ - 'http' => ['timeout' => 3], - 'https' => [ - 'timeout' => 3, - 'verify_peer' => true, - 'verify_peer_name' => true, - ], - ]); - - $imageContent = @file_get_contents($state, false, $context, 0, 262144); //256KB - - if (!$imageContent) { - throw new \Exception(trans('admin/egg.import.image_error')); - } - - $mimeType = $allowedExtensions[$extension]; - $base64 = 'data:' . $mimeType . ';base64,' . base64_encode($imageContent); - - $set('base64Image', $base64); - $set('image_url_error', null); - - } catch (\Exception $e) { - $set('image_url_error', $e->getMessage()); - $set('base64Image', null); - } - }), - TextEntry::make('image_url_error') - ->hiddenLabel() - ->visible(fn (Get $get) => $get('image_url_error') !== null) - ->afterStateHydrated(fn (Get $get) => $get('image_url_error')), - Image::make(fn (Get $get) => $get('image_url'), '') - ->imageSize(150) - ->visible(fn (Get $get) => $get('image_url') && !$get('image_url_error')) - ->alignCenter(), - ]), - Tab::make(trans('admin/egg.import.file')) - ->schema([ - FileUpload::make('image') - ->hiddenLabel() - ->previewable() - ->openable(false) - ->downloadable(false) - ->maxSize(256) - ->maxFiles(1) - ->columnSpanFull() - ->alignCenter() - ->imageEditor() - ->image() - ->saveUploadedFileUsing(function ($file, Set $set) { - $base64 = "data:{$file->getMimeType()};base64,". base64_encode(file_get_contents($file->getRealPath())); - $set('base64Image', $base64); - - return $base64; - }), - ]), - ]), - ]) - ->action(function (array $data, $record): void { - $base64 = $data['base64Image'] ?? null; - - if (empty($base64) && !empty($data['image'])) { - $base64 = $data['image']; - } - - if (!empty($base64)) { - $record->update([ - 'icon' => $base64, - ]); - - Notification::make() - ->title(trans('server/setting.server_info.icon.updated')) - ->success() - ->send(); - - $record->refresh(); - } else { - Notification::make() - ->title(trans('admin/egg.import.no_image')) - ->warning() - ->send(); - } - }), - Action::make('deleteIcon') - ->visible(fn ($record) => $record->icon) - ->label('') - ->icon('tabler-trash') - ->iconButton()->iconSize(IconSize::Large) - ->color('danger') - ->action(function ($record) { - $record->update([ - 'icon' => null, - ]); - - Notification::make() - ->title(trans('server/setting.server_info.icon.deleted')) - ->success() - ->send(); - - $record->refresh(); - }), - ]), - Grid::make() - ->columns(3) - ->columnStart(2) - ->columnSpan([ - 'default' => 2, - 'sm' => 2, - 'md' => 3, - 'lg' => 5, - ]) - ->schema([ - TextInput::make('name') - ->prefixIcon('tabler-server') - ->label(trans('admin/server.name')) - ->suffixAction(Action::make('random') - ->icon('tabler-dice-' . random_int(1, 6)) - ->action(function (Set $set, Get $get) { - $egg = Egg::find($get('egg_id')); - $prefix = $egg ? str($egg->name)->lower()->kebab() . '-' : ''; - - $word = (new RandomWordService())->word(); - - $set('name', $prefix . $word); - })) - ->columnSpan([ - 'default' => 2, - 'sm' => 1, - 'md' => 2, - 'lg' => 3, - ]) - ->required() - ->maxLength(255), - Select::make('owner_id') - ->prefixIcon('tabler-user') - ->label(trans('admin/server.owner')) - ->columnSpan([ - 'default' => 2, - 'sm' => 1, - 'md' => 2, - 'lg' => 2, - ]) - ->relationship('user', 'username') - ->searchable(['username', 'email']) - ->getOptionLabelFromRecordUsing(fn (User $user) => "$user->username ($user->email)") - ->preload() - ->required(), - 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()]) - ->stateCast(new ServerConditionStateCast()) - ->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')) - ->schema([ - CodeEditor::make('logs') - ->hiddenLabel() - ->formatStateUsing(function (Server $server, DaemonServerRepository $serverRepository) { - try { - $logs = $serverRepository->setServer($server)->getInstallLogs(); - - return mb_convert_encoding($logs, 'UTF-8', ['UTF-8', 'UTF-16', 'ISO-8859-1', 'ASCII']); - } 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 ''; - }), - ]) - ), - ]), - Textarea::make('description') - ->label(trans('admin/server.description')) - ->columnSpanFull(), - TextInput::make('uuid') - ->label(trans('admin/server.uuid')) - ->copyable() - ->columnSpan([ - 'default' => 2, - 'sm' => 1, - 'md' => 2, - 'lg' => 3, - ]) - ->readOnly() - ->dehydrated(false), - TextInput::make('uuid_short') - ->label(trans('admin/server.short_uuid')) - ->copyable() - ->columnSpan([ - 'default' => 2, - 'sm' => 1, - 'md' => 2, - 'lg' => 3, - ]) - ->readOnly() - ->dehydrated(false), - TextInput::make('external_id') - ->label(trans('admin/server.external_id')) - ->columnSpan([ - 'default' => 2, - 'sm' => 1, - 'md' => 2, - 'lg' => 3, - ]) - ->unique() - ->maxLength(255), - Select::make('node_id') - ->label(trans('admin/server.node')) - ->relationship('node', 'name', fn (Builder $query) => $query->whereIn('id', user()?->accessibleNodes()->pluck('id'))) - ->columnSpan([ - 'default' => 2, - 'sm' => 1, - 'md' => 2, - 'lg' => 3, - ]) - ->disabled(), - ]), - Tab::make('environment_configuration') - ->label(trans('admin/server.tabs.environment_configuration')) - ->icon('tabler-brand-docker') - ->schema([ - Fieldset::make(trans('admin/server.resource_limits')) - ->columnSpanFull() - ->columns([ - 'default' => 1, - 'sm' => 2, - 'md' => 3, - 'lg' => 3, - ]) - ->schema([ - Grid::make() - ->columns(4) - ->columnSpanFull() - ->schema([ - ToggleButtons::make('unlimited_cpu') - ->dehydrated() - ->label(trans('admin/server.cpu'))->inlineLabel()->inline() - ->afterStateUpdated(fn (Set $set) => $set('cpu', 0)) - ->formatStateUsing(fn (Get $get) => $get('cpu') == 0) - ->live() - ->stateCast(new BooleanStateCast(false, true)) - ->options([ - 1 => trans('admin/server.unlimited'), - 0 => trans('admin/server.limited'), - ]) - ->colors([ - 1 => 'primary', - 0 => 'warning', - ]) - ->columnSpan(2), - - TextInput::make('cpu') - ->dehydratedWhenHidden() - ->hidden(fn (Get $get) => $get('unlimited_cpu')) - ->label(trans('admin/server.cpu_limit'))->inlineLabel() - ->suffix('%') - ->hintIcon('tabler-question-mark', trans('admin/server.cpu_helper')) - ->required() - ->columnSpan(2) - ->numeric() - ->minValue(0), - ]), - Grid::make() - ->columns(4) - ->columnSpanFull() - ->schema([ - ToggleButtons::make('unlimited_mem') - ->dehydrated() - ->label(trans('admin/server.memory'))->inlineLabel()->inline() - ->afterStateUpdated(fn (Set $set) => $set('memory', 0)) - ->formatStateUsing(fn (Get $get) => $get('memory') == 0) - ->live() - ->stateCast(new BooleanStateCast(false, true)) - ->options([ - 1 => trans('admin/server.unlimited'), - 0 => trans('admin/server.limited'), - ]) - ->colors([ - 1 => 'primary', - 0 => 'warning', - ]) - ->columnSpan(2), - - TextInput::make('memory') - ->dehydratedWhenHidden() - ->hidden(fn (Get $get) => $get('unlimited_mem')) - ->label(trans('admin/server.memory_limit'))->inlineLabel() - ->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB') - ->hintIcon('tabler-question-mark', trans('admin/server.memory_helper')) - ->required() - ->columnSpan(2) - ->numeric() - ->minValue(0), - ]), - - Grid::make() - ->columns(4) - ->columnSpanFull() - ->schema([ - ToggleButtons::make('unlimited_disk') - ->dehydrated() - ->label(trans('admin/server.disk'))->inlineLabel()->inline() - ->live() - ->afterStateUpdated(fn (Set $set) => $set('disk', 0)) - ->formatStateUsing(fn (Get $get) => $get('disk') == 0) - ->stateCast(new BooleanStateCast(false, true)) - ->options([ - 1 => trans('admin/server.unlimited'), - 0 => trans('admin/server.limited'), - ]) - ->colors([ - 1 => 'primary', - 0 => 'warning', - ]) - ->columnSpan(2), - - TextInput::make('disk') - ->dehydratedWhenHidden() - ->hidden(fn (Get $get) => $get('unlimited_disk')) - ->label(trans('admin/server.disk_limit'))->inlineLabel() - ->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB') - ->required() - ->columnSpan(2) - ->numeric() - ->minValue(0), - ]), - ]), - - Fieldset::make(trans('admin/server.advanced_limits')) - ->columnSpanFull() - ->columns([ - 'default' => 1, - 'sm' => 2, - 'md' => 3, - 'lg' => 3, - ]) - ->schema([ - Grid::make() - ->columns(4) - ->columnSpanFull() - ->schema([ - Grid::make() - ->columns(4) - ->columnSpanFull() - ->schema([ - ToggleButtons::make('cpu_pinning') - ->label(trans('admin/server.cpu_pin'))->inlineLabel()->inline() - ->default(0) - ->afterStateUpdated(fn (Set $set) => $set('threads', [])) - ->formatStateUsing(fn (Get $get) => !empty($get('threads'))) - ->live() - ->stateCast(new BooleanStateCast(false, true)) - ->options([ - 0 => trans('admin/server.disabled'), - 1 => trans('admin/server.enabled'), - ]) - ->colors([ - 0 => 'success', - 1 => 'warning', - ]) - ->columnSpan(2), - - TagsInput::make('threads') - ->dehydratedWhenHidden() - ->hidden(fn (Get $get) => !$get('cpu_pinning')) - ->label(trans('admin/server.threads'))->inlineLabel() - ->required(fn (Get $get) => $get('cpu_pinning')) - ->columnSpan(2) - ->separator() - ->splitKeys([',']) - ->placeholder(trans('admin/server.pin_help')), - ]), - ToggleButtons::make('swap_support') - ->live() - ->label(trans('admin/server.swap'))->inlineLabel()->inline() - ->columnSpan(2) - ->afterStateUpdated(function ($state, Set $set) { - $value = match ($state) { - 'unlimited' => -1, - 'disabled' => 0, - 'limited' => 128, - default => throw new LogicException('Invalid state') - }; - - $set('swap', $value); - }) - ->formatStateUsing(function (Get $get) { - return match (true) { - $get('swap') > 0 => 'limited', - $get('swap') == 0 => 'disabled', - $get('swap') < 0 => 'unlimited', - default => throw new LogicException('Invalid state') - }; - }) - ->options([ - 'unlimited' => trans('admin/server.unlimited'), - 'limited' => trans('admin/server.limited'), - 'disabled' => trans('admin/server.disabled'), - ]) - ->colors([ - 'unlimited' => 'primary', - 'limited' => 'warning', - 'disabled' => 'danger', - ]), - - TextInput::make('swap') - ->dehydratedWhenHidden() - ->hidden(fn (Get $get) => match ($get('swap_support')) { - 'disabled', 'unlimited', true => true, - default => false, - }) - ->label(trans('admin/server.swap'))->inlineLabel() - ->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB') - ->minValue(-1) - ->columnSpan(2) - ->required() - ->integer(), - ]), - - Hidden::make('io') - ->helperText('The IO performance relative to other running containers') - ->label('Block IO Proportion'), - - Grid::make() - ->columns(4) - ->columnSpanFull() - ->schema([ - ToggleButtons::make('oom_killer') - ->dehydrated() - ->label(trans('admin/server.oom')) - ->formatStateUsing(fn ($state) => $state) - ->inlineLabel() - ->inline() - ->columnSpan(2) - ->stateCast(new BooleanStateCast(false, true)) - ->options([ - 0 => trans('admin/server.disabled'), - 1 => trans('admin/server.enabled'), - ]) - ->colors([ - 0 => 'success', - 1 => 'danger', - ]), - ]), - ]), - - Fieldset::make(trans('admin/server.feature_limits')) - ->inlineLabel() - ->columnSpanFull() - ->columns([ - 'default' => 1, - 'sm' => 2, - 'md' => 3, - 'lg' => 3, - ]) - ->schema([ - TextInput::make('allocation_limit') - ->label(trans('admin/server.allocations')) - ->suffixIcon('tabler-network') - ->required() - ->minValue(0) - ->numeric(), - TextInput::make('database_limit') - ->label(trans('admin/server.databases')) - ->suffixIcon('tabler-database') - ->required() - ->minValue(0) - ->numeric(), - TextInput::make('backup_limit') - ->label(trans('admin/server.backups')) - ->suffixIcon('tabler-copy-check') - ->required() - ->minValue(0) - ->numeric(), - ]), - Fieldset::make(trans('admin/server.docker_settings')) - ->columnSpanFull() - ->columns([ - 'default' => 1, - 'sm' => 2, - 'md' => 3, - 'lg' => 4, - ]) - ->schema([ - Select::make('select_image') - ->label(trans('admin/server.image_name')) - ->live() - ->afterStateUpdated(fn (Set $set, $state) => $set('image', $state)) - ->options(function ($state, Get $get, Set $set) { - $egg = Egg::query()->find($get('egg_id')); - $images = $egg->docker_images ?? []; - - $currentImage = $get('image'); - if (!$currentImage && $images) { - $defaultImage = collect($images)->first(); - $set('image', $defaultImage); - $set('select_image', $defaultImage); - } - - return array_flip($images) + ['ghcr.io/custom-image' => 'Custom Image']; - }) - ->selectablePlaceholder(false) - ->columnSpan([ - 'default' => 1, - 'sm' => 2, - 'md' => 3, - 'lg' => 2, - ]), - - TextInput::make('image') - ->label(trans('admin/server.image')) - ->required() - ->afterStateUpdated(function ($state, Get $get, Set $set) { - $egg = Egg::query()->find($get('egg_id')); - $images = $egg->docker_images ?? []; - - if (in_array($state, $images)) { - $set('select_image', $state); - } else { - $set('select_image', 'ghcr.io/custom-image'); - } - }) - ->placeholder(trans('admin/server.image_placeholder')) - ->columnSpan([ - 'default' => 1, - 'sm' => 2, - 'md' => 3, - 'lg' => 2, - ]), - - KeyValue::make('docker_labels') - ->live() - ->label(trans('admin/server.container_labels')) - ->keyLabel(trans('admin/server.title')) - ->valueLabel(trans('admin/server.description')) - ->columnSpanFull(), - ]), - ]), - Tab::make('egg') - ->label(trans('admin/server.egg')) - ->icon('tabler-egg') - ->columns([ - 'default' => 1, - 'sm' => 3, - 'md' => 3, - 'lg' => 5, - ]) - ->schema([ - Select::make('egg_id') - ->disabled() - ->prefixIcon('tabler-egg') - ->columnSpan([ - 'default' => 6, - 'sm' => 3, - 'md' => 3, - 'lg' => 4, - ]) - ->relationship('egg', 'name') - ->label(trans('admin/server.name')) - ->searchable() - ->preload() - ->required() - ->hintAction( - Action::make('change_egg') - ->label(trans('admin/server.change_egg')) - ->action(function (array $data, Server $server, EggChangerService $service) { - $service->handle($server, $data['egg_id'], $data['keep_old_variables']); - - // Use redirect instead of fillForm to prevent server variables from duplicating - $this->redirect($this->getUrl(['record' => $server, 'tab' => 'egg::data::tab']), true); - }) - ->schema(fn (Server $server) => [ - Select::make('egg_id') - ->label(trans('admin/server.new_egg')) - ->prefixIcon('tabler-egg') - ->options(fn () => Egg::all()->filter(fn (Egg $egg) => $egg->id !== $server->egg->id)->mapWithKeys(fn (Egg $egg) => [$egg->id => $egg->name])) - ->searchable() - ->preload() - ->required(), - Toggle::make('keep_old_variables') - ->label(trans('admin/server.keep_old_variables')) - ->default(true), - ]) - ), - - ToggleButtons::make('skip_scripts') - ->label(trans('admin/server.install_script')) - ->inline() - ->columnSpan([ - 'default' => 6, - 'sm' => 1, - 'md' => 1, - 'lg' => 2, - ]) - ->stateCast(new BooleanStateCast(false, true)) - ->options([ - 0 => trans('admin/server.yes'), - 1 => trans('admin/server.skip'), - ]) - ->colors([ - 0 => 'primary', - 1 => 'danger', - ]) - ->icons([ - 0 => 'tabler-code', - 1 => 'tabler-code-off', - ]) - ->required(), - - Hidden::make('previewing') - ->default(false), - - Select::make('select_startup') - ->label(trans('admin/server.startup_cmd')) - ->required() - ->live() - ->options(function (Get $get) { - $egg = Egg::find($get('egg_id')); - - return array_flip($egg->startup_commands ?? []) + ['custom' => 'Custom Startup']; - }) - ->formatStateUsing(fn (Server $server) => in_array($server->startup, $server->egg->startup_commands) ? $server->startup : 'custom') - ->afterStateUpdated(function (Set $set, string $state) { - if ($state !== 'custom') { - $set('startup', $state); - } - $set('previewing', false); - }) - ->selectablePlaceholder(false) - ->columnSpanFull() - ->hintAction(PreviewStartupAction::make('preview')), - - Textarea::make('startup') - ->hiddenLabel() - ->required() - ->live() - ->autosize() - ->afterStateUpdated(function ($state, Get $get, Set $set) { - $egg = Egg::find($get('egg_id')); - $startups = $egg->startup_commands ?? []; - - if (in_array($state, $startups)) { - $set('select_startup', $state); - } else { - $set('select_startup', 'custom'); - } - }) - ->placeholder(trans('admin/server.startup_placeholder')) - ->columnSpanFull(), - - Repeater::make('server_variables') - ->hiddenLabel() - ->relationship('serverVariables', function (Builder $query) { - /** @var Server $server */ - $server = $this->getRecord(); - - $server->ensureVariablesExist(); - - return $query->orderByPowerJoins('variable.sort'); - }) - ->grid() - ->mutateRelationshipDataBeforeSaveUsing(function (array $data): array { - $data['variable_value'] ??= ''; - - return $data; - }) - ->reorderable(false)->addable(false)->deletable(false) - ->schema([ - StartupVariable::make('variable_value') - ->fromRecord(), - ]) - ->columnSpan(6), - ]), - Tab::make('mounts') - ->label(trans('admin/server.mounts')) - ->icon('tabler-layers-linked') - ->schema(fn (Get $get) => [ - ServerResource::getMountCheckboxList($get), - ]), - Tab::make('actions') - ->label(trans('admin/server.actions')) - ->icon('tabler-settings') - ->schema([ - Fieldset::make(trans('admin/server.actions')) - ->columnSpanFull() - ->columns([ - 'default' => 1, - 'sm' => 2, - 'md' => 2, - 'lg' => 6, - ]) - ->schema([ - Grid::make() - ->columnSpan(3) - ->schema([ - Actions::make([ - Action::make('toggleInstall') - ->label(trans('admin/server.toggle_install')) - ->disabled(fn (Server $server) => $server->isSuspended()) - ->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->isFailedInstall()) { - try { - $reinstallService->handle($server); - - Notification::make() - ->title(trans('admin/server.notifications.reinstall_started')) - ->success() - ->send(); - - } catch (Exception) { - Notification::make() - ->title(trans('admin/server.notifications.reinstall_failed')) - ->body(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name])) - ->danger() - ->send(); - } - } else { - try { - $toggleService->handle($server); - - Notification::make() - ->title(trans('admin/server.notifications.install_toggled')) - ->success() - ->send(); - - } catch (Exception $exception) { - Notification::make() - ->title(trans('admin/server.notifications.install_toggle_failed')) - ->body($exception->getMessage()) - ->danger() - ->send(); - } - } - }), - ])->fullWidth(), - ToggleButtons::make('install_help') - ->hiddenLabel() - ->hint(trans('admin/server.toggle_install_help')), - ]), - Grid::make() - ->columnSpan(3) - ->schema([ - Actions::make([ - Action::make('toggleSuspend') - ->label(trans('admin/server.suspend')) - ->color('warning') - ->hidden(fn (Server $server) => $server->isSuspended()) - ->action(function (SuspensionService $suspensionService, Server $server) { - try { - $suspensionService->handle($server, SuspendAction::Suspend); - - Notification::make() - ->success() - ->title(trans('admin/server.notifications.server_suspended')) - ->send(); - - } catch (Exception) { - Notification::make() - ->warning() - ->title(trans('admin/server.notifications.server_suspension')) - ->body(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name])) - ->send(); - } - }), - Action::make('toggleUnsuspend') - ->label(trans('admin/server.unsuspend')) - ->color('success') - ->hidden(fn (Server $server) => !$server->isSuspended()) - ->action(function (SuspensionService $suspensionService, Server $server) { - try { - $suspensionService->handle($server, SuspendAction::Unsuspend); - - Notification::make() - ->success() - ->title(trans('admin/server.notifications.server_unsuspended')) - ->send(); - - } catch (Exception) { - Notification::make() - ->warning() - ->title(trans('admin/server.notifications.server_suspension')) - ->body(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name])) - ->send(); - } - }), - ])->fullWidth(), - ToggleButtons::make('server_suspend') - ->hiddenLabel() - ->hidden(fn (Server $server) => $server->isSuspended()) - ->hint(trans('admin/server.notifications.server_suspend_help')), - ToggleButtons::make('server_unsuspend') - ->hiddenLabel() - ->hidden(fn (Server $server) => !$server->isSuspended()) - ->hint(trans('admin/server.notifications.server_unsuspend_help')), - ]), - Grid::make() - ->columnSpan(3) - ->schema([ - Actions::make([ - Action::make('transfer') - ->label(trans('admin/server.transfer')) - ->disabled(fn (Server $server) => user()?->accessibleNodes()->count() <= 1 || $server->isInConflictState()) - ->modalHeading(trans('admin/server.transfer')) - ->schema($this->transferServer()) - ->action(function (TransferServerService $transfer, Server $server, $data) { - try { - $transfer->handle($server, Arr::get($data, 'node_id'), Arr::get($data, 'allocation_id'), Arr::get($data, 'allocation_additional', [])); - - Notification::make() - ->title(trans('admin/server.notifications.transfer_started')) - ->success() - ->send(); - } catch (Exception $exception) { - Notification::make() - ->title(trans('admin/server.notifications.transfer_failed')) - ->body($exception->getMessage()) - ->danger() - ->send(); - } - }), - ])->fullWidth(), - ToggleButtons::make('server_transfer') - ->hiddenLabel() - ->hint(new HtmlString(trans('admin/server.transfer_help'))), - ]), - Grid::make() - ->columnSpan(3) - ->schema([ - Actions::make([ - Action::make('reinstall') - ->label(trans('admin/server.reinstall')) - ->color('danger') - ->requiresConfirmation() - ->modalHeading(trans('admin/server.reinstall_modal_heading')) - ->modalDescription(trans('admin/server.reinstall_modal_description')) - ->disabled(fn (Server $server) => $server->isSuspended()) - ->action(function (ReinstallServerService $service, Server $server) { - try { - $service->handle($server); - - Notification::make() - ->title(trans('admin/server.notifications.reinstall_started')) - ->success() - ->send(); - } catch (Exception) { - Notification::make() - ->title(trans('admin/server.notifications.reinstall_failed')) - ->body(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name])) - ->danger() - ->send(); - } - }), - ])->fullWidth(), - ToggleButtons::make('server_reinstall') - ->hiddenLabel() - ->hint(trans('admin/server.reinstall_help')), - ]), - ]), - ]), - ]), - ]); - } - - /** @return Component[] - * @throws Exception - */ - protected function transferServer(): array - { - return [ - Select::make('node_id') - ->label(trans('admin/server.node')) - ->prefixIcon('tabler-server-2') - ->selectablePlaceholder(false) - ->default(fn (Server $server) => user()?->accessibleNodes()->whereNot('id', $server->node->id)->first()?->id) - ->required() - ->live() - ->options(fn (Server $server) => user()?->accessibleNodes()->whereNot('id', $server->node->id)->pluck('name', 'id')->all()), - Select::make('allocation_id') - ->label(trans('admin/server.primary_allocation')) - ->disabled(fn (Get $get, Server $server) => !$get('node_id') || !$server->allocation_id) - ->required(fn (Server $server) => $server->allocation_id) - ->prefixIcon('tabler-network') - ->options(fn (Get $get) => Allocation::where('node_id', $get('node_id'))->whereNull('server_id')->get()->mapWithKeys(fn (Allocation $allocation) => [$allocation->id => $allocation->address])) - ->searchable(['ip', 'port', 'ip_alias']) - ->placeholder(trans('admin/server.select_allocation')), - Select::make('allocation_additional') - ->label(trans('admin/server.additional_allocations')) - ->disabled(fn (Get $get, Server $server) => !$get('node_id') || $server->allocations->count() <= 1) - ->multiple() - ->minItems(fn (Select $select) => $select->getMaxItems()) - ->maxItems(fn (Select $select, Server $server) => $select->isDisabled() ? null : $server->allocations->count() - 1) - ->prefixIcon('tabler-network') - ->required(fn (Server $server) => $server->allocations->count() > 1) - ->options(fn (Get $get) => Allocation::where('node_id', $get('node_id'))->whereNull('server_id')->when($get('allocation_id'), fn ($query) => $query->whereNot('id', $get('allocation_id')))->get()->mapWithKeys(fn (Allocation $allocation) => [$allocation->id => $allocation->address])) - ->searchable(['ip', 'port', 'ip_alias']) - ->placeholder(trans('admin/server.select_additional')), - ]; + return ServerResource::schema($schema); } /** @return array */ @@ -1094,10 +53,15 @@ protected function getDefaultHeaderActions(): array $canForceDelete = cache()->get("servers.$server->uuid.canForceDelete", false); return [ + Action::make('console') + ->label(trans('admin/server.console')) + ->icon('tabler-terminal') + ->iconButton()->iconSize(IconSize::ExtraLarge) + ->url(fn (Server $server) => Console::getUrl(panel: 'server', tenant: $server)), Action::make('Delete') ->color('danger') ->label(trans('filament-actions::delete.single.label')) - ->modalHeading(trans('filament-actions::delete.single.modal.heading', ['label' => $this->getRecordTitle()])) + ->modalHeading(trans('filament-actions::delete.single.modal.heading', ['label' => $server->name])) ->modalSubmitActionLabel(trans('filament-actions::delete.single.label')) ->requiresConfirmation() ->action(function (Server $server, ServerDeletionService $service) { @@ -1118,13 +82,12 @@ protected function getDefaultHeaderActions(): array } }) ->hidden(fn () => $canForceDelete) - ->authorize(fn (Server $server) => user()?->can('delete server', $server)) ->icon('tabler-trash') ->iconButton()->iconSize(IconSize::ExtraLarge), Action::make('ForceDelete') ->color('danger') ->label(trans('filament-actions::force-delete.single.label')) - ->modalHeading(trans('filament-actions::force-delete.single.modal.heading', ['label' => $this->getRecordTitle()])) + ->modalHeading(trans('filament-actions::force-delete.single.modal.heading', ['label' => $server->name])) ->modalSubmitActionLabel(trans('filament-actions::force-delete.single.label')) ->requiresConfirmation() ->action(function (Server $server, ServerDeletionService $service) { @@ -1138,62 +101,16 @@ protected function getDefaultHeaderActions(): array }) ->visible(fn () => $canForceDelete) ->authorize(fn (Server $server) => user()?->can('delete server', $server)), - Action::make('console') - ->label(trans('admin/server.console')) - ->icon('tabler-terminal') - ->iconButton()->iconSize(IconSize::ExtraLarge) - ->url(fn (Server $server) => Console::getUrl(panel: 'server', tenant: $server)), $this->getSaveFormAction()->formId('form') ->iconButton()->iconSize(IconSize::ExtraLarge) ->icon('tabler-device-floppy'), ]; - } protected function getFormActions(): array { return []; } - - protected function mutateFormDataBeforeSave(array $data): array - { - if (!isset($data['description'])) { - $data['description'] = ''; - } - - unset($data['docker'], $data['status'], $data['allocation_id']); - - return $data; - } - - protected function afterSave(): void - { - /** @var Server $server */ - $server = $this->record; - - $changed = collect($server->getChanges())->except(['updated_at', 'name', 'owner_id', 'condition', 'description', 'external_id', 'tags', 'cpu_pinning', 'allocation_limit', 'database_limit', 'backup_limit', 'skip_scripts'])->all(); - - try { - if ($changed) { - $this->daemonServerRepository->setServer($server)->sync(); - } - parent::getSavedNotification()?->send(); - } catch (ConnectionException) { - Notification::make() - ->title(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name])) - ->body(trans('admin/server.notifications.error_connecting_description')) - ->color('warning') - ->icon('tabler-database') - ->warning() - ->send(); - } - } - - protected function getSavedNotification(): ?Notification - { - return null; - } - public function getRelationManagers(): array { return [ diff --git a/app/Filament/Admin/Resources/Servers/Pages/ListServers.php b/app/Filament/Admin/Resources/Servers/Pages/ListServers.php index ecb366b223..4a78c095b2 100644 --- a/app/Filament/Admin/Resources/Servers/Pages/ListServers.php +++ b/app/Filament/Admin/Resources/Servers/Pages/ListServers.php @@ -11,6 +11,7 @@ use Filament\Actions\ActionGroup; use Filament\Actions\CreateAction; use Filament\Actions\EditAction; +use Filament\Actions\ViewAction; use Filament\Resources\Pages\ListRecords; use Filament\Support\Enums\IconSize; use Filament\Tables\Columns\SelectColumn; @@ -29,6 +30,10 @@ class ListServers extends ListRecords public function table(Table $table): Table { return $table + ->recordUrl(fn (Server $server) => user()?->can('edit server', $server) + ? ServerResource::getUrl('edit', ['record' => $server]) + : ServerResource::getUrl('view', ['record' => $server]) + ) ->searchable(false) ->defaultGroup('node.name') ->groups([ @@ -91,14 +96,21 @@ public function table(Table $table): Table ->sortable(), ]) ->recordActions([ - Action::make('View') - ->label(trans('admin/server.view')) + Action::make('Console') + ->label(trans('admin/server.console')) ->iconButton() ->icon('tabler-terminal') ->iconSize(IconSize::Large) ->url(fn (Server $server) => Console::getUrl(panel: 'server', tenant: $server)) ->authorize(fn (Server $server) => user()?->canAccessTenant($server)), - EditAction::make(), + EditAction::make() + ->icon('tabler-pencil') + ->hiddenLabel() + ->iconSize(IconSize::Large), + ViewAction::make() + ->hiddenLabel() + ->icon('tabler-eye') + ->iconSize(IconSize::ExtraLarge), ]) ->emptyStateIcon('tabler-brand-docker') ->searchable() diff --git a/app/Filament/Admin/Resources/Servers/Pages/ViewServer.php b/app/Filament/Admin/Resources/Servers/Pages/ViewServer.php new file mode 100644 index 0000000000..e28e3d1e4b --- /dev/null +++ b/app/Filament/Admin/Resources/Servers/Pages/ViewServer.php @@ -0,0 +1,120 @@ +daemonServerRepository = $daemonServerRepository; + + } + + /** + * @throws \Random\RandomException + * @throws \Exception + */ + public function form(Schema $schema): Schema + { + return ServerResource::schema($schema); + } + + public function getRelationManagers(): array + { + return [ + AllocationsRelationManager::class, + DatabasesRelationManager::class, + ]; + } + + /** @return array */ + protected function getDefaultHeaderActions(): array + { + /** @var Server $server */ + $server = $this->getRecord(); + + $canForceDelete = cache()->get("servers.$server->uuid.canForceDelete", false); + + return [ + Action::make('console') + ->label(trans('admin/server.console')) + ->icon('tabler-terminal') + ->iconButton()->iconSize(IconSize::ExtraLarge) + ->url(fn (Server $server) => Console::getUrl(panel: 'server', tenant: $server)), + Action::make('Delete') + ->color('danger') + ->label(trans('filament-actions::delete.single.label')) + ->modalHeading(trans('filament-actions::delete.single.modal.heading', ['label' => $server->name])) + ->modalSubmitActionLabel(trans('filament-actions::delete.single.label')) + ->requiresConfirmation() + ->action(function (Server $server, ServerDeletionService $service) { + try { + $service->handle($server); + + return redirect(ListServers::getUrl(panel: 'admin')); + } catch (ConnectionException|PredisConnectionException) { + cache()->put("servers.$server->uuid.canForceDelete", true, now()->addMinutes(5)); + + return Notification::make() + ->title(trans('admin/server.notifications.error_server_delete')) + ->body(trans('admin/server.notifications.error_server_delete_body')) + ->color('warning') + ->icon('tabler-database') + ->warning() + ->send(); + } + }) + ->hidden(fn () => $canForceDelete) + ->authorize(fn (Server $server) => user()?->can('delete server', $server)) + ->icon('tabler-trash') + ->iconButton()->iconSize(IconSize::ExtraLarge), + Action::make('ForceDelete') + ->color('danger') + ->label(trans('filament-actions::force-delete.single.label')) + ->modalHeading(trans('filament-actions::force-delete.single.modal.heading', ['label' => $server->name])) + ->modalSubmitActionLabel(trans('filament-actions::force-delete.single.label')) + ->requiresConfirmation() + ->action(function (Server $server, ServerDeletionService $service) { + try { + $service->withForce()->handle($server); + + return redirect(ListServers::getUrl(panel: 'admin')); + } catch (ConnectionException|PredisConnectionException) { + return cache()->forget("servers.$server->uuid.canForceDelete"); + } + }) + ->visible(fn () => $canForceDelete) + ->authorize(fn (Server $server) => user()?->can('delete server', $server)), + EditAction::make() + ->icon('tabler-pencil') + ->iconButton()->iconSize(IconSize::ExtraLarge), + ]; + } +} diff --git a/app/Filament/Admin/Resources/Servers/RelationManagers/AllocationsRelationManager.php b/app/Filament/Admin/Resources/Servers/RelationManagers/AllocationsRelationManager.php index b7ad65ee25..f0090b0e45 100644 --- a/app/Filament/Admin/Resources/Servers/RelationManagers/AllocationsRelationManager.php +++ b/app/Filament/Admin/Resources/Servers/RelationManagers/AllocationsRelationManager.php @@ -72,14 +72,17 @@ public function table(Table $table): Table Action::make('make-primary') ->label(trans('admin/server.make_primary')) ->action(fn (Allocation $allocation) => $this->getOwnerRecord()->update(['allocation_id' => $allocation->id]) && $this->deselectAllTableRecords()) + ->disabled(fn () => !user()?->can('edit server', $this->getOwnerRecord())) ->hidden(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id), Action::make('lock') ->label(trans('admin/server.lock')) ->action(fn (Allocation $allocation) => $allocation->update(['is_locked' => true]) && $this->deselectAllTableRecords()) + ->disabled(fn () => !user()?->can('edit server', $this->getOwnerRecord())) ->hidden(fn (Allocation $allocation) => $allocation->is_locked), Action::make('unlock') ->label(trans('admin/server.unlock')) ->action(fn (Allocation $allocation) => $allocation->update(['is_locked' => false]) && $this->deselectAllTableRecords()) + ->disabled(fn () => !user()?->can('edit server', $this->getOwnerRecord())) ->visible(fn (Allocation $allocation) => $allocation->is_locked), DissociateAction::make() ->after(function (Allocation $allocation) { diff --git a/app/Filament/Admin/Resources/Servers/ServerResource.php b/app/Filament/Admin/Resources/Servers/ServerResource.php index 5273ddcc94..daeea11250 100644 --- a/app/Filament/Admin/Resources/Servers/ServerResource.php +++ b/app/Filament/Admin/Resources/Servers/ServerResource.php @@ -3,10 +3,16 @@ namespace App\Filament\Admin\Resources\Servers; use App\Enums\CustomizationKey; +use App\Enums\SuspendAction; +use App\Filament\Admin\Resources\DatabaseHosts\RelationManagers\DatabasesRelationManager; use App\Filament\Admin\Resources\Servers\Pages\CreateServer; use App\Filament\Admin\Resources\Servers\Pages\EditServer; use App\Filament\Admin\Resources\Servers\Pages\ListServers; +use App\Filament\Admin\Resources\Servers\Pages\ViewServer; use App\Filament\Admin\Resources\Servers\RelationManagers\AllocationsRelationManager; +use App\Filament\Components\Actions\PreviewStartupAction; +use App\Filament\Components\Forms\Fields\StartupVariable; +use App\Filament\Components\StateCasts\ServerConditionStateCast; use App\Models\Mount; use App\Models\Server; use App\Traits\Filament\CanCustomizePages; @@ -17,13 +23,65 @@ use Filament\Resources\RelationManagers\RelationManager; use Filament\Resources\Resource; use Filament\Schemas\Components\Utilities\Get; +use App\Filament\Server\Pages\Console; +use App\Models\Allocation; +use App\Models\Egg; +use App\Models\User; +use App\Repositories\Daemon\DaemonServerRepository; +use App\Services\Eggs\EggChangerService; +use App\Services\Servers\RandomWordService; +use App\Services\Servers\ReinstallServerService; +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 Filament\Actions\Action; +use Filament\Actions\ActionGroup; +use Filament\Actions\CreateAction; +use Filament\Actions\EditAction; +use Filament\Actions\ViewAction; +use Filament\Forms\Components\CodeEditor; +use Filament\Forms\Components\FileUpload; +use Filament\Forms\Components\Hidden; +use Filament\Forms\Components\KeyValue; +use Filament\Forms\Components\Repeater; +use Filament\Forms\Components\Select; +use Filament\Forms\Components\TagsInput; +use Filament\Forms\Components\Textarea; +use Filament\Forms\Components\TextInput; +use Filament\Forms\Components\Toggle; +use Filament\Forms\Components\ToggleButtons; +use Filament\Infolists\Components\TextEntry; +use Filament\Notifications\Notification; +use Filament\Resources\Pages\ListRecords; +use Filament\Schemas\Components\Actions; +use Filament\Schemas\Components\Fieldset; +use Filament\Schemas\Components\Grid; +use Filament\Schemas\Components\Image; +use Filament\Schemas\Components\StateCasts\BooleanStateCast; +use Filament\Schemas\Components\Tabs; +use Filament\Schemas\Components\Tabs\Tab; +use Filament\Schemas\Schema; +use Filament\Support\Enums\Alignment; +use Filament\Support\Enums\IconSize; +use Filament\Tables\Columns\SelectColumn; +use Filament\Tables\Columns\TextColumn; +use Filament\Tables\Grouping\Group; +use Filament\Tables\Table; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\HtmlString; +use LogicException; +use Pest\Support\Arr; +use Predis\Connection\ConnectionException; +use Filament\Schemas\Components\Utilities\Set; class ServerResource extends Resource { use CanCustomizePages; use CanCustomizeRelations; - + protected DaemonServerRepository $daemonServerRepository; protected static ?string $model = Server::class; protected static string|\BackedEnum|null $navigationIcon = 'tabler-brand-docker'; @@ -54,7 +112,1128 @@ public static function getNavigationBadge(): ?string { return (string) static::getEloquentQuery()->count() ?: null; } + public static function schema(Schema $schema): Schema + { + return $schema + ->components([ + Tabs::make('Tabs') + ->persistTabInQueryString() + ->columns([ + 'default' => 2, + 'sm' => 2, + 'md' => 4, + 'lg' => 6, + ]) + ->columnSpanFull() + ->tabs([ + Tab::make('information') + ->label(trans('admin/server.tabs.information')) + ->icon('tabler-info-circle') + ->schema([ + Grid::make() + ->columns(2) + ->columnStart(1) + ->schema([ + Image::make('', 'icon') + ->hidden(fn ($record) => !$record->icon && !$record->egg->image) + ->url(fn ($record) => $record->icon ?: $record->egg->image) + ->tooltip(fn ($record) => $record->icon ? '' : trans('server/setting.server_info.icon.tooltip')) + ->columnSpan(2) + ->alignJustify(), + Action::make('uploadIcon') + ->iconButton()->iconSize(IconSize::Large) + ->icon('tabler-photo-up') + ->modal() + ->modalSubmitActionLabel(trans('server/setting.server_info.icon.upload')) + ->schema([ + Tabs::make()->tabs([ + Tab::make(trans('admin/egg.import.url')) + ->schema([ + Hidden::make('base64Image'), + TextInput::make('image_url') + ->label(trans('admin/egg.import.image_url')) + ->reactive() + ->autocomplete(false) + ->debounce(500) + ->afterStateUpdated(function ($state, Set $set) { + if (!$state) { + $set('image_url_error', null); + + return; + } + + try { + if (!in_array(parse_url($state, PHP_URL_SCHEME), ['http', 'https'], true)) { + throw new \Exception(trans('admin/egg.import.invalid_url')); + } + + if (!filter_var($state, FILTER_VALIDATE_URL)) { + throw new \Exception(trans('admin/egg.import.invalid_url')); + } + + $allowedExtensions = [ + 'png' => 'image/png', + 'jpg' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'gif' => 'image/gif', + 'webp' => 'image/webp', + 'svg' => 'image/svg+xml', + ]; + + $extension = strtolower(pathinfo(parse_url($state, PHP_URL_PATH), PATHINFO_EXTENSION)); + + if (!array_key_exists($extension, $allowedExtensions)) { + throw new \Exception(trans('admin/egg.import.unsupported_format', ['format' => implode(', ', array_keys($allowedExtensions))])); + } + + $host = parse_url($state, PHP_URL_HOST); + $ip = gethostbyname($host); + + if ( + filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false + ) { + throw new \Exception(trans('admin/egg.import.no_local_ip')); + } + + $context = stream_context_create([ + 'http' => ['timeout' => 3], + 'https' => [ + 'timeout' => 3, + 'verify_peer' => true, + 'verify_peer_name' => true, + ], + ]); + + $imageContent = @file_get_contents($state, false, $context, 0, 262144); //256KB + + if (!$imageContent) { + throw new \Exception(trans('admin/egg.import.image_error')); + } + + $mimeType = $allowedExtensions[$extension]; + $base64 = 'data:' . $mimeType . ';base64,' . base64_encode($imageContent); + + $set('base64Image', $base64); + $set('image_url_error', null); + + } catch (\Exception $e) { + $set('image_url_error', $e->getMessage()); + $set('base64Image', null); + } + }), + TextEntry::make('image_url_error') + ->hiddenLabel() + ->visible(fn (Get $get) => $get('image_url_error') !== null) + ->afterStateHydrated(fn (Get $get) => $get('image_url_error')), + Image::make(fn (Get $get) => $get('image_url'), '') + ->imageSize(150) + ->visible(fn (Get $get) => $get('image_url') && !$get('image_url_error')) + ->alignCenter(), + ]), + Tab::make(trans('admin/egg.import.file')) + ->schema([ + FileUpload::make('image') + ->hiddenLabel() + ->previewable() + ->openable(false) + ->downloadable(false) + ->maxSize(256) + ->maxFiles(1) + ->columnSpanFull() + ->alignCenter() + ->imageEditor() + ->image() + ->saveUploadedFileUsing(function ($file, Set $set) { + $base64 = "data:{$file->getMimeType()};base64,". base64_encode(file_get_contents($file->getRealPath())); + $set('base64Image', $base64); + + return $base64; + }), + ]), + ]), + ]) + ->action(function (array $data, $record): void { + $base64 = $data['base64Image'] ?? null; + + if (empty($base64) && !empty($data['image'])) { + $base64 = $data['image']; + } + + if (!empty($base64)) { + $record->update([ + 'icon' => $base64, + ]); + + Notification::make() + ->title(trans('server/setting.server_info.icon.updated')) + ->success() + ->send(); + + $record->refresh(); + } else { + Notification::make() + ->title(trans('admin/egg.import.no_image')) + ->warning() + ->send(); + } + }), + Action::make('deleteIcon') + ->visible(fn ($record) => $record->icon) + ->label('') + ->icon('tabler-trash') + ->iconButton()->iconSize(IconSize::Large) + ->color('danger') + ->action(function ($record) { + $record->update([ + 'icon' => null, + ]); + + Notification::make() + ->title(trans('server/setting.server_info.icon.deleted')) + ->success() + ->send(); + + $record->refresh(); + }), + ]), + Grid::make() + ->columns(3) + ->columnStart(2) + ->columnSpan([ + 'default' => 2, + 'sm' => 2, + 'md' => 3, + 'lg' => 5, + ]) + ->schema([ + TextInput::make('name') + ->prefixIcon('tabler-server') + ->label(trans('admin/server.name')) + ->suffixAction(Action::make('random') + ->icon('tabler-dice-' . random_int(1, 6)) + ->action(function (Set $set, Get $get) { + $egg = Egg::find($get('egg_id')); + $prefix = $egg ? str($egg->name)->lower()->kebab() . '-' : ''; + + $word = (new RandomWordService())->word(); + + $set('name', $prefix . $word); + })) + ->columnSpan([ + 'default' => 2, + 'sm' => 1, + 'md' => 2, + 'lg' => 3, + ]) + ->required() + ->maxLength(255), + Select::make('owner_id') + ->prefixIcon('tabler-user') + ->label(trans('admin/server.owner')) + ->columnSpan([ + 'default' => 2, + 'sm' => 1, + 'md' => 2, + 'lg' => 2, + ]) + ->relationship('user', 'username') + ->searchable(['username', 'email']) + ->getOptionLabelFromRecordUsing(fn (User $user) => "$user->username ($user->email)") + ->preload() + ->required(), + 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()]) + ->stateCast(new ServerConditionStateCast()) + ->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')) + ->schema([ + CodeEditor::make('logs') + ->hiddenLabel() + ->formatStateUsing(function (Server $server, DaemonServerRepository $serverRepository) { + try { + $logs = $serverRepository->setServer($server)->getInstallLogs(); + + return mb_convert_encoding($logs, 'UTF-8', ['UTF-8', 'UTF-16', 'ISO-8859-1', 'ASCII']); + } 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 ''; + }), + ]) + ), + ]), + Textarea::make('description') + ->label(trans('admin/server.description')) + ->columnSpanFull(), + TextInput::make('uuid') + ->label(trans('admin/server.uuid')) + ->copyable() + ->columnSpan([ + 'default' => 2, + 'sm' => 1, + 'md' => 2, + 'lg' => 3, + ]) + ->readOnly() + ->dehydrated(false), + TextInput::make('uuid_short') + ->label(trans('admin/server.short_uuid')) + ->copyable() + ->columnSpan([ + 'default' => 2, + 'sm' => 1, + 'md' => 2, + 'lg' => 3, + ]) + ->readOnly() + ->dehydrated(false), + TextInput::make('external_id') + ->label(trans('admin/server.external_id')) + ->columnSpan([ + 'default' => 2, + 'sm' => 1, + 'md' => 2, + 'lg' => 3, + ]) + ->unique() + ->maxLength(255), + Select::make('node_id') + ->label(trans('admin/server.node')) + ->relationship('node', 'name', fn (Builder $query) => $query->whereIn('id', user()?->accessibleNodes()->pluck('id'))) + ->columnSpan([ + 'default' => 2, + 'sm' => 1, + 'md' => 2, + 'lg' => 3, + ]) + ->disabled(), + ]), + Tab::make('environment_configuration') + ->label(trans('admin/server.tabs.environment_configuration')) + ->icon('tabler-brand-docker') + ->schema([ + Fieldset::make(trans('admin/server.resource_limits')) + ->columnSpanFull() + ->columns([ + 'default' => 1, + 'sm' => 2, + 'md' => 3, + 'lg' => 3, + ]) + ->schema([ + Grid::make() + ->columns(4) + ->columnSpanFull() + ->schema([ + ToggleButtons::make('unlimited_cpu') + ->dehydrated() + ->label(trans('admin/server.cpu'))->inlineLabel()->inline() + ->afterStateUpdated(fn (Set $set) => $set('cpu', 0)) + ->formatStateUsing(fn (Get $get) => $get('cpu') == 0) + ->live() + ->stateCast(new BooleanStateCast(false, true)) + ->options([ + 1 => trans('admin/server.unlimited'), + 0 => trans('admin/server.limited'), + ]) + ->colors([ + 1 => 'primary', + 0 => 'warning', + ]) + ->columnSpan(2), + + TextInput::make('cpu') + ->dehydratedWhenHidden() + ->hidden(fn (Get $get) => $get('unlimited_cpu')) + ->label(trans('admin/server.cpu_limit'))->inlineLabel() + ->suffix('%') + ->hintIcon('tabler-question-mark', trans('admin/server.cpu_helper')) + ->required() + ->columnSpan(2) + ->numeric() + ->minValue(0), + ]), + Grid::make() + ->columns(4) + ->columnSpanFull() + ->schema([ + ToggleButtons::make('unlimited_mem') + ->dehydrated() + ->label(trans('admin/server.memory'))->inlineLabel()->inline() + ->afterStateUpdated(fn (Set $set) => $set('memory', 0)) + ->formatStateUsing(fn (Get $get) => $get('memory') == 0) + ->live() + ->stateCast(new BooleanStateCast(false, true)) + ->options([ + 1 => trans('admin/server.unlimited'), + 0 => trans('admin/server.limited'), + ]) + ->colors([ + 1 => 'primary', + 0 => 'warning', + ]) + ->columnSpan(2), + + TextInput::make('memory') + ->dehydratedWhenHidden() + ->hidden(fn (Get $get) => $get('unlimited_mem')) + ->label(trans('admin/server.memory_limit'))->inlineLabel() + ->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB') + ->hintIcon('tabler-question-mark', trans('admin/server.memory_helper')) + ->required() + ->columnSpan(2) + ->numeric() + ->minValue(0), + ]), + + Grid::make() + ->columns(4) + ->columnSpanFull() + ->schema([ + ToggleButtons::make('unlimited_disk') + ->dehydrated() + ->label(trans('admin/server.disk'))->inlineLabel()->inline() + ->live() + ->afterStateUpdated(fn (Set $set) => $set('disk', 0)) + ->formatStateUsing(fn (Get $get) => $get('disk') == 0) + ->stateCast(new BooleanStateCast(false, true)) + ->options([ + 1 => trans('admin/server.unlimited'), + 0 => trans('admin/server.limited'), + ]) + ->colors([ + 1 => 'primary', + 0 => 'warning', + ]) + ->columnSpan(2), + + TextInput::make('disk') + ->dehydratedWhenHidden() + ->hidden(fn (Get $get) => $get('unlimited_disk')) + ->label(trans('admin/server.disk_limit'))->inlineLabel() + ->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB') + ->required() + ->columnSpan(2) + ->numeric() + ->minValue(0), + ]), + ]), + + Fieldset::make(trans('admin/server.advanced_limits')) + ->columnSpanFull() + ->columns([ + 'default' => 1, + 'sm' => 2, + 'md' => 3, + 'lg' => 3, + ]) + ->schema([ + Grid::make() + ->columns(4) + ->columnSpanFull() + ->schema([ + Grid::make() + ->columns(4) + ->columnSpanFull() + ->schema([ + ToggleButtons::make('cpu_pinning') + ->label(trans('admin/server.cpu_pin'))->inlineLabel()->inline() + ->default(0) + ->afterStateUpdated(fn (Set $set) => $set('threads', [])) + ->formatStateUsing(fn (Get $get) => !empty($get('threads'))) + ->live() + ->stateCast(new BooleanStateCast(false, true)) + ->options([ + 0 => trans('admin/server.disabled'), + 1 => trans('admin/server.enabled'), + ]) + ->colors([ + 0 => 'success', + 1 => 'warning', + ]) + ->columnSpan(2), + + TagsInput::make('threads') + ->dehydratedWhenHidden() + ->hidden(fn (Get $get) => !$get('cpu_pinning')) + ->label(trans('admin/server.threads'))->inlineLabel() + ->required(fn (Get $get) => $get('cpu_pinning')) + ->columnSpan(2) + ->separator() + ->splitKeys([',']) + ->placeholder(trans('admin/server.pin_help')), + ]), + ToggleButtons::make('swap_support') + ->live() + ->label(trans('admin/server.swap'))->inlineLabel()->inline() + ->columnSpan(2) + ->afterStateUpdated(function ($state, Set $set) { + $value = match ($state) { + 'unlimited' => -1, + 'disabled' => 0, + 'limited' => 128, + default => throw new LogicException('Invalid state') + }; + + $set('swap', $value); + }) + ->formatStateUsing(function (Get $get) { + return match (true) { + $get('swap') > 0 => 'limited', + $get('swap') == 0 => 'disabled', + $get('swap') < 0 => 'unlimited', + default => throw new LogicException('Invalid state') + }; + }) + ->options([ + 'unlimited' => trans('admin/server.unlimited'), + 'limited' => trans('admin/server.limited'), + 'disabled' => trans('admin/server.disabled'), + ]) + ->colors([ + 'unlimited' => 'primary', + 'limited' => 'warning', + 'disabled' => 'danger', + ]), + + TextInput::make('swap') + ->dehydratedWhenHidden() + ->hidden(fn (Get $get) => match ($get('swap_support')) { + 'disabled', 'unlimited', true => true, + default => false, + }) + ->label(trans('admin/server.swap'))->inlineLabel() + ->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB') + ->minValue(-1) + ->columnSpan(2) + ->required() + ->integer(), + ]), + + Hidden::make('io') + ->helperText('The IO performance relative to other running containers') + ->label('Block IO Proportion'), + + Grid::make() + ->columns(4) + ->columnSpanFull() + ->schema([ + ToggleButtons::make('oom_killer') + ->dehydrated() + ->label(trans('admin/server.oom')) + ->formatStateUsing(fn ($state) => $state) + ->inlineLabel() + ->inline() + ->columnSpan(2) + ->stateCast(new BooleanStateCast(false, true)) + ->options([ + 0 => trans('admin/server.disabled'), + 1 => trans('admin/server.enabled'), + ]) + ->colors([ + 0 => 'success', + 1 => 'danger', + ]), + ]), + ]), + + Fieldset::make(trans('admin/server.feature_limits')) + ->inlineLabel() + ->columnSpanFull() + ->columns([ + 'default' => 1, + 'sm' => 2, + 'md' => 3, + 'lg' => 3, + ]) + ->schema([ + TextInput::make('allocation_limit') + ->label(trans('admin/server.allocations')) + ->suffixIcon('tabler-network') + ->required() + ->minValue(0) + ->numeric(), + TextInput::make('database_limit') + ->label(trans('admin/server.databases')) + ->suffixIcon('tabler-database') + ->required() + ->minValue(0) + ->numeric(), + TextInput::make('backup_limit') + ->label(trans('admin/server.backups')) + ->suffixIcon('tabler-copy-check') + ->required() + ->minValue(0) + ->numeric(), + ]), + Fieldset::make(trans('admin/server.docker_settings')) + ->columnSpanFull() + ->columns([ + 'default' => 1, + 'sm' => 2, + 'md' => 3, + 'lg' => 4, + ]) + ->schema([ + Select::make('select_image') + ->label(trans('admin/server.image_name')) + ->live() + ->afterStateUpdated(fn (Set $set, $state) => $set('image', $state)) + ->options(function ($state, Get $get, Set $set) { + $egg = Egg::query()->find($get('egg_id')); + $images = $egg->docker_images ?? []; + + $currentImage = $get('image'); + if (!$currentImage && $images) { + $defaultImage = collect($images)->first(); + $set('image', $defaultImage); + $set('select_image', $defaultImage); + } + + return array_flip($images) + ['ghcr.io/custom-image' => 'Custom Image']; + }) + ->selectablePlaceholder(false) + ->columnSpan([ + 'default' => 1, + 'sm' => 2, + 'md' => 3, + 'lg' => 2, + ]), + + TextInput::make('image') + ->label(trans('admin/server.image')) + ->required() + ->afterStateUpdated(function ($state, Get $get, Set $set) { + $egg = Egg::query()->find($get('egg_id')); + $images = $egg->docker_images ?? []; + + if (in_array($state, $images)) { + $set('select_image', $state); + } else { + $set('select_image', 'ghcr.io/custom-image'); + } + }) + ->placeholder(trans('admin/server.image_placeholder')) + ->columnSpan([ + 'default' => 1, + 'sm' => 2, + 'md' => 3, + 'lg' => 2, + ]), + + KeyValue::make('docker_labels') + ->live() + ->label(trans('admin/server.container_labels')) + ->keyLabel(trans('admin/server.title')) + ->valueLabel(trans('admin/server.description')) + ->columnSpanFull(), + ]), + ]), + Tab::make('egg') + ->label(trans('admin/server.egg')) + ->icon('tabler-egg') + ->columns([ + 'default' => 1, + 'sm' => 3, + 'md' => 3, + 'lg' => 5, + ]) + ->schema([ + Select::make('egg_id') + ->disabled() + ->prefixIcon('tabler-egg') + ->columnSpan([ + 'default' => 6, + 'sm' => 3, + 'md' => 3, + 'lg' => 4, + ]) + ->relationship('egg', 'name') + ->label(trans('admin/server.name')) + ->searchable() + ->preload() + ->required() + ->hintAction( + Action::make('change_egg') + ->label(trans('admin/server.change_egg')) + ->action(function (array $data, Server $server, EggChangerService $service, $livewire) { + $service->handle($server, $data['egg_id'], $data['keep_old_variables']); + + // Use redirect instead of fillForm to prevent server variables from duplicating + return redirect($livewire::getUrl(['record' => $server, 'tab' => 'egg::data::tab'])); + }) + ->schema(fn (Server $server) => [ + Select::make('egg_id') + ->label(trans('admin/server.new_egg')) + ->prefixIcon('tabler-egg') + ->options(fn () => Egg::all()->filter(fn (Egg $egg) => $egg->id !== $server->egg->id)->mapWithKeys(fn (Egg $egg) => [$egg->id => $egg->name])) + ->searchable() + ->preload() + ->required(), + Toggle::make('keep_old_variables') + ->label(trans('admin/server.keep_old_variables')) + ->default(true), + ]) + ), + + ToggleButtons::make('skip_scripts') + ->label(trans('admin/server.install_script')) + ->inline() + ->columnSpan([ + 'default' => 6, + 'sm' => 1, + 'md' => 1, + 'lg' => 2, + ]) + ->stateCast(new BooleanStateCast(false, true)) + ->options([ + 0 => trans('admin/server.yes'), + 1 => trans('admin/server.skip'), + ]) + ->colors([ + 0 => 'primary', + 1 => 'danger', + ]) + ->icons([ + 0 => 'tabler-code', + 1 => 'tabler-code-off', + ]) + ->required(), + + Hidden::make('previewing') + ->default(false), + + Select::make('select_startup') + ->label(trans('admin/server.startup_cmd')) + ->required() + ->live() + ->options(function (Get $get) { + $egg = Egg::find($get('egg_id')); + + return array_flip($egg->startup_commands ?? []) + ['custom' => 'Custom Startup']; + }) + ->formatStateUsing(fn (Server $server) => in_array($server->startup, $server->egg->startup_commands) ? $server->startup : 'custom') + ->afterStateUpdated(function (Set $set, string $state) { + if ($state !== 'custom') { + $set('startup', $state); + } + $set('previewing', false); + }) + ->selectablePlaceholder(false) + ->columnSpanFull() + ->hintAction(PreviewStartupAction::make('preview')), + + Textarea::make('startup') + ->hiddenLabel() + ->required() + ->live() + ->autosize() + ->afterStateUpdated(function ($state, Get $get, Set $set) { + $egg = Egg::find($get('egg_id')); + $startups = $egg->startup_commands ?? []; + + if (in_array($state, $startups)) { + $set('select_startup', $state); + } else { + $set('select_startup', 'custom'); + } + }) + ->placeholder(trans('admin/server.startup_placeholder')) + ->columnSpanFull(), + + Repeater::make('server_variables') + ->hiddenLabel() + ->relationship('serverVariables', function (Builder $query, $livewire) { + /** @var Server $server */ + $server = $livewire->getRecord(); + + $server->ensureVariablesExist(); + + return $query->orderByPowerJoins('variable.sort'); + }) + ->grid() + ->mutateRelationshipDataBeforeSaveUsing(function (array $data): array { + $data['variable_value'] ??= ''; + + return $data; + }) + ->reorderable(false)->addable(false)->deletable(false) + ->schema([ + StartupVariable::make('variable_value') + ->fromRecord(), + ]) + ->columnSpan(6), + ]), + Tab::make('mounts') + ->label(trans('admin/server.mounts')) + ->icon('tabler-layers-linked') + ->schema(fn (Get $get) => [ + ServerResource::getMountCheckboxList($get), + ]), + Tab::make('actions') + ->label(trans('admin/server.actions')) + ->icon('tabler-settings') + ->schema([ + Fieldset::make(trans('admin/server.actions')) + ->columnSpanFull() + ->columns([ + 'default' => 1, + 'sm' => 2, + 'md' => 2, + 'lg' => 6, + ]) + ->schema([ + Grid::make() + ->columnSpan(3) + ->schema([ + Actions::make([ + Action::make('toggleInstall') + ->label(trans('admin/server.toggle_install')) + ->disabled(fn (Server $server) => $server->isSuspended()) + ->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->isFailedInstall()) { + try { + $reinstallService->handle($server); + + Notification::make() + ->title(trans('admin/server.notifications.reinstall_started')) + ->success() + ->send(); + + } catch (Exception) { + Notification::make() + ->title(trans('admin/server.notifications.reinstall_failed')) + ->body(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name])) + ->danger() + ->send(); + } + } else { + try { + $toggleService->handle($server); + + Notification::make() + ->title(trans('admin/server.notifications.install_toggled')) + ->success() + ->send(); + + } catch (Exception $exception) { + Notification::make() + ->title(trans('admin/server.notifications.install_toggle_failed')) + ->body($exception->getMessage()) + ->danger() + ->send(); + } + } + }), + ])->fullWidth(), + ToggleButtons::make('install_help') + ->hiddenLabel() + ->hint(trans('admin/server.toggle_install_help')), + ]), + Grid::make() + ->columnSpan(3) + ->schema([ + Actions::make([ + Action::make('toggleSuspend') + ->label(trans('admin/server.suspend')) + ->color('warning') + ->hidden(fn (Server $server) => $server->isSuspended()) + ->action(function (SuspensionService $suspensionService, Server $server) { + try { + $suspensionService->handle($server, SuspendAction::Suspend); + + Notification::make() + ->success() + ->title(trans('admin/server.notifications.server_suspended')) + ->send(); + + } catch (Exception) { + Notification::make() + ->warning() + ->title(trans('admin/server.notifications.server_suspension')) + ->body(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name])) + ->send(); + } + }), + Action::make('toggleUnsuspend') + ->label(trans('admin/server.unsuspend')) + ->color('success') + ->hidden(fn (Server $server) => !$server->isSuspended()) + ->action(function (SuspensionService $suspensionService, Server $server) { + try { + $suspensionService->handle($server, SuspendAction::Unsuspend); + + Notification::make() + ->success() + ->title(trans('admin/server.notifications.server_unsuspended')) + ->send(); + + } catch (Exception) { + Notification::make() + ->warning() + ->title(trans('admin/server.notifications.server_suspension')) + ->body(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name])) + ->send(); + } + }), + ])->fullWidth(), + ToggleButtons::make('server_suspend') + ->hiddenLabel() + ->hidden(fn (Server $server) => $server->isSuspended()) + ->hint(trans('admin/server.notifications.server_suspend_help')), + ToggleButtons::make('server_unsuspend') + ->hiddenLabel() + ->hidden(fn (Server $server) => !$server->isSuspended()) + ->hint(trans('admin/server.notifications.server_unsuspend_help')), + ]), + Grid::make() + ->columnSpan(3) + ->schema([ + Actions::make([ + Action::make('transfer') + ->label(trans('admin/server.transfer')) + ->disabled(fn (Server $server) => user()?->accessibleNodes()->count() <= 1 || $server->isInConflictState()) + ->modalHeading(trans('admin/server.transfer')) + ->schema(static::transferServer()) + ->action(function (TransferServerService $transfer, Server $server, $data) { + try { + $transfer->handle($server, Arr::get($data, 'node_id'), Arr::get($data, 'allocation_id'), Arr::get($data, 'allocation_additional', [])); + + Notification::make() + ->title(trans('admin/server.notifications.transfer_started')) + ->success() + ->send(); + } catch (Exception $exception) { + Notification::make() + ->title(trans('admin/server.notifications.transfer_failed')) + ->body($exception->getMessage()) + ->danger() + ->send(); + } + }), + ])->fullWidth(), + ToggleButtons::make('server_transfer') + ->hiddenLabel() + ->hint(new HtmlString(trans('admin/server.transfer_help'))), + ]), + Grid::make() + ->columnSpan(3) + ->schema([ + Actions::make([ + Action::make('reinstall') + ->label(trans('admin/server.reinstall')) + ->color('danger') + ->requiresConfirmation() + ->modalHeading(trans('admin/server.reinstall_modal_heading')) + ->modalDescription(trans('admin/server.reinstall_modal_description')) + ->disabled(fn (Server $server) => $server->isSuspended()) + ->action(function (ReinstallServerService $service, Server $server) { + try { + $service->handle($server); + + Notification::make() + ->title(trans('admin/server.notifications.reinstall_started')) + ->success() + ->send(); + } catch (Exception) { + Notification::make() + ->title(trans('admin/server.notifications.reinstall_failed')) + ->body(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name])) + ->danger() + ->send(); + } + }), + ])->fullWidth(), + ToggleButtons::make('server_reinstall') + ->hiddenLabel() + ->hint(trans('admin/server.reinstall_help')), + ]), + ]), + ]), + ]), + ]); + } + /** @return Component[] + * @throws Exception + */ + protected static function transferServer(): array + { + return [ + Select::make('node_id') + ->label(trans('admin/server.node')) + ->prefixIcon('tabler-server-2') + ->selectablePlaceholder(false) + ->default(fn (Server $server) => user()?->accessibleNodes()->whereNot('id', $server->node->id)->first()?->id) + ->required() + ->live() + ->options(fn (Server $server) => user()?->accessibleNodes()->whereNot('id', $server->node->id)->pluck('name', 'id')->all()), + Select::make('allocation_id') + ->label(trans('admin/server.primary_allocation')) + ->disabled(fn (Get $get, Server $server) => !$get('node_id') || !$server->allocation_id) + ->required(fn (Server $server) => $server->allocation_id) + ->prefixIcon('tabler-network') + ->options(fn (Get $get) => Allocation::where('node_id', $get('node_id'))->whereNull('server_id')->get()->mapWithKeys(fn (Allocation $allocation) => [$allocation->id => $allocation->address])) + ->searchable(['ip', 'port', 'ip_alias']) + ->placeholder(trans('admin/server.select_allocation')), + Select::make('allocation_additional') + ->label(trans('admin/server.additional_allocations')) + ->disabled(fn (Get $get, Server $server) => !$get('node_id') || $server->allocations->count() <= 1) + ->multiple() + ->minItems(fn (Select $select) => $select->getMaxItems()) + ->maxItems(fn (Select $select, Server $server) => $select->isDisabled() ? null : $server->allocations->count() - 1) + ->prefixIcon('tabler-network') + ->required(fn (Server $server) => $server->allocations->count() > 1) + ->options(fn (Get $get) => Allocation::where('node_id', $get('node_id'))->whereNull('server_id')->when($get('allocation_id'), fn ($query) => $query->whereNot('id', $get('allocation_id')))->get()->mapWithKeys(fn (Allocation $allocation) => [$allocation->id => $allocation->address])) + ->searchable(['ip', 'port', 'ip_alias']) + ->placeholder(trans('admin/server.select_additional')), + ]; + } + + /** @return array */ + public function getDefaultHeaderActions(): array + { + /** @var Server $server */ + $server = $this->getRecord(); + + $canForceDelete = cache()->get("servers.$server->uuid.canForceDelete", false); + + return [ + Action::make('Delete') + ->color('danger') + ->label(trans('filament-actions::delete.single.label')) + ->modalHeading(trans('filament-actions::delete.single.modal.heading', ['label' => $server->name])) + ->modalSubmitActionLabel(trans('filament-actions::delete.single.label')) + ->requiresConfirmation() + ->action(function (Server $server, ServerDeletionService $service) { + try { + $service->handle($server); + + return redirect(ListServers::getUrl(panel: 'admin')); + } catch (ConnectionException) { + cache()->put("servers.$server->uuid.canForceDelete", true, now()->addMinutes(5)); + + return Notification::make() + ->title(trans('admin/server.notifications.error_server_delete')) + ->body(trans('admin/server.notifications.error_server_delete_body')) + ->color('warning') + ->icon('tabler-database') + ->warning() + ->send(); + } + }) + ->hidden(fn () => $canForceDelete) + ->authorize(fn (Server $server) => user()?->can('delete server', $server)) + ->icon('tabler-trash') + ->iconButton()->iconSize(IconSize::ExtraLarge), + Action::make('ForceDelete') + ->color('danger') + ->label(trans('filament-actions::force-delete.single.label')) + ->modalHeading(trans('filament-actions::force-delete.single.modal.heading', ['label' => $server->name])) + ->modalSubmitActionLabel(trans('filament-actions::force-delete.single.label')) + ->requiresConfirmation() + ->action(function (Server $server, ServerDeletionService $service) { + try { + $service->withForce()->handle($server); + + return redirect(ListServers::getUrl(panel: 'admin')); + } catch (ConnectionException) { + return cache()->forget("servers.$server->uuid.canForceDelete"); + } + }) + ->visible(fn () => $canForceDelete) + ->authorize(fn (Server $server) => user()?->can('delete server', $server)), + Action::make('console') + ->label(trans('admin/server.console')) + ->icon('tabler-terminal') + ->iconButton()->iconSize(IconSize::ExtraLarge) + ->url(fn (Server $server) => Console::getUrl(panel: 'server', tenant: $server)), + $this->getSaveFormAction()->formId('form') + ->iconButton()->iconSize(IconSize::ExtraLarge) + ->icon('tabler-device-floppy'), + ]; + + } + + protected function getFormActions(): array + { + return []; + } + + protected function mutateFormDataBeforeSave(array $data): array + { + if (!isset($data['description'])) { + $data['description'] = ''; + } + + unset($data['docker'], $data['status'], $data['allocation_id']); + + return $data; + } + + protected function afterSave(): void + { + /** @var Server $server */ + $server = $this->getRecord(); + + $changed = collect($server->getChanges())->except(['updated_at', 'name', 'owner_id', 'condition', 'description', 'external_id', 'tags', 'cpu_pinning', 'allocation_limit', 'database_limit', 'backup_limit', 'skip_scripts'])->all(); + + try { + if ($changed) { + $this->daemonServerRepository->setServer($server)->sync(); + } + parent::getSavedNotification()?->send(); + } catch (ConnectionException) { + Notification::make() + ->title(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name])) + ->body(trans('admin/server.notifications.error_connecting_description')) + ->color('warning') + ->icon('tabler-database') + ->warning() + ->send(); + } + } + + protected function getSavedNotification(): ?Notification + { + return null; + } + + public function getRelationManagers(): array + { + return [ + AllocationsRelationManager::class, + DatabasesRelationManager::class, + ]; + } /** * @throws Exception */ @@ -96,6 +1275,7 @@ public static function getDefaultPages(): array 'index' => ListServers::route('/'), 'create' => CreateServer::route('/create'), 'edit' => EditServer::route('/{record}/edit'), + 'view' => ViewServer::route('/{record}'), ]; } From 0fbcba1fbad5f43e42cdeb96ac1897118c204cdf Mon Sep 17 00:00:00 2001 From: JoanFo1456 <161775222+JoanFo1456@users.noreply.github.com> Date: Sat, 13 Dec 2025 15:41:19 +0000 Subject: [PATCH 02/10] Node view page --- .../Admin/Resources/Nodes/NodeResource.php | 770 ++++++++++++++++- .../Admin/Resources/Nodes/Pages/EditNode.php | 786 +----------------- .../Admin/Resources/Nodes/Pages/ListNodes.php | 4 + .../Admin/Resources/Nodes/Pages/ViewNode.php | 45 + 4 files changed, 836 insertions(+), 769 deletions(-) create mode 100644 app/Filament/Admin/Resources/Nodes/Pages/ViewNode.php diff --git a/app/Filament/Admin/Resources/Nodes/NodeResource.php b/app/Filament/Admin/Resources/Nodes/NodeResource.php index efc4a3f7fb..e568d0cf25 100644 --- a/app/Filament/Admin/Resources/Nodes/NodeResource.php +++ b/app/Filament/Admin/Resources/Nodes/NodeResource.php @@ -3,18 +3,56 @@ namespace App\Filament\Admin\Resources\Nodes; use App\Enums\CustomizationKey; +use App\Filament\Admin\Resources\Eggs\RelationManagers\ServersRelationManager; use App\Filament\Admin\Resources\Nodes\Pages\CreateNode; use App\Filament\Admin\Resources\Nodes\Pages\EditNode; use App\Filament\Admin\Resources\Nodes\Pages\ListNodes; +use App\Filament\Admin\Resources\Nodes\Pages\ViewNode; use App\Filament\Admin\Resources\Nodes\RelationManagers\AllocationsRelationManager; -use App\Filament\Admin\Resources\Nodes\RelationManagers\ServersRelationManager; use App\Models\Node; +use App\Repositories\Daemon\DaemonSystemRepository; +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 App\Traits\Filament\CanCustomizePages; use App\Traits\Filament\CanCustomizeRelations; -use Filament\Resources\Pages\PageRegistration; -use Filament\Resources\RelationManagers\RelationManager; -use Filament\Resources\Resource; +use Exception; +use Filament\Actions\Action; +use Filament\Actions\DeleteAction; +use Filament\Forms\Components\Hidden; +use Filament\Forms\Components\Slider; +use Filament\Forms\Components\Slider\Enums\PipsMode; +use Filament\Forms\Components\TagsInput; +use Filament\Forms\Components\Textarea; +use Filament\Forms\Components\TextInput; +use Filament\Forms\Components\ToggleButtons; +use Filament\Infolists\Components\CodeEntry; +use Filament\Infolists\Components\TextEntry; +use Filament\Notifications\Notification; +use Filament\Resources\Pages\EditRecord; +use Filament\Schemas\Components\Actions; +use Filament\Schemas\Components\Fieldset; +use Filament\Schemas\Components\Grid; +use Filament\Schemas\Components\Section; +use Filament\Schemas\Components\StateCasts\BooleanStateCast; +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\Schemas\Components\View; +use Filament\Schemas\Schema; +use Filament\Support\Enums\Alignment; +use Filament\Support\Enums\IconSize; +use Filament\Support\RawJs; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Http\Client\ConnectionException; +use Illuminate\Support\Facades\Http; +use Illuminate\Support\HtmlString; +use Phiki\Grammar\Grammar; +use Filament\Resources\Resource; +use Throwable; class NodeResource extends Resource { @@ -67,6 +105,7 @@ public static function getDefaultPages(): array return [ 'index' => ListNodes::route('/'), 'create' => CreateNode::route('/create'), + 'view' => ViewNode::route('/{record}'), 'edit' => EditNode::route('/{record}/edit'), ]; } @@ -77,4 +116,727 @@ public static function getEloquentQuery(): Builder return $query->whereIn('id', user()?->accessibleNodes()->pluck('id')); } + + /** + * @throws Throwable + */ + public static function schema(Schema $schema): Schema + { + return $schema->components([ + Tabs::make('Tabs') + ->columns([ + 'default' => 2, + 'sm' => 3, + 'md' => 3, + 'lg' => 4, + ]) + ->persistTabInQueryString() + ->columnSpanFull() + ->tabs([ + Tab::make('overview') + ->label(trans('admin/node.tabs.overview')) + ->icon('tabler-chart-area-line-filled') + ->columns([ + 'default' => 4, + 'sm' => 2, + 'md' => 4, + 'lg' => 4, + ]) + ->schema([ + Fieldset::make() + ->label(trans('admin/node.node_info')) + ->columns(4) + ->columnSpanFull() + ->schema([ + TextEntry::make('wings_version') + ->label(trans('admin/node.wings_version')) + ->state(fn (Node $node, SoftwareVersionService $versionService) => ($node->systemInformation()['version'] ?? trans('admin/node.unknown')) . ' ' . trans('admin/node.latest', ['version' => $versionService->latestWingsVersion()])), + TextEntry::make('cpu_threads') + ->label(trans('admin/node.cpu_threads')) + ->state(fn (Node $node) => $node->systemInformation()['cpu_count'] ?? 0), + TextEntry::make('architecture') + ->label(trans('admin/node.architecture')) + ->state(fn (Node $node) => $node->systemInformation()['architecture'] ?? trans('admin/node.unknown')), + TextEntry::make('kernel') + ->label(trans('admin/node.kernel')) + ->state(fn (Node $node) => $node->systemInformation()['kernel_version'] ?? trans('admin/node.unknown')), + ]), + View::make('filament.components.node-cpu-chart') + ->columnSpan([ + 'default' => 4, + 'sm' => 1, + 'md' => 2, + 'lg' => 2, + ]), + View::make('filament.components.node-memory-chart') + ->columnSpan([ + 'default' => 4, + 'sm' => 1, + 'md' => 2, + 'lg' => 2, + ]), + View::make('filament.components.node-storage-chart') + ->columnSpanFull(), + ]), + Tab::make('basic_settings') + ->label(trans('admin/node.tabs.basic_settings')) + ->icon('tabler-server') + ->schema([ + TextInput::make('fqdn') + ->columnSpan(2) + ->required() + ->autofocus() + ->live(debounce: 1500) + ->rules(Node::getRulesForField('fqdn')) + ->prohibited(fn ($state) => is_ip($state) && request()->isSecure()) + ->label(fn ($state) => is_ip($state) ? trans('admin/node.ip_address') : trans('admin/node.domain')) + ->placeholder(fn ($state) => is_ip($state) ? '192.168.1.1' : 'node.example.com') + ->helperText(function ($state) { + if (is_ip($state)) { + if (request()->isSecure()) { + return trans('admin/node.fqdn_help'); + } + + return ''; + } + + return trans('admin/node.error'); + }) + ->hintColor('danger') + ->hint(function ($state) { + if (is_ip($state) && request()->isSecure()) { + return trans('admin/node.ssl_ip'); + } + + return ''; + }) + ->afterStateUpdated(function (Set $set, ?string $state) { + $set('dns', null); + $set('ip', null); + + [$subdomain] = str($state)->explode('.', 2); + if (!is_numeric($subdomain)) { + $set('name', $subdomain); + } + + if (!$state || is_ip($state)) { + $set('dns', null); + + return; + } + + $ip = get_ip_from_hostname($state); + if ($ip) { + $set('dns', true); + + $set('ip', $ip); + } else { + $set('dns', false); + } + }) + ->maxLength(255), + TextInput::make('ip') + ->disabled() + ->hidden(), + ToggleButtons::make('dns') + ->label(trans('admin/node.dns')) + ->helperText(trans('admin/node.dns_help')) + ->disabled() + ->inline() + ->default(null) + ->hint(fn (Get $get) => $get('ip')) + ->hintColor('success') + ->stateCast(new BooleanStateCast(false, true)) + ->options([ + 1 => trans('admin/node.valid'), + 0 => trans('admin/node.invalid'), + ]) + ->colors([ + 1 => 'success', + 0 => 'danger', + ]) + ->columnSpan(1), + 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) + ->required() + ->integer(), + TextInput::make('name') + ->label(trans('admin/node.display_name')) + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 1, + 'lg' => 2, + ]) + ->required() + ->maxLength(100), + Hidden::make('scheme'), + Hidden::make('behind_proxy'), + ToggleButtons::make('connection') + ->label(trans('admin/node.ssl')) + ->columnSpan(1) + ->inline() + ->helperText(function (Get $get) { + if (request()->isSecure()) { + return new HtmlString(trans('admin/node.panel_on_ssl')); + } + + if (is_ip($get('fqdn'))) { + return trans('admin/node.ssl_help'); + } + + return ''; + }) + ->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', + ]) + ->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('advanced_settings') + ->label(trans('admin/node.tabs.advanced_settings')) + ->columns([ + 'default' => 1, + 'sm' => 1, + 'md' => 4, + 'lg' => 6, + ]) + ->icon('tabler-server-cog') + ->schema([ + TextInput::make('id') + ->label(trans('admin/node.node_id')) + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 2, + 'lg' => 1, + ]) + ->disabled(), + TextInput::make('uuid') + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 2, + 'lg' => 2, + ]) + ->label(trans('admin/node.node_uuid')) + ->hintCopy() + ->disabled(), + TagsInput::make('tags') + ->label(trans('admin/node.tags')) + ->placeholder('') + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 2, + 'lg' => 2, + ]), + TextInput::make('upload_size') + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 2, + 'lg' => 1, + ]) + ->label(trans('admin/node.upload_limit')) + ->hintIcon('tabler-question-mark', trans('admin/node.upload_limit_help.0') . trans('admin/node.upload_limit_help.1')) + ->numeric()->required() + ->minValue(1) + ->maxValue(1024) + ->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB'), + TextInput::make('daemon_sftp') + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 1, + 'lg' => 3, + ]) + ->label(trans('admin/node.sftp_port')) + ->minValue(1) + ->maxValue(65535) + ->default(2022) + ->required() + ->integer(), + TextInput::make('daemon_sftp_alias') + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 1, + 'lg' => 3, + ]) + ->label(trans('admin/node.sftp_alias')) + ->helperText(trans('admin/node.sftp_alias_help')), + ToggleButtons::make('public') + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 1, + 'lg' => 3, + ]) + ->label(trans('admin/node.use_for_deploy')) + ->inline() + ->stateCast(new BooleanStateCast(false, true)) + ->options([ + 1 => trans('admin/node.yes'), + 0 => trans('admin/node.no'), + ]) + ->colors([ + 1 => 'success', + 0 => 'danger', + ]), + ToggleButtons::make('maintenance_mode') + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 1, + 'lg' => 3, + ]) + ->label(trans('admin/node.maintenance_mode')) + ->inline() + ->hintIcon('tabler-question-mark', trans('admin/node.maintenance_mode_help')) + ->stateCast(new BooleanStateCast(false, true)) + ->options([ + 1 => trans('admin/node.enabled'), + 0 => trans('admin/node.disabled'), + ]) + ->colors([ + 1 => 'danger', + 0 => 'success', + ]), + Grid::make() + ->columns([ + 'default' => 1, + 'sm' => 1, + 'md' => 3, + 'lg' => 6, + ]) + ->columnSpanFull() + ->schema([ + ToggleButtons::make('unlimited_mem') + ->dehydrated() + ->label(trans('admin/node.memory'))->inlineLabel()->inline() + ->afterStateUpdated(fn (Set $set) => $set('memory', 0)) + ->afterStateUpdated(fn (Set $set) => $set('memory_overallocate', 0)) + ->formatStateUsing(fn (Get $get) => $get('memory') == 0) + ->live() + ->stateCast(new BooleanStateCast(false, true)) + ->options([ + 1 => trans('admin/node.unlimited'), + 0 => trans('admin/node.limited'), + ]) + ->colors([ + 1 => 'primary', + 0 => 'warning', + ]) + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 1, + 'lg' => 2, + ]), + TextInput::make('memory') + ->dehydratedWhenHidden() + ->hidden(fn (Get $get) => $get('unlimited_mem')) + ->label(trans('admin/node.memory_limit'))->inlineLabel() + ->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB') + ->required() + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 1, + 'lg' => 2, + ]) + ->numeric() + ->minValue(0), + TextInput::make('memory_overallocate') + ->dehydratedWhenHidden() + ->label(trans('admin/node.overallocate'))->inlineLabel() + ->required() + ->hidden(fn (Get $get) => $get('unlimited_mem')) + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 1, + 'lg' => 2, + ]) + ->numeric() + ->minValue(-1) + ->maxValue(100) + ->suffix('%'), + ]), + Grid::make() + ->columnSpanFull() + ->columns([ + 'default' => 1, + 'sm' => 1, + 'md' => 3, + 'lg' => 6, + ]) + ->schema([ + ToggleButtons::make('unlimited_disk') + ->dehydrated() + ->label(trans('admin/node.disk'))->inlineLabel()->inline() + ->live() + ->afterStateUpdated(fn (Set $set) => $set('disk', 0)) + ->afterStateUpdated(fn (Set $set) => $set('disk_overallocate', 0)) + ->formatStateUsing(fn (Get $get) => $get('disk') == 0) + ->stateCast(new BooleanStateCast(false, true)) + ->options([ + 1 => trans('admin/node.unlimited'), + 0 => trans('admin/node.limited'), + ]) + ->colors([ + 1 => 'primary', + 0 => 'warning', + ]) + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 1, + 'lg' => 2, + ]), + TextInput::make('disk') + ->dehydratedWhenHidden() + ->hidden(fn (Get $get) => $get('unlimited_disk')) + ->label(trans('admin/node.disk_limit'))->inlineLabel() + ->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB') + ->required() + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 1, + 'lg' => 2, + ]) + ->numeric() + ->minValue(0), + TextInput::make('disk_overallocate') + ->dehydratedWhenHidden() + ->hidden(fn (Get $get) => $get('unlimited_disk')) + ->label(trans('admin/node.overallocate'))->inlineLabel() + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 1, + 'lg' => 2, + ]) + ->required() + ->numeric() + ->minValue(-1) + ->maxValue(100) + ->suffix('%'), + ]), + Grid::make() + ->columns(6) + ->columnSpanFull() + ->schema([ + ToggleButtons::make('unlimited_cpu') + ->dehydrated() + ->label(trans('admin/node.cpu'))->inlineLabel()->inline() + ->live() + ->afterStateUpdated(fn (Set $set) => $set('cpu', 0)) + ->afterStateUpdated(fn (Set $set) => $set('cpu_overallocate', 0)) + ->formatStateUsing(fn (Get $get) => $get('cpu') == 0) + ->stateCast(new BooleanStateCast(false, true)) + ->options([ + 1 => trans('admin/node.unlimited'), + 0 => trans('admin/node.limited'), + ]) + ->colors([ + 1 => 'primary', + 0 => 'warning', + ]) + ->columnSpan(2), + TextInput::make('cpu') + ->dehydratedWhenHidden() + ->hidden(fn (Get $get) => $get('unlimited_cpu')) + ->label(trans('admin/node.cpu_limit'))->inlineLabel() + ->suffix('%') + ->required() + ->columnSpan(2) + ->numeric() + ->minValue(0), + TextInput::make('cpu_overallocate') + ->dehydratedWhenHidden() + ->hidden(fn (Get $get) => $get('unlimited_cpu')) + ->label(trans('admin/node.overallocate'))->inlineLabel() + ->columnSpan(2) + ->required() + ->numeric() + ->minValue(-1) + ->maxValue(100) + ->suffix('%'), + ]), + ]), + Tab::make('config_file') + ->label(trans('admin/node.tabs.config_file')) + ->icon('tabler-code') + ->hidden(fn (Node $node) => !user()?->can('edit node', $node)) + ->schema([ + TextEntry::make('instructions') + ->label(trans('admin/node.instructions')) + ->columnSpanFull() + ->state(new HtmlString(trans('admin/node.instructions_help'))), + CodeEntry::make('config') + ->label('/etc/pelican/config.yml') + ->grammar(Grammar::Yaml) + ->state(fn (Node $node) => $node->getYamlConfiguration()) + ->copyable() + ->disabled() + ->columnSpanFull(), + Grid::make() + ->columns() + ->columnSpanFull() + ->schema([ + Actions::make([ + Action::make('autoDeploy') + ->label(trans('admin/node.auto_deploy')) + ->color('primary') + ->modalHeading(trans('admin/node.auto_deploy')) + ->icon('tabler-rocket') + ->modalSubmitAction(false) + ->modalCancelAction(false) + ->modalFooterActionsAlignment(Alignment::Center) + ->schema([ + ToggleButtons::make('docker') + ->label(trans('admin/node.auto_label')) + ->live() + ->helperText(trans('admin/node.auto_question')) + ->inline() + ->default(false) + ->afterStateUpdated(fn (bool $state, NodeAutoDeployService $service, Node $node, Set $set) => $set('generatedToken', $service->handle(request(), $node, $state))) + ->stateCast(new BooleanStateCast(false, true)) + ->options([ + 0 => trans('admin/node.standalone'), + 1 => trans('admin/node.docker'), + ]) + ->colors([ + 0 => 'primary', + 1 => 'success', + ]) + ->columnSpan(1), + Textarea::make('generatedToken') + ->label(trans('admin/node.auto_command')) + ->readOnly() + ->autosize() + ->hintCopy() + ->formatStateUsing(fn (NodeAutoDeployService $service, Node $node, Set $set, Get $get) => $set('generatedToken', $service->handle(request(), $node, $get('docker')))), + ]) + ->mountUsing(function (Schema $schema) { + $schema->fill(); + }), + ])->fullWidth(), + Actions::make([ + Action::make('resetKey') + ->label(trans('admin/node.reset_token')) + ->color('danger') + ->requiresConfirmation() + ->modalHeading(trans('admin/node.reset_token')) + ->modalDescription(trans('admin/node.reset_help')) + ->action(function (Node $node, NodeUpdateService $nodeUpdateService, $livewire) { + try { + $nodeUpdateService->handle($node, [], true); + } catch (Exception) { + Notification::make() + ->title(trans('admin/node.error_connecting', ['node' => $node->name])) + ->body(trans('admin/node.error_connecting_description')) + ->color('warning') + ->icon('tabler-database') + ->warning() + ->send(); + + } + Notification::make()->success()->title(trans('admin/node.token_reset'))->send(); + $livewire->refresh(); + }), + ])->fullWidth(), + ]), + ]), + Tab::make('diagnostics') + ->hidden(fn (Node $node) => !user()?->can('edit node', $node)) + ->label(trans('admin/node.tabs.diagnostics')) + ->icon('tabler-heart-search') + ->schema([ + Section::make('diag') + ->heading(trans('admin/node.tabs.diagnostics')) + ->columnSpanFull() + ->columns(4) + ->disabled(fn (Get $get) => $get('pulled')) + ->headerActions([ + Action::make('pull') + ->label(trans('admin/node.diagnostics.pull')) + ->icon('tabler-cloud-download')->iconButton()->iconSize(IconSize::ExtraLarge) + ->hidden(fn (Get $get) => $get('pulled')) + ->action(function (Get $get, Set $set, Node $node, DaemonSystemRepository $daemonSystemRepository) { + $includeEndpoints = $get('include_endpoints') ?? true; + $includeLogs = $get('include_logs') ?? true; + $logLines = $get('log_lines') ?? 200; + + try { + $response = $daemonSystemRepository->setNode($node)->getDiagnostics($logLines, $includeEndpoints, $includeLogs); + + if ($response->status() === 404) { + Notification::make() + ->title(trans('admin/node.diagnostics.404')) + ->warning() + ->send(); + + return; + } + + $set('pulled', true); + $set('uploaded', false); + $set('log', $response->body()); + + Notification::make() + ->title(trans('admin/node.diagnostics.logs_pulled')) + ->success() + ->send(); + } catch (ConnectionException $e) { + Notification::make() + ->title(trans('admin/node.error_connecting', ['node' => $node->name])) + ->body($e->getMessage()) + ->danger() + ->send(); + + } + }), + Action::make('upload') + ->label(trans('admin/node.diagnostics.upload')) + ->visible(fn (Get $get) => $get('pulled') ?? false) + ->icon('tabler-cloud-upload')->iconButton()->iconSize(IconSize::ExtraLarge) + ->action(function (Get $get, Set $set) { + try { + $response = Http::asMultipart()->post('https://logs.pelican.dev', [ + [ + 'name' => 'c', + 'contents' => $get('log'), + ], + [ + 'name' => 'e', + 'contents' => '14d', + ], + ]); + + if ($response->failed()) { + Notification::make() + ->title(trans('admin/node.diagnostics.upload_failed')) + ->body(fn () => $response->status() . ' - ' . $response->body()) + ->danger() + ->send(); + + return; + } + + $data = $response->json(); + $url = $data['url']; + + Notification::make() + ->title(trans('admin/node.diagnostics.logs_uploaded')) + ->body("{$url}") + ->success() + ->actions([ + Action::make('viewLogs') + ->label(trans('admin/node.diagnostics.view_logs')) + ->url($url) + ->openUrlInNewTab(true), + ]) + ->persistent() + ->send(); + $set('log', $url); + $set('pulled', false); + $set('uploaded', true); + + } catch (\Exception $e) { + Notification::make() + ->title(trans('admin/node.diagnostics.upload_failed')) + ->body($e->getMessage()) + ->danger() + ->send(); + } + }), + Action::make('clear') + ->label(trans('admin/node.diagnostics.clear')) + ->visible(fn (Get $get) => $get('pulled') ?? false) + ->icon('tabler-trash')->iconButton()->iconSize(IconSize::ExtraLarge)->color('danger') + ->action(function (Get $get, Set $set, $livewire) { + $set('pulled', false); + $set('uploaded', false); + $set('log', null); + $livewire->refresh(); + } + ), + ]) + ->schema([ + ToggleButtons::make('include_endpoints') + ->hintIcon('tabler-question-mark')->inline() + ->hintIconTooltip(trans('admin/node.diagnostics.include_endpoints_hint')) + ->formatStateUsing(fn () => 1) + ->boolean(), + ToggleButtons::make('include_logs') + ->live() + ->hintIcon('tabler-question-mark')->inline() + ->hintIconTooltip(trans('admin/node.diagnostics.include_logs_hint')) + ->formatStateUsing(fn () => 1) + ->boolean(), + Slider::make('log_lines') + ->columnSpan(2) + ->hiddenLabel() + ->live() + ->tooltips(RawJs::make(<<<'JS' + `${$value} lines` + JS)) + ->visible(fn (Get $get) => $get('include_logs')) + ->range(minValue: 100, maxValue: 500) + ->pips(PipsMode::Steps, density: 10) + ->step(50) + ->formatStateUsing(fn () => 200) + ->fillTrack(), + Hidden::make('pulled'), + Hidden::make('uploaded'), + ]), + Textarea::make('log') + ->hiddenLabel() + ->columnSpanFull() + ->rows(35) + ->visible(fn (Get $get) => ($get('pulled') ?? false) || ($get('uploaded') ?? false)), + ]), + ]), + ]); + } } diff --git a/app/Filament/Admin/Resources/Nodes/Pages/EditNode.php b/app/Filament/Admin/Resources/Nodes/Pages/EditNode.php index dc273d72fa..4619543cb8 100644 --- a/app/Filament/Admin/Resources/Nodes/Pages/EditNode.php +++ b/app/Filament/Admin/Resources/Nodes/Pages/EditNode.php @@ -5,44 +5,17 @@ use App\Filament\Admin\Resources\Nodes\NodeResource; use App\Models\Node; use App\Repositories\Daemon\DaemonSystemRepository; -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\Forms\Components\Hidden; -use Filament\Forms\Components\Slider; -use Filament\Forms\Components\Slider\Enums\PipsMode; -use Filament\Forms\Components\TagsInput; -use Filament\Forms\Components\Textarea; -use Filament\Forms\Components\TextInput; -use Filament\Forms\Components\ToggleButtons; -use Filament\Infolists\Components\CodeEntry; -use Filament\Infolists\Components\TextEntry; use Filament\Notifications\Notification; use Filament\Resources\Pages\EditRecord; -use Filament\Schemas\Components\Actions; -use Filament\Schemas\Components\Fieldset; -use Filament\Schemas\Components\Grid; -use Filament\Schemas\Components\Section; -use Filament\Schemas\Components\StateCasts\BooleanStateCast; -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\Schemas\Components\View; use Filament\Schemas\Schema; -use Filament\Support\Enums\Alignment; use Filament\Support\Enums\IconSize; -use Filament\Support\RawJs; use Illuminate\Http\Client\ConnectionException; -use Illuminate\Support\Facades\Http; -use Illuminate\Support\HtmlString; -use Phiki\Grammar\Grammar; -use Throwable; class EditNode extends EditRecord { @@ -52,7 +25,6 @@ class EditNode extends EditRecord protected static string $resource = NodeResource::class; private DaemonSystemRepository $daemonSystemRepository; - private NodeUpdateService $nodeUpdateService; public function boot(DaemonSystemRepository $daemonSystemRepository, NodeUpdateService $nodeUpdateService): void @@ -61,725 +33,9 @@ public function boot(DaemonSystemRepository $daemonSystemRepository, NodeUpdateS $this->nodeUpdateService = $nodeUpdateService; } - /** - * @throws Throwable - */ public function form(Schema $schema): Schema { - return $schema->components([ - Tabs::make('Tabs') - ->columns([ - 'default' => 2, - 'sm' => 3, - 'md' => 3, - 'lg' => 4, - ]) - ->persistTabInQueryString() - ->columnSpanFull() - ->tabs([ - Tab::make('overview') - ->label(trans('admin/node.tabs.overview')) - ->icon('tabler-chart-area-line-filled') - ->columns([ - 'default' => 4, - 'sm' => 2, - 'md' => 4, - 'lg' => 4, - ]) - ->schema([ - Fieldset::make() - ->label(trans('admin/node.node_info')) - ->columns(4) - ->columnSpanFull() - ->schema([ - TextEntry::make('wings_version') - ->label(trans('admin/node.wings_version')) - ->state(fn (Node $node, SoftwareVersionService $versionService) => ($node->systemInformation()['version'] ?? trans('admin/node.unknown')) . ' ' . trans('admin/node.latest', ['version' => $versionService->latestWingsVersion()])), - TextEntry::make('cpu_threads') - ->label(trans('admin/node.cpu_threads')) - ->state(fn (Node $node) => $node->systemInformation()['cpu_count'] ?? 0), - TextEntry::make('architecture') - ->label(trans('admin/node.architecture')) - ->state(fn (Node $node) => $node->systemInformation()['architecture'] ?? trans('admin/node.unknown')), - TextEntry::make('kernel') - ->label(trans('admin/node.kernel')) - ->state(fn (Node $node) => $node->systemInformation()['kernel_version'] ?? trans('admin/node.unknown')), - ]), - View::make('filament.components.node-cpu-chart') - ->columnSpan([ - 'default' => 4, - 'sm' => 1, - 'md' => 2, - 'lg' => 2, - ]), - View::make('filament.components.node-memory-chart') - ->columnSpan([ - 'default' => 4, - 'sm' => 1, - 'md' => 2, - 'lg' => 2, - ]), - View::make('filament.components.node-storage-chart') - ->columnSpanFull(), - ]), - Tab::make('basic_settings') - ->label(trans('admin/node.tabs.basic_settings')) - ->icon('tabler-server') - ->schema([ - TextInput::make('fqdn') - ->columnSpan(2) - ->required() - ->autofocus() - ->live(debounce: 1500) - ->rules(Node::getRulesForField('fqdn')) - ->prohibited(fn ($state) => is_ip($state) && request()->isSecure()) - ->label(fn ($state) => is_ip($state) ? trans('admin/node.ip_address') : trans('admin/node.domain')) - ->placeholder(fn ($state) => is_ip($state) ? '192.168.1.1' : 'node.example.com') - ->helperText(function ($state) { - if (is_ip($state)) { - if (request()->isSecure()) { - return trans('admin/node.fqdn_help'); - } - - return ''; - } - - return trans('admin/node.error'); - }) - ->hintColor('danger') - ->hint(function ($state) { - if (is_ip($state) && request()->isSecure()) { - return trans('admin/node.ssl_ip'); - } - - return ''; - }) - ->afterStateUpdated(function (Set $set, ?string $state) { - $set('dns', null); - $set('ip', null); - - [$subdomain] = str($state)->explode('.', 2); - if (!is_numeric($subdomain)) { - $set('name', $subdomain); - } - - if (!$state || is_ip($state)) { - $set('dns', null); - - return; - } - - $ip = get_ip_from_hostname($state); - if ($ip) { - $set('dns', true); - - $set('ip', $ip); - } else { - $set('dns', false); - } - }) - ->maxLength(255), - TextInput::make('ip') - ->disabled() - ->hidden(), - ToggleButtons::make('dns') - ->label(trans('admin/node.dns')) - ->helperText(trans('admin/node.dns_help')) - ->disabled() - ->inline() - ->default(null) - ->hint(fn (Get $get) => $get('ip')) - ->hintColor('success') - ->stateCast(new BooleanStateCast(false, true)) - ->options([ - 1 => trans('admin/node.valid'), - 0 => trans('admin/node.invalid'), - ]) - ->colors([ - 1 => 'success', - 0 => 'danger', - ]) - ->columnSpan(1), - 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) - ->required() - ->integer(), - TextInput::make('name') - ->label(trans('admin/node.display_name')) - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 1, - 'lg' => 2, - ]) - ->required() - ->maxLength(100), - Hidden::make('scheme'), - Hidden::make('behind_proxy'), - ToggleButtons::make('connection') - ->label(trans('admin/node.ssl')) - ->columnSpan(1) - ->inline() - ->helperText(function (Get $get) { - if (request()->isSecure()) { - return new HtmlString(trans('admin/node.panel_on_ssl')); - } - - if (is_ip($get('fqdn'))) { - return trans('admin/node.ssl_help'); - } - - return ''; - }) - ->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', - ]) - ->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('advanced_settings') - ->label(trans('admin/node.tabs.advanced_settings')) - ->columns([ - 'default' => 1, - 'sm' => 1, - 'md' => 4, - 'lg' => 6, - ]) - ->icon('tabler-server-cog') - ->schema([ - TextInput::make('id') - ->label(trans('admin/node.node_id')) - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 2, - 'lg' => 1, - ]) - ->disabled(), - TextInput::make('uuid') - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 2, - 'lg' => 2, - ]) - ->label(trans('admin/node.node_uuid')) - ->hintCopy() - ->disabled(), - TagsInput::make('tags') - ->label(trans('admin/node.tags')) - ->placeholder('') - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 2, - 'lg' => 2, - ]), - TextInput::make('upload_size') - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 2, - 'lg' => 1, - ]) - ->label(trans('admin/node.upload_limit')) - ->hintIcon('tabler-question-mark', trans('admin/node.upload_limit_help.0') . trans('admin/node.upload_limit_help.1')) - ->numeric()->required() - ->minValue(1) - ->maxValue(1024) - ->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB'), - TextInput::make('daemon_sftp') - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 1, - 'lg' => 3, - ]) - ->label(trans('admin/node.sftp_port')) - ->minValue(1) - ->maxValue(65535) - ->default(2022) - ->required() - ->integer(), - TextInput::make('daemon_sftp_alias') - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 1, - 'lg' => 3, - ]) - ->label(trans('admin/node.sftp_alias')) - ->helperText(trans('admin/node.sftp_alias_help')), - ToggleButtons::make('public') - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 1, - 'lg' => 3, - ]) - ->label(trans('admin/node.use_for_deploy')) - ->inline() - ->stateCast(new BooleanStateCast(false, true)) - ->options([ - 1 => trans('admin/node.yes'), - 0 => trans('admin/node.no'), - ]) - ->colors([ - 1 => 'success', - 0 => 'danger', - ]), - ToggleButtons::make('maintenance_mode') - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 1, - 'lg' => 3, - ]) - ->label(trans('admin/node.maintenance_mode')) - ->inline() - ->hintIcon('tabler-question-mark', trans('admin/node.maintenance_mode_help')) - ->stateCast(new BooleanStateCast(false, true)) - ->options([ - 1 => trans('admin/node.enabled'), - 0 => trans('admin/node.disabled'), - ]) - ->colors([ - 1 => 'danger', - 0 => 'success', - ]), - Grid::make() - ->columns([ - 'default' => 1, - 'sm' => 1, - 'md' => 3, - 'lg' => 6, - ]) - ->columnSpanFull() - ->schema([ - ToggleButtons::make('unlimited_mem') - ->dehydrated() - ->label(trans('admin/node.memory'))->inlineLabel()->inline() - ->afterStateUpdated(fn (Set $set) => $set('memory', 0)) - ->afterStateUpdated(fn (Set $set) => $set('memory_overallocate', 0)) - ->formatStateUsing(fn (Get $get) => $get('memory') == 0) - ->live() - ->stateCast(new BooleanStateCast(false, true)) - ->options([ - 1 => trans('admin/node.unlimited'), - 0 => trans('admin/node.limited'), - ]) - ->colors([ - 1 => 'primary', - 0 => 'warning', - ]) - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 1, - 'lg' => 2, - ]), - TextInput::make('memory') - ->dehydratedWhenHidden() - ->hidden(fn (Get $get) => $get('unlimited_mem')) - ->label(trans('admin/node.memory_limit'))->inlineLabel() - ->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB') - ->required() - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 1, - 'lg' => 2, - ]) - ->numeric() - ->minValue(0), - TextInput::make('memory_overallocate') - ->dehydratedWhenHidden() - ->label(trans('admin/node.overallocate'))->inlineLabel() - ->required() - ->hidden(fn (Get $get) => $get('unlimited_mem')) - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 1, - 'lg' => 2, - ]) - ->numeric() - ->minValue(-1) - ->maxValue(100) - ->suffix('%'), - ]), - Grid::make() - ->columnSpanFull() - ->columns([ - 'default' => 1, - 'sm' => 1, - 'md' => 3, - 'lg' => 6, - ]) - ->schema([ - ToggleButtons::make('unlimited_disk') - ->dehydrated() - ->label(trans('admin/node.disk'))->inlineLabel()->inline() - ->live() - ->afterStateUpdated(fn (Set $set) => $set('disk', 0)) - ->afterStateUpdated(fn (Set $set) => $set('disk_overallocate', 0)) - ->formatStateUsing(fn (Get $get) => $get('disk') == 0) - ->stateCast(new BooleanStateCast(false, true)) - ->options([ - 1 => trans('admin/node.unlimited'), - 0 => trans('admin/node.limited'), - ]) - ->colors([ - 1 => 'primary', - 0 => 'warning', - ]) - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 1, - 'lg' => 2, - ]), - TextInput::make('disk') - ->dehydratedWhenHidden() - ->hidden(fn (Get $get) => $get('unlimited_disk')) - ->label(trans('admin/node.disk_limit'))->inlineLabel() - ->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB') - ->required() - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 1, - 'lg' => 2, - ]) - ->numeric() - ->minValue(0), - TextInput::make('disk_overallocate') - ->dehydratedWhenHidden() - ->hidden(fn (Get $get) => $get('unlimited_disk')) - ->label(trans('admin/node.overallocate'))->inlineLabel() - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 1, - 'lg' => 2, - ]) - ->required() - ->numeric() - ->minValue(-1) - ->maxValue(100) - ->suffix('%'), - ]), - Grid::make() - ->columns(6) - ->columnSpanFull() - ->schema([ - ToggleButtons::make('unlimited_cpu') - ->dehydrated() - ->label(trans('admin/node.cpu'))->inlineLabel()->inline() - ->live() - ->afterStateUpdated(fn (Set $set) => $set('cpu', 0)) - ->afterStateUpdated(fn (Set $set) => $set('cpu_overallocate', 0)) - ->formatStateUsing(fn (Get $get) => $get('cpu') == 0) - ->stateCast(new BooleanStateCast(false, true)) - ->options([ - 1 => trans('admin/node.unlimited'), - 0 => trans('admin/node.limited'), - ]) - ->colors([ - 1 => 'primary', - 0 => 'warning', - ]) - ->columnSpan(2), - TextInput::make('cpu') - ->dehydratedWhenHidden() - ->hidden(fn (Get $get) => $get('unlimited_cpu')) - ->label(trans('admin/node.cpu_limit'))->inlineLabel() - ->suffix('%') - ->required() - ->columnSpan(2) - ->numeric() - ->minValue(0), - TextInput::make('cpu_overallocate') - ->dehydratedWhenHidden() - ->hidden(fn (Get $get) => $get('unlimited_cpu')) - ->label(trans('admin/node.overallocate'))->inlineLabel() - ->columnSpan(2) - ->required() - ->numeric() - ->minValue(-1) - ->maxValue(100) - ->suffix('%'), - ]), - ]), - Tab::make('config_file') - ->label(trans('admin/node.tabs.config_file')) - ->icon('tabler-code') - ->schema([ - TextEntry::make('instructions') - ->label(trans('admin/node.instructions')) - ->columnSpanFull() - ->state(new HtmlString(trans('admin/node.instructions_help'))), - CodeEntry::make('config') - ->label('/etc/pelican/config.yml') - ->grammar(Grammar::Yaml) - ->state(fn (Node $node) => $node->getYamlConfiguration()) - ->copyable() - ->disabled() - ->columnSpanFull(), - Grid::make() - ->columns() - ->columnSpanFull() - ->schema([ - Actions::make([ - Action::make('autoDeploy') - ->label(trans('admin/node.auto_deploy')) - ->color('primary') - ->modalHeading(trans('admin/node.auto_deploy')) - ->icon('tabler-rocket') - ->modalSubmitAction(false) - ->modalCancelAction(false) - ->modalFooterActionsAlignment(Alignment::Center) - ->schema([ - ToggleButtons::make('docker') - ->label(trans('admin/node.auto_label')) - ->live() - ->helperText(trans('admin/node.auto_question')) - ->inline() - ->default(false) - ->afterStateUpdated(fn (bool $state, NodeAutoDeployService $service, Node $node, Set $set) => $set('generatedToken', $service->handle(request(), $node, $state))) - ->stateCast(new BooleanStateCast(false, true)) - ->options([ - 0 => trans('admin/node.standalone'), - 1 => trans('admin/node.docker'), - ]) - ->colors([ - 0 => 'primary', - 1 => 'success', - ]) - ->columnSpan(1), - Textarea::make('generatedToken') - ->label(trans('admin/node.auto_command')) - ->readOnly() - ->autosize() - ->hintCopy() - ->formatStateUsing(fn (NodeAutoDeployService $service, Node $node, Set $set, Get $get) => $set('generatedToken', $service->handle(request(), $node, $get('docker')))), - ]) - ->mountUsing(function (Schema $schema) { - $schema->fill(); - }), - ])->fullWidth(), - Actions::make([ - Action::make('resetKey') - ->label(trans('admin/node.reset_token')) - ->color('danger') - ->requiresConfirmation() - ->modalHeading(trans('admin/node.reset_token')) - ->modalDescription(trans('admin/node.reset_help')) - ->action(function (Node $node) { - try { - $this->nodeUpdateService->handle($node, [], true); - } catch (Exception) { - Notification::make() - ->title(trans('admin/node.error_connecting', ['node' => $node->name])) - ->body(trans('admin/node.error_connecting_description')) - ->color('warning') - ->icon('tabler-database') - ->warning() - ->send(); - - } - Notification::make()->success()->title(trans('admin/node.token_reset'))->send(); - $this->fillForm(); - }), - ])->fullWidth(), - ]), - ]), - Tab::make('diagnostics') - ->label(trans('admin/node.tabs.diagnostics')) - ->icon('tabler-heart-search') - ->schema([ - Section::make('diag') - ->heading(trans('admin/node.tabs.diagnostics')) - ->columnSpanFull() - ->columns(4) - ->disabled(fn (Get $get) => $get('pulled')) - ->headerActions([ - Action::make('pull') - ->label(trans('admin/node.diagnostics.pull')) - ->icon('tabler-cloud-download')->iconButton()->iconSize(IconSize::ExtraLarge) - ->hidden(fn (Get $get) => $get('pulled')) - ->action(function (Get $get, Set $set, Node $node) { - $includeEndpoints = $get('include_endpoints') ?? true; - $includeLogs = $get('include_logs') ?? true; - $logLines = $get('log_lines') ?? 200; - - try { - $response = $this->daemonSystemRepository->setNode($node)->getDiagnostics($logLines, $includeEndpoints, $includeLogs); - - if ($response->status() === 404) { - Notification::make() - ->title(trans('admin/node.diagnostics.404')) - ->warning() - ->send(); - - return; - } - - $set('pulled', true); - $set('uploaded', false); - $set('log', $response->body()); - - Notification::make() - ->title(trans('admin/node.diagnostics.logs_pulled')) - ->success() - ->send(); - } catch (ConnectionException $e) { - Notification::make() - ->title(trans('admin/node.error_connecting', ['node' => $node->name])) - ->body($e->getMessage()) - ->danger() - ->send(); - - } - }), - Action::make('upload') - ->label(trans('admin/node.diagnostics.upload')) - ->visible(fn (Get $get) => $get('pulled') ?? false) - ->icon('tabler-cloud-upload')->iconButton()->iconSize(IconSize::ExtraLarge) - ->action(function (Get $get, Set $set) { - try { - $response = Http::asMultipart()->post('https://logs.pelican.dev', [ - [ - 'name' => 'c', - 'contents' => $get('log'), - ], - [ - 'name' => 'e', - 'contents' => '14d', - ], - ]); - - if ($response->failed()) { - Notification::make() - ->title(trans('admin/node.diagnostics.upload_failed')) - ->body(fn () => $response->status() . ' - ' . $response->body()) - ->danger() - ->send(); - - return; - } - - $data = $response->json(); - $url = $data['url']; - - Notification::make() - ->title(trans('admin/node.diagnostics.logs_uploaded')) - ->body("{$url}") - ->success() - ->actions([ - Action::make('viewLogs') - ->label(trans('admin/node.diagnostics.view_logs')) - ->url($url) - ->openUrlInNewTab(true), - ]) - ->persistent() - ->send(); - $set('log', $url); - $set('pulled', false); - $set('uploaded', true); - - } catch (\Exception $e) { - Notification::make() - ->title(trans('admin/node.diagnostics.upload_failed')) - ->body($e->getMessage()) - ->danger() - ->send(); - } - }), - Action::make('clear') - ->label(trans('admin/node.diagnostics.clear')) - ->visible(fn (Get $get) => $get('pulled') ?? false) - ->icon('tabler-trash')->iconButton()->iconSize(IconSize::ExtraLarge)->color('danger') - ->action(function (Get $get, Set $set) { - $set('pulled', false); - $set('uploaded', false); - $set('log', null); - $this->refresh(); - } - ), - ]) - ->schema([ - ToggleButtons::make('include_endpoints') - ->hintIcon('tabler-question-mark')->inline() - ->hintIconTooltip(trans('admin/node.diagnostics.include_endpoints_hint')) - ->formatStateUsing(fn () => 1) - ->boolean(), - ToggleButtons::make('include_logs') - ->live() - ->hintIcon('tabler-question-mark')->inline() - ->hintIconTooltip(trans('admin/node.diagnostics.include_logs_hint')) - ->formatStateUsing(fn () => 1) - ->boolean(), - Slider::make('log_lines') - ->columnSpan(2) - ->hiddenLabel() - ->live() - ->tooltips(RawJs::make(<<<'JS' - `${$value} lines` - JS)) - ->visible(fn (Get $get) => $get('include_logs')) - ->range(minValue: 100, maxValue: 500) - ->pips(PipsMode::Steps, density: 10) - ->step(50) - ->formatStateUsing(fn () => 200) - ->fillTrack(), - Hidden::make('pulled'), - Hidden::make('uploaded'), - ]), - Textarea::make('log') - ->hiddenLabel() - ->columnSpanFull() - ->rows(35) - ->visible(fn (Get $get) => ($get('pulled') ?? false) || ($get('uploaded') ?? false)), - ]), - ]), - ]); + return NodeResource::schema($schema); } protected function mutateFormDataBeforeFill(array $data): array @@ -799,12 +55,21 @@ protected function mutateFormDataBeforeFill(array $data): array return $data; } + protected function mutateFormDataBeforeSave(array $data): array + { + if (!$data['behind_proxy']) { + $data['daemon_listen'] = $data['daemon_connect']; + } + + return $data; + } + protected function getFormActions(): array { return []; } - /** @return array */ + /** @return array */ protected function getDefaultHeaderActions(): array { return [ @@ -812,27 +77,21 @@ protected function getDefaultHeaderActions(): array ->disabled(fn (Node $node) => $node->servers()->count() > 0) ->label(fn (Node $node) => $node->servers()->count() > 0 ? trans('admin/node.node_has_servers') : trans('filament-actions::delete.single.label')) ->iconButton()->iconSize(IconSize::ExtraLarge), - $this->getSaveFormAction()->formId('form') + Action::make('save') + ->label(trans('filament-actions::edit.single.label')) + ->formId('form') ->iconButton()->iconSize(IconSize::ExtraLarge) - ->icon('tabler-device-floppy'), + ->icon('tabler-device-floppy') + ->action('save'), ]; } - protected function mutateFormDataBeforeSave(array $data): array - { - if (!$data['behind_proxy']) { - $data['daemon_listen'] = $data['daemon_connect']; - } - - return $data; - } - protected function afterSave(): void { - $this->fillForm(); + $this->refresh(); /** @var Node $node */ - $node = $this->record; + $node = $this->getRecord(); $changed = collect($node->getChanges())->except(['updated_at', 'name', 'tags', 'public', 'maintenance_mode', 'memory', 'memory_overallocate', 'disk', 'disk_overallocate', 'cpu', 'cpu_overallocate'])->all(); @@ -840,7 +99,7 @@ protected function afterSave(): void if ($changed) { $this->daemonSystemRepository->setNode($node)->update($node); } - parent::getSavedNotification()?->send(); + $this->getSavedNotification()?->send(); } catch (ConnectionException) { Notification::make() ->title(trans('admin/node.error_connecting', ['node' => $node->name])) @@ -851,18 +110,15 @@ protected function afterSave(): void ->send(); } } - protected function getSavedNotification(): ?Notification { return null; } - - protected function getColumnSpan(): ?int - { + protected function getColumnSpan() { return null; } - protected function getColumnStart(): ?int + protected function getColumnStart(): ?int { return null; } diff --git a/app/Filament/Admin/Resources/Nodes/Pages/ListNodes.php b/app/Filament/Admin/Resources/Nodes/Pages/ListNodes.php index 9d4fd952fb..bef4a6fc23 100644 --- a/app/Filament/Admin/Resources/Nodes/Pages/ListNodes.php +++ b/app/Filament/Admin/Resources/Nodes/Pages/ListNodes.php @@ -28,6 +28,10 @@ class ListNodes extends ListRecords public function table(Table $table): Table { return $table + ->recordUrl(fn (Node $node) => user()?->can('edit node', $node) + ? NodeResource::getUrl('edit', ['record' => $node]) + : NodeResource::getUrl('view', ['record' => $node]) + ) ->searchable(false) ->checkIfRecordIsSelectableUsing(fn (Node $node) => $node->servers_count <= 0) ->columns([ diff --git a/app/Filament/Admin/Resources/Nodes/Pages/ViewNode.php b/app/Filament/Admin/Resources/Nodes/Pages/ViewNode.php new file mode 100644 index 0000000000..1d0a36a3ac --- /dev/null +++ b/app/Filament/Admin/Resources/Nodes/Pages/ViewNode.php @@ -0,0 +1,45 @@ + Date: Sat, 13 Dec 2025 16:04:37 +0000 Subject: [PATCH 03/10] Fixed a bunch of permissions missing & egg view --- .../Admin/Resources/Eggs/EggResource.php | 434 ++++++++++++++++++ .../Admin/Resources/Eggs/Pages/EditEgg.php | 397 +--------------- .../Admin/Resources/Eggs/Pages/ListEggs.php | 4 + .../Admin/Resources/Eggs/Pages/ViewEgg.php | 22 + .../Admin/Resources/Nodes/NodeResource.php | 4 +- .../Admin/Resources/Nodes/Pages/ListNodes.php | 2 +- .../Resources/Servers/Pages/ListServers.php | 2 +- 7 files changed, 466 insertions(+), 399 deletions(-) create mode 100644 app/Filament/Admin/Resources/Eggs/Pages/ViewEgg.php diff --git a/app/Filament/Admin/Resources/Eggs/EggResource.php b/app/Filament/Admin/Resources/Eggs/EggResource.php index 861288d322..f604e13045 100644 --- a/app/Filament/Admin/Resources/Eggs/EggResource.php +++ b/app/Filament/Admin/Resources/Eggs/EggResource.php @@ -6,6 +6,7 @@ use App\Filament\Admin\Resources\Eggs\Pages\CreateEgg; use App\Filament\Admin\Resources\Eggs\Pages\EditEgg; use App\Filament\Admin\Resources\Eggs\Pages\ListEggs; +use App\Filament\Admin\Resources\Eggs\Pages\ViewEgg; use App\Filament\Admin\Resources\Eggs\RelationManagers\ServersRelationManager; use App\Models\Egg; use App\Traits\Filament\CanCustomizePages; @@ -13,6 +14,42 @@ use Filament\Resources\Pages\PageRegistration; use Filament\Resources\RelationManagers\RelationManager; use Filament\Resources\Resource; +use Filament\Schemas\Schema; + +use App\Filament\Components\Actions\ExportEggAction; +use App\Filament\Components\Actions\ImportEggAction; +use App\Filament\Components\Forms\Fields\CopyFrom; +use App\Models\EggVariable; +use App\Traits\Filament\CanCustomizeHeaderActions; +use App\Traits\Filament\CanCustomizeHeaderWidgets; +use Exception; +use Filament\Actions\Action; +use Filament\Actions\ActionGroup; +use Filament\Actions\DeleteAction; +use Filament\Forms\Components\Checkbox; +use Filament\Forms\Components\CodeEditor; +use Filament\Forms\Components\FileUpload; +use Filament\Forms\Components\Hidden; +use Filament\Forms\Components\KeyValue; +use Filament\Forms\Components\Repeater; +use Filament\Forms\Components\Select; +use Filament\Forms\Components\TagsInput; +use Filament\Forms\Components\Textarea; +use Filament\Forms\Components\TextInput; +use Filament\Forms\Components\Toggle; +use Filament\Infolists\Components\TextEntry; +use Filament\Notifications\Notification; +use Filament\Resources\Pages\EditRecord; +use Filament\Schemas\Components\Fieldset; +use Filament\Schemas\Components\Flex; +use Filament\Schemas\Components\Grid; +use Filament\Schemas\Components\Image; +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\IconSize; +use Illuminate\Validation\Rules\Unique; class EggResource extends Resource { @@ -62,7 +99,403 @@ public static function getDefaultRelations(): array ServersRelationManager::class, ]; } + /** + * @throws Exception + */ + public static function schema(Schema $schema): Schema + { + return $schema + ->components([ + Tabs::make()->tabs([ + Tab::make('configuration') + ->label(trans('admin/egg.tabs.configuration')) + ->columns(['default' => 2, 'sm' => 2, 'md' => 4, 'lg' => 6]) + ->icon('tabler-egg') + ->schema([ + Grid::make(2) + ->columnSpan(1) + ->schema([ + Image::make('', '') + ->hidden(fn ($record) => !$record->image) + ->url(fn ($record) => $record->image) + ->alt('') + ->alignJustify() + ->imageSize(150) + ->columnSpanFull(), + Flex::make([ + Action::make('uploadImage') + ->iconButton() + ->iconSize(IconSize::Large) + ->icon('tabler-photo-up') + ->modal() + ->modalHeading('') + ->modalSubmitActionLabel(trans('admin/egg.import.import_image')) + ->schema([ + Tabs::make() + ->contained(false) + ->tabs([ + Tab::make(trans('admin/egg.import.url')) + ->schema([ + Hidden::make('base64Image'), + TextInput::make('image_url') + ->label(trans('admin/egg.import.image_url')) + ->reactive() + ->autocomplete(false) + ->debounce(500) + ->afterStateUpdated(function ($state, Set $set) { + if (!$state) { + $set('image_url_error', null); + + return; + } + + try { + if (!filter_var($state, FILTER_VALIDATE_URL)) { + throw new \Exception(trans('admin/egg.import.invalid_url')); + } + + $allowedExtensions = [ + 'png' => 'image/png', + 'jpg' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'gif' => 'image/gif', + 'webp' => 'image/webp', + 'svg' => 'image/svg+xml', + ]; + + $extension = strtolower(pathinfo(parse_url($state, PHP_URL_PATH), PATHINFO_EXTENSION)); + + if (!array_key_exists($extension, $allowedExtensions)) { + throw new \Exception(trans('admin/egg.import.unsupported_format', ['format' => implode(', ', $allowedExtensions)])); + } + + $host = parse_url($state, PHP_URL_HOST); + $ip = gethostbyname($host); + + if ( + filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false + ) { + throw new \Exception(trans('admin/egg.import.no_local_ip')); + } + + $context = stream_context_create([ + 'http' => ['timeout' => 3], + 'https' => [ + 'timeout' => 3, + 'verify_peer' => true, + 'verify_peer_name' => true, + ], + ]); + + $imageContent = @file_get_contents($state, false, $context, 0, 1048576); // 1024KB + + if (!$imageContent) { + throw new \Exception(trans('admin/egg.import.image_error')); + } + + if (strlen($imageContent) >= 1048576) { + throw new \Exception(trans('admin/egg.import.image_too_large')); + } + $mimeType = $allowedExtensions[$extension]; + $base64 = 'data:' . $mimeType . ';base64,' . base64_encode($imageContent); + + $set('base64Image', $base64); + $set('image_url_error', null); + + } catch (\Exception $e) { + $set('image_url_error', $e->getMessage()); + $set('base64Image', null); + } + }), + TextEntry::make('image_url_error') + ->hiddenLabel() + ->visible(fn ($get) => $get('image_url_error') !== null) + ->afterStateHydrated(fn ($set, $get) => $get('image_url_error')), + Image::make(fn (Get $get) => $get('image_url'), '') + ->imageSize(150) + ->visible(fn ($get) => $get('image_url') && !$get('image_url_error')) + ->alignCenter(), + ]), + Tab::make(trans('admin/egg.import.file')) + ->schema([ + FileUpload::make('image') + ->hiddenLabel() + ->previewable() + ->openable(false) + ->downloadable(false) + ->maxSize(1024) + ->maxFiles(1) + ->columnSpanFull() + ->alignCenter() + ->imageEditor() + ->saveUploadedFileUsing(function ($file, Set $set) { + $base64 = "data:{$file->getMimeType()};base64,". base64_encode(file_get_contents($file->getRealPath())); + $set('base64Image', $base64); + + return $base64; + }), + ]), + ]), + ]) + ->action(function (array $data, $record): void { + $base64 = $data['base64Image'] ?? null; + + if (empty($base64) && !empty($data['image'])) { + $base64 = $data['image']; + } + + if (!empty($base64)) { + $record->update([ + 'image' => $base64, + ]); + + Notification::make() + ->title(trans('admin/egg.import.image_updated')) + ->success() + ->send(); + + $record->refresh(); + } else { + Notification::make() + ->title(trans('admin/egg.import.no_image')) + ->warning() + ->send(); + } + }), + Action::make('deleteImage') + ->visible(fn ($record) => $record->image) + ->label('') + ->icon('tabler-trash') + ->iconButton() + ->iconSize(IconSize::Large) + ->color('danger') + ->action(function ($record) { + + $record->update([ + 'image' => null, + ]); + + Notification::make() + ->title(trans('admin/egg.import.image_deleted')) + ->success() + ->send(); + + $record->refresh(); + }), + ]), + ]), + TextInput::make('name') + ->label(trans('admin/egg.name')) + ->required() + ->maxLength(255) + ->columnSpan(['default' => 2, 'sm' => 2, 'md' => 3, 'lg' => 2]) + ->helperText(trans('admin/egg.name_help')), + Textarea::make('description') + ->label(trans('admin/egg.description')) + ->rows(3) + ->columnSpan(['default' => 2, 'sm' => 2, 'md' => 4, 'lg' => 3]) + ->helperText(trans('admin/egg.description_help')), + TextInput::make('id') + ->label(trans('admin/egg.egg_id')) + ->columnSpan(1) + ->disabled(), + TextInput::make('uuid') + ->label(trans('admin/egg.egg_uuid')) + ->disabled() + ->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2]) + ->helperText(trans('admin/egg.uuid_help')), + TextInput::make('author') + ->label(trans('admin/egg.author')) + ->required() + ->maxLength(255) + ->email() + ->disabled() + ->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2]) + ->helperText(trans('admin/egg.author_help_edit')), + Toggle::make('force_outgoing_ip') + ->inline(false) + ->label(trans('admin/egg.force_ip')) + ->columnSpan(1) + ->hintIcon('tabler-question-mark', trans('admin/egg.force_ip_help')), + KeyValue::make('startup_commands') + ->label(trans('admin/egg.startup_commands')) + ->live() + ->columnSpanFull() + ->required() + ->addActionLabel(trans('admin/egg.add_startup')) + ->keyLabel(trans('admin/egg.startup_name')) + ->valueLabel(trans('admin/egg.startup_command')) + ->helperText(trans('admin/egg.startup_help')), + TagsInput::make('file_denylist') + ->label(trans('admin/egg.file_denylist')) + ->placeholder('denied-file.txt') + ->helperText(trans('admin/egg.file_denylist_help')) + ->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]), + TextInput::make('update_url') + ->label(trans('admin/egg.update_url')) + ->url() + ->hintIcon('tabler-question-mark', trans('admin/egg.update_url_help')) + ->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]), + TagsInput::make('features') + ->label(trans('admin/egg.features')) + ->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]), + Hidden::make('script_is_privileged') + ->helperText('The docker images available to servers using this egg.'), + TagsInput::make('tags') + ->label(trans('admin/egg.tags')) + ->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]), + KeyValue::make('docker_images') + ->label(trans('admin/egg.docker_images')) + ->live() + ->columnSpanFull() + ->required() + ->addActionLabel(trans('admin/egg.add_image')) + ->keyLabel(trans('admin/egg.docker_name')) + ->valueLabel(trans('admin/egg.docker_uri')) + ->helperText(trans('admin/egg.docker_help')), + ]), + Tab::make('process_management') + ->label(trans('admin/egg.tabs.process_management')) + ->columns() + ->icon('tabler-server-cog') + ->schema([ + CopyFrom::make('copy_process_from') + ->process(), + TextInput::make('config_stop') + ->label(trans('admin/egg.stop_command')) + ->maxLength(255) + ->helperText(trans('admin/egg.stop_command_help')), + Textarea::make('config_startup')->rows(10)->json() + ->label(trans('admin/egg.start_config')) + ->helperText(trans('admin/egg.start_config_help')), + Textarea::make('config_files')->rows(10)->json() + ->label(trans('admin/egg.config_files')) + ->helperText(trans('admin/egg.config_files_help')), + Textarea::make('config_logs')->rows(10)->json() + ->label(trans('admin/egg.log_config')) + ->helperText(trans('admin/egg.log_config_help')), + ]), + Tab::make('egg_variables') + ->label(trans('admin/egg.tabs.egg_variables')) + ->columnSpanFull() + ->icon('tabler-variable') + ->schema([ + Repeater::make('variables') + ->hiddenLabel() + ->grid() + ->relationship('variables') + ->reorderable() + ->collapsible()->collapsed() + ->orderColumn() + ->addActionLabel(trans('admin/egg.add_new_variable')) + ->itemLabel(fn (array $state) => $state['name']) + ->mutateRelationshipDataBeforeCreateUsing(function (array $data): array { + $data['default_value'] ??= ''; + $data['description'] ??= ''; + $data['rules'] ??= []; + $data['user_viewable'] ??= ''; + $data['user_editable'] ??= ''; + + return $data; + }) + ->mutateRelationshipDataBeforeSaveUsing(function (array $data): array { + $data['default_value'] ??= ''; + $data['description'] ??= ''; + $data['rules'] ??= []; + $data['user_viewable'] ??= ''; + $data['user_editable'] ??= ''; + + return $data; + }) + ->schema([ + TextInput::make('name') + ->label(trans('admin/egg.name')) + ->live() + ->debounce(750) + ->maxLength(255) + ->columnSpanFull() + ->afterStateUpdated(fn (Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString())) + ->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id'))) + ->validationMessages([ + 'unique' => trans('admin/egg.error_unique'), + ]) + ->required(), + Textarea::make('description')->label(trans('admin/egg.description'))->columnSpanFull(), + TextInput::make('env_variable') + ->label(trans('admin/egg.environment_variable')) + ->maxLength(255) + ->prefix('{{') + ->suffix('}}') + ->hintIcon('tabler-code', fn ($state) => "{{{$state}}}") + ->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id'))) + ->rules(EggVariable::getRulesForField('env_variable')) + ->validationMessages([ + 'unique' => trans('admin/egg.error_unique'), + 'required' => trans('admin/egg.error_required'), + '*' => trans('admin/egg.error_reserved'), + ]) + ->required(), + TextInput::make('default_value')->label(trans('admin/egg.default_value')), + Fieldset::make(trans('admin/egg.user_permissions')) + ->schema([ + Checkbox::make('user_viewable')->label(trans('admin/egg.viewable')), + Checkbox::make('user_editable')->label(trans('admin/egg.editable')), + ]), + TagsInput::make('rules') + ->label(trans('admin/egg.rules')) + ->columnSpanFull() + ->reorderable() + ->suggestions([ + 'required', + 'nullable', + 'string', + 'integer', + 'numeric', + 'boolean', + 'alpha', + 'alpha_dash', + 'alpha_num', + 'url', + 'email', + 'regex:', + 'min:', + 'max:', + 'between:', + 'between:1024,65535', + 'in:', + 'in:true,false', + ]), + ]), + ]), + Tab::make('install_script') + ->label(trans('admin/egg.tabs.install_script')) + ->columns(3) + ->icon('tabler-file-download') + ->schema([ + CopyFrom::make('copy_script_from') + ->script(), + TextInput::make('script_container') + ->label(trans('admin/egg.script_container')) + ->required() + ->maxLength(255) + ->placeholder('ghcr.io/pelican-eggs/installers:debian'), + Select::make('script_entry') + ->label(trans('admin/egg.script_entry')) + ->selectablePlaceholder(false) + ->options([ + 'bash' => 'bash', + 'ash' => 'ash', + '/bin/bash' => '/bin/bash', + ]) + ->required(), + CodeEditor::make('script_install') + ->hiddenLabel() + ->columnSpanFull(), + ]), + ])->columnSpanFull()->persistTabInQueryString(), + ]); + } /** @return array */ public static function getDefaultPages(): array { @@ -70,6 +503,7 @@ public static function getDefaultPages(): array 'index' => ListEggs::route('/'), 'create' => CreateEgg::route('/create'), 'edit' => EditEgg::route('/{record}/edit'), + 'view' => ViewEgg::route('/{record}'), ]; } } diff --git a/app/Filament/Admin/Resources/Eggs/Pages/EditEgg.php b/app/Filament/Admin/Resources/Eggs/Pages/EditEgg.php index 09179b635a..f59b140811 100644 --- a/app/Filament/Admin/Resources/Eggs/Pages/EditEgg.php +++ b/app/Filament/Admin/Resources/Eggs/Pages/EditEgg.php @@ -47,402 +47,9 @@ class EditEgg extends EditRecord protected static string $resource = EggResource::class; - /** - * @throws Exception - */ - public function form(Schema $schema): Schema + public function form(Schema $schema): Schema { - return $schema - ->components([ - Tabs::make()->tabs([ - Tab::make('configuration') - ->label(trans('admin/egg.tabs.configuration')) - ->columns(['default' => 2, 'sm' => 2, 'md' => 4, 'lg' => 6]) - ->icon('tabler-egg') - ->schema([ - Grid::make(2) - ->columnSpan(1) - ->schema([ - Image::make('', '') - ->hidden(fn ($record) => !$record->image) - ->url(fn ($record) => $record->image) - ->alt('') - ->alignJustify() - ->imageSize(150) - ->columnSpanFull(), - Flex::make([ - Action::make('uploadImage') - ->iconButton() - ->iconSize(IconSize::Large) - ->icon('tabler-photo-up') - ->modal() - ->modalHeading('') - ->modalSubmitActionLabel(trans('admin/egg.import.import_image')) - ->schema([ - Tabs::make() - ->contained(false) - ->tabs([ - Tab::make(trans('admin/egg.import.url')) - ->schema([ - Hidden::make('base64Image'), - TextInput::make('image_url') - ->label(trans('admin/egg.import.image_url')) - ->reactive() - ->autocomplete(false) - ->debounce(500) - ->afterStateUpdated(function ($state, Set $set) { - if (!$state) { - $set('image_url_error', null); - - return; - } - - try { - if (!filter_var($state, FILTER_VALIDATE_URL)) { - throw new \Exception(trans('admin/egg.import.invalid_url')); - } - - $allowedExtensions = [ - 'png' => 'image/png', - 'jpg' => 'image/jpeg', - 'jpeg' => 'image/jpeg', - 'gif' => 'image/gif', - 'webp' => 'image/webp', - 'svg' => 'image/svg+xml', - ]; - - $extension = strtolower(pathinfo(parse_url($state, PHP_URL_PATH), PATHINFO_EXTENSION)); - - if (!array_key_exists($extension, $allowedExtensions)) { - throw new \Exception(trans('admin/egg.import.unsupported_format', ['format' => implode(', ', $allowedExtensions)])); - } - - $host = parse_url($state, PHP_URL_HOST); - $ip = gethostbyname($host); - - if ( - filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false - ) { - throw new \Exception(trans('admin/egg.import.no_local_ip')); - } - - $context = stream_context_create([ - 'http' => ['timeout' => 3], - 'https' => [ - 'timeout' => 3, - 'verify_peer' => true, - 'verify_peer_name' => true, - ], - ]); - - $imageContent = @file_get_contents($state, false, $context, 0, 1048576); // 1024KB - - if (!$imageContent) { - throw new \Exception(trans('admin/egg.import.image_error')); - } - - if (strlen($imageContent) >= 1048576) { - throw new \Exception(trans('admin/egg.import.image_too_large')); - } - - $mimeType = $allowedExtensions[$extension]; - $base64 = 'data:' . $mimeType . ';base64,' . base64_encode($imageContent); - - $set('base64Image', $base64); - $set('image_url_error', null); - - } catch (\Exception $e) { - $set('image_url_error', $e->getMessage()); - $set('base64Image', null); - } - }), - TextEntry::make('image_url_error') - ->hiddenLabel() - ->visible(fn ($get) => $get('image_url_error') !== null) - ->afterStateHydrated(fn ($set, $get) => $get('image_url_error')), - Image::make(fn (Get $get) => $get('image_url'), '') - ->imageSize(150) - ->visible(fn ($get) => $get('image_url') && !$get('image_url_error')) - ->alignCenter(), - ]), - Tab::make(trans('admin/egg.import.file')) - ->schema([ - FileUpload::make('image') - ->hiddenLabel() - ->previewable() - ->openable(false) - ->downloadable(false) - ->maxSize(1024) - ->maxFiles(1) - ->columnSpanFull() - ->alignCenter() - ->imageEditor() - ->saveUploadedFileUsing(function ($file, Set $set) { - $base64 = "data:{$file->getMimeType()};base64,". base64_encode(file_get_contents($file->getRealPath())); - $set('base64Image', $base64); - - return $base64; - }), - ]), - ]), - ]) - ->action(function (array $data, $record): void { - $base64 = $data['base64Image'] ?? null; - - if (empty($base64) && !empty($data['image'])) { - $base64 = $data['image']; - } - - if (!empty($base64)) { - $record->update([ - 'image' => $base64, - ]); - - Notification::make() - ->title(trans('admin/egg.import.image_updated')) - ->success() - ->send(); - - $record->refresh(); - } else { - Notification::make() - ->title(trans('admin/egg.import.no_image')) - ->warning() - ->send(); - } - }), - Action::make('deleteImage') - ->visible(fn ($record) => $record->image) - ->label('') - ->icon('tabler-trash') - ->iconButton() - ->iconSize(IconSize::Large) - ->color('danger') - ->action(function ($record) { - - $record->update([ - 'image' => null, - ]); - - Notification::make() - ->title(trans('admin/egg.import.image_deleted')) - ->success() - ->send(); - - $record->refresh(); - }), - ]), - ]), - TextInput::make('name') - ->label(trans('admin/egg.name')) - ->required() - ->maxLength(255) - ->columnSpan(['default' => 2, 'sm' => 2, 'md' => 3, 'lg' => 2]) - ->helperText(trans('admin/egg.name_help')), - Textarea::make('description') - ->label(trans('admin/egg.description')) - ->rows(3) - ->columnSpan(['default' => 2, 'sm' => 2, 'md' => 4, 'lg' => 3]) - ->helperText(trans('admin/egg.description_help')), - TextInput::make('id') - ->label(trans('admin/egg.egg_id')) - ->columnSpan(1) - ->disabled(), - TextInput::make('uuid') - ->label(trans('admin/egg.egg_uuid')) - ->disabled() - ->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2]) - ->helperText(trans('admin/egg.uuid_help')), - TextInput::make('author') - ->label(trans('admin/egg.author')) - ->required() - ->maxLength(255) - ->email() - ->disabled() - ->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2]) - ->helperText(trans('admin/egg.author_help_edit')), - Toggle::make('force_outgoing_ip') - ->inline(false) - ->label(trans('admin/egg.force_ip')) - ->columnSpan(1) - ->hintIcon('tabler-question-mark', trans('admin/egg.force_ip_help')), - KeyValue::make('startup_commands') - ->label(trans('admin/egg.startup_commands')) - ->live() - ->columnSpanFull() - ->required() - ->addActionLabel(trans('admin/egg.add_startup')) - ->keyLabel(trans('admin/egg.startup_name')) - ->valueLabel(trans('admin/egg.startup_command')) - ->helperText(trans('admin/egg.startup_help')), - TagsInput::make('file_denylist') - ->label(trans('admin/egg.file_denylist')) - ->placeholder('denied-file.txt') - ->helperText(trans('admin/egg.file_denylist_help')) - ->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]), - TextInput::make('update_url') - ->label(trans('admin/egg.update_url')) - ->url() - ->hintIcon('tabler-question-mark', trans('admin/egg.update_url_help')) - ->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]), - TagsInput::make('features') - ->label(trans('admin/egg.features')) - ->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]), - Hidden::make('script_is_privileged') - ->helperText('The docker images available to servers using this egg.'), - TagsInput::make('tags') - ->label(trans('admin/egg.tags')) - ->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]), - KeyValue::make('docker_images') - ->label(trans('admin/egg.docker_images')) - ->live() - ->columnSpanFull() - ->required() - ->addActionLabel(trans('admin/egg.add_image')) - ->keyLabel(trans('admin/egg.docker_name')) - ->valueLabel(trans('admin/egg.docker_uri')) - ->helperText(trans('admin/egg.docker_help')), - ]), - Tab::make('process_management') - ->label(trans('admin/egg.tabs.process_management')) - ->columns() - ->icon('tabler-server-cog') - ->schema([ - CopyFrom::make('copy_process_from') - ->process(), - TextInput::make('config_stop') - ->label(trans('admin/egg.stop_command')) - ->maxLength(255) - ->helperText(trans('admin/egg.stop_command_help')), - Textarea::make('config_startup')->rows(10)->json() - ->label(trans('admin/egg.start_config')) - ->helperText(trans('admin/egg.start_config_help')), - Textarea::make('config_files')->rows(10)->json() - ->label(trans('admin/egg.config_files')) - ->helperText(trans('admin/egg.config_files_help')), - Textarea::make('config_logs')->rows(10)->json() - ->label(trans('admin/egg.log_config')) - ->helperText(trans('admin/egg.log_config_help')), - ]), - Tab::make('egg_variables') - ->label(trans('admin/egg.tabs.egg_variables')) - ->columnSpanFull() - ->icon('tabler-variable') - ->schema([ - Repeater::make('variables') - ->hiddenLabel() - ->grid() - ->relationship('variables') - ->reorderable() - ->collapsible()->collapsed() - ->orderColumn() - ->addActionLabel(trans('admin/egg.add_new_variable')) - ->itemLabel(fn (array $state) => $state['name']) - ->mutateRelationshipDataBeforeCreateUsing(function (array $data): array { - $data['default_value'] ??= ''; - $data['description'] ??= ''; - $data['rules'] ??= []; - $data['user_viewable'] ??= ''; - $data['user_editable'] ??= ''; - - return $data; - }) - ->mutateRelationshipDataBeforeSaveUsing(function (array $data): array { - $data['default_value'] ??= ''; - $data['description'] ??= ''; - $data['rules'] ??= []; - $data['user_viewable'] ??= ''; - $data['user_editable'] ??= ''; - - return $data; - }) - ->schema([ - TextInput::make('name') - ->label(trans('admin/egg.name')) - ->live() - ->debounce(750) - ->maxLength(255) - ->columnSpanFull() - ->afterStateUpdated(fn (Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString())) - ->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id'))) - ->validationMessages([ - 'unique' => trans('admin/egg.error_unique'), - ]) - ->required(), - Textarea::make('description')->label(trans('admin/egg.description'))->columnSpanFull(), - TextInput::make('env_variable') - ->label(trans('admin/egg.environment_variable')) - ->maxLength(255) - ->prefix('{{') - ->suffix('}}') - ->hintIcon('tabler-code', fn ($state) => "{{{$state}}}") - ->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id'))) - ->rules(EggVariable::getRulesForField('env_variable')) - ->validationMessages([ - 'unique' => trans('admin/egg.error_unique'), - 'required' => trans('admin/egg.error_required'), - '*' => trans('admin/egg.error_reserved'), - ]) - ->required(), - TextInput::make('default_value')->label(trans('admin/egg.default_value')), - Fieldset::make(trans('admin/egg.user_permissions')) - ->schema([ - Checkbox::make('user_viewable')->label(trans('admin/egg.viewable')), - Checkbox::make('user_editable')->label(trans('admin/egg.editable')), - ]), - TagsInput::make('rules') - ->label(trans('admin/egg.rules')) - ->columnSpanFull() - ->reorderable() - ->suggestions([ - 'required', - 'nullable', - 'string', - 'integer', - 'numeric', - 'boolean', - 'alpha', - 'alpha_dash', - 'alpha_num', - 'url', - 'email', - 'regex:', - 'min:', - 'max:', - 'between:', - 'between:1024,65535', - 'in:', - 'in:true,false', - ]), - ]), - ]), - Tab::make('install_script') - ->label(trans('admin/egg.tabs.install_script')) - ->columns(3) - ->icon('tabler-file-download') - ->schema([ - CopyFrom::make('copy_script_from') - ->script(), - TextInput::make('script_container') - ->label(trans('admin/egg.script_container')) - ->required() - ->maxLength(255) - ->placeholder('ghcr.io/pelican-eggs/installers:debian'), - Select::make('script_entry') - ->label(trans('admin/egg.script_entry')) - ->selectablePlaceholder(false) - ->options([ - 'bash' => 'bash', - 'ash' => 'ash', - '/bin/bash' => '/bin/bash', - ]) - ->required(), - CodeEditor::make('script_install') - ->hiddenLabel() - ->columnSpanFull(), - ]), - ])->columnSpanFull()->persistTabInQueryString(), - ]); + return EggResource::schema($schema); } /** @return array */ diff --git a/app/Filament/Admin/Resources/Eggs/Pages/ListEggs.php b/app/Filament/Admin/Resources/Eggs/Pages/ListEggs.php index cf7bed2f77..13a150bd05 100644 --- a/app/Filament/Admin/Resources/Eggs/Pages/ListEggs.php +++ b/app/Filament/Admin/Resources/Eggs/Pages/ListEggs.php @@ -39,6 +39,10 @@ public function table(Table $table): Table { return $table ->searchable(true) + ->recordUrl(fn (Egg $egg) => user()?->can('update egg', $egg) + ? EggResource::getUrl('edit', ['record' => $egg]) + : EggResource::getUrl('view', ['record' => $egg]) + ) ->defaultPaginationPageOption(25) ->columns([ TextColumn::make('id') diff --git a/app/Filament/Admin/Resources/Eggs/Pages/ViewEgg.php b/app/Filament/Admin/Resources/Eggs/Pages/ViewEgg.php new file mode 100644 index 0000000000..10875852cf --- /dev/null +++ b/app/Filament/Admin/Resources/Eggs/Pages/ViewEgg.php @@ -0,0 +1,22 @@ +label(trans('admin/node.tabs.config_file')) ->icon('tabler-code') - ->hidden(fn (Node $node) => !user()?->can('edit node', $node)) + ->hidden(fn (Node $node) => !user()?->can('update node', $node)) ->schema([ TextEntry::make('instructions') ->label(trans('admin/node.instructions')) @@ -688,7 +688,7 @@ public static function schema(Schema $schema): Schema ]), ]), Tab::make('diagnostics') - ->hidden(fn (Node $node) => !user()?->can('edit node', $node)) + ->hidden(fn (Node $node) => !user()?->can('update node', $node)) ->label(trans('admin/node.tabs.diagnostics')) ->icon('tabler-heart-search') ->schema([ diff --git a/app/Filament/Admin/Resources/Nodes/Pages/ListNodes.php b/app/Filament/Admin/Resources/Nodes/Pages/ListNodes.php index bef4a6fc23..5609b640ef 100644 --- a/app/Filament/Admin/Resources/Nodes/Pages/ListNodes.php +++ b/app/Filament/Admin/Resources/Nodes/Pages/ListNodes.php @@ -28,7 +28,7 @@ class ListNodes extends ListRecords public function table(Table $table): Table { return $table - ->recordUrl(fn (Node $node) => user()?->can('edit node', $node) + ->recordUrl(fn (Node $node) => user()?->can('update node', $node) ? NodeResource::getUrl('edit', ['record' => $node]) : NodeResource::getUrl('view', ['record' => $node]) ) diff --git a/app/Filament/Admin/Resources/Servers/Pages/ListServers.php b/app/Filament/Admin/Resources/Servers/Pages/ListServers.php index 4a78c095b2..77187d1ba1 100644 --- a/app/Filament/Admin/Resources/Servers/Pages/ListServers.php +++ b/app/Filament/Admin/Resources/Servers/Pages/ListServers.php @@ -30,7 +30,7 @@ class ListServers extends ListRecords public function table(Table $table): Table { return $table - ->recordUrl(fn (Server $server) => user()?->can('edit server', $server) + ->recordUrl(fn (Server $server) => user()?->can('update server', $server) ? ServerResource::getUrl('edit', ['record' => $server]) : ServerResource::getUrl('view', ['record' => $server]) ) From 1abb046a03f6eb298d826a466545032245fc9299 Mon Sep 17 00:00:00 2001 From: JoanFo1456 <161775222+JoanFo1456@users.noreply.github.com> Date: Sat, 13 Dec 2025 16:22:10 +0000 Subject: [PATCH 04/10] phpstan and pint, jesus... --- .../Admin/Resources/Eggs/EggResource.php | 22 +-- .../Admin/Resources/Eggs/Pages/EditEgg.php | 27 +-- .../Admin/Resources/Eggs/Pages/ListEggs.php | 4 +- .../Admin/Resources/Eggs/Pages/ViewEgg.php | 2 +- .../Admin/Resources/Nodes/NodeResource.php | 8 +- .../Admin/Resources/Nodes/Pages/EditNode.php | 10 +- .../Admin/Resources/Nodes/Pages/ListNodes.php | 4 +- .../Admin/Resources/Nodes/Pages/ViewNode.php | 7 +- .../Resources/Servers/Pages/EditServer.php | 11 +- .../Resources/Servers/Pages/ListServers.php | 4 +- .../Resources/Servers/Pages/ViewServer.php | 30 ++-- .../Resources/Servers/ServerResource.php | 161 ++---------------- 12 files changed, 69 insertions(+), 221 deletions(-) diff --git a/app/Filament/Admin/Resources/Eggs/EggResource.php b/app/Filament/Admin/Resources/Eggs/EggResource.php index f604e13045..b79853845c 100644 --- a/app/Filament/Admin/Resources/Eggs/EggResource.php +++ b/app/Filament/Admin/Resources/Eggs/EggResource.php @@ -8,24 +8,13 @@ use App\Filament\Admin\Resources\Eggs\Pages\ListEggs; use App\Filament\Admin\Resources\Eggs\Pages\ViewEgg; use App\Filament\Admin\Resources\Eggs\RelationManagers\ServersRelationManager; +use App\Filament\Components\Forms\Fields\CopyFrom; use App\Models\Egg; +use App\Models\EggVariable; 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 Filament\Schemas\Schema; - -use App\Filament\Components\Actions\ExportEggAction; -use App\Filament\Components\Actions\ImportEggAction; -use App\Filament\Components\Forms\Fields\CopyFrom; -use App\Models\EggVariable; -use App\Traits\Filament\CanCustomizeHeaderActions; -use App\Traits\Filament\CanCustomizeHeaderWidgets; use Exception; use Filament\Actions\Action; -use Filament\Actions\ActionGroup; -use Filament\Actions\DeleteAction; use Filament\Forms\Components\Checkbox; use Filament\Forms\Components\CodeEditor; use Filament\Forms\Components\FileUpload; @@ -39,7 +28,9 @@ use Filament\Forms\Components\Toggle; use Filament\Infolists\Components\TextEntry; use Filament\Notifications\Notification; -use Filament\Resources\Pages\EditRecord; +use Filament\Resources\Pages\PageRegistration; +use Filament\Resources\RelationManagers\RelationManager; +use Filament\Resources\Resource; use Filament\Schemas\Components\Fieldset; use Filament\Schemas\Components\Flex; use Filament\Schemas\Components\Grid; @@ -48,6 +39,7 @@ use Filament\Schemas\Components\Tabs\Tab; use Filament\Schemas\Components\Utilities\Get; use Filament\Schemas\Components\Utilities\Set; +use Filament\Schemas\Schema; use Filament\Support\Enums\IconSize; use Illuminate\Validation\Rules\Unique; @@ -99,6 +91,7 @@ public static function getDefaultRelations(): array ServersRelationManager::class, ]; } + /** * @throws Exception */ @@ -496,6 +489,7 @@ public static function schema(Schema $schema): Schema ])->columnSpanFull()->persistTabInQueryString(), ]); } + /** @return array */ public static function getDefaultPages(): array { diff --git a/app/Filament/Admin/Resources/Eggs/Pages/EditEgg.php b/app/Filament/Admin/Resources/Eggs/Pages/EditEgg.php index f59b140811..c55e01eebe 100644 --- a/app/Filament/Admin/Resources/Eggs/Pages/EditEgg.php +++ b/app/Filament/Admin/Resources/Eggs/Pages/EditEgg.php @@ -5,40 +5,15 @@ use App\Filament\Admin\Resources\Eggs\EggResource; 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 Exception; use Filament\Actions\Action; use Filament\Actions\ActionGroup; use Filament\Actions\DeleteAction; -use Filament\Forms\Components\Checkbox; -use Filament\Forms\Components\CodeEditor; -use Filament\Forms\Components\FileUpload; -use Filament\Forms\Components\Hidden; -use Filament\Forms\Components\KeyValue; -use Filament\Forms\Components\Repeater; -use Filament\Forms\Components\Select; -use Filament\Forms\Components\TagsInput; -use Filament\Forms\Components\Textarea; -use Filament\Forms\Components\TextInput; -use Filament\Forms\Components\Toggle; -use Filament\Infolists\Components\TextEntry; -use Filament\Notifications\Notification; use Filament\Resources\Pages\EditRecord; -use Filament\Schemas\Components\Fieldset; -use Filament\Schemas\Components\Flex; -use Filament\Schemas\Components\Grid; -use Filament\Schemas\Components\Image; -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\Schemas\Schema; use Filament\Support\Enums\IconSize; -use Illuminate\Validation\Rules\Unique; class EditEgg extends EditRecord { @@ -47,7 +22,7 @@ class EditEgg extends EditRecord protected static string $resource = EggResource::class; - public function form(Schema $schema): Schema + public function form(Schema $schema): Schema { return EggResource::schema($schema); } diff --git a/app/Filament/Admin/Resources/Eggs/Pages/ListEggs.php b/app/Filament/Admin/Resources/Eggs/Pages/ListEggs.php index 13a150bd05..011d98386e 100644 --- a/app/Filament/Admin/Resources/Eggs/Pages/ListEggs.php +++ b/app/Filament/Admin/Resources/Eggs/Pages/ListEggs.php @@ -39,8 +39,8 @@ public function table(Table $table): Table { return $table ->searchable(true) - ->recordUrl(fn (Egg $egg) => user()?->can('update egg', $egg) - ? EggResource::getUrl('edit', ['record' => $egg]) + ->recordUrl(fn (Egg $egg) => user()?->can('update egg', $egg) + ? EggResource::getUrl('edit', ['record' => $egg]) : EggResource::getUrl('view', ['record' => $egg]) ) ->defaultPaginationPageOption(25) diff --git a/app/Filament/Admin/Resources/Eggs/Pages/ViewEgg.php b/app/Filament/Admin/Resources/Eggs/Pages/ViewEgg.php index 10875852cf..c2f0f91f83 100644 --- a/app/Filament/Admin/Resources/Eggs/Pages/ViewEgg.php +++ b/app/Filament/Admin/Resources/Eggs/Pages/ViewEgg.php @@ -19,4 +19,4 @@ public function form(Schema $schema): Schema { return EggResource::schema($schema); } -} \ No newline at end of file +} diff --git a/app/Filament/Admin/Resources/Nodes/NodeResource.php b/app/Filament/Admin/Resources/Nodes/NodeResource.php index 7869c655ba..7f8f9764e1 100644 --- a/app/Filament/Admin/Resources/Nodes/NodeResource.php +++ b/app/Filament/Admin/Resources/Nodes/NodeResource.php @@ -14,13 +14,10 @@ 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 App\Traits\Filament\CanCustomizePages; use App\Traits\Filament\CanCustomizeRelations; use Exception; use Filament\Actions\Action; -use Filament\Actions\DeleteAction; use Filament\Forms\Components\Hidden; use Filament\Forms\Components\Slider; use Filament\Forms\Components\Slider\Enums\PipsMode; @@ -31,7 +28,9 @@ use Filament\Infolists\Components\CodeEntry; use Filament\Infolists\Components\TextEntry; use Filament\Notifications\Notification; -use Filament\Resources\Pages\EditRecord; +use Filament\Resources\Pages\PageRegistration; +use Filament\Resources\RelationManagers\RelationManager; +use Filament\Resources\Resource; use Filament\Schemas\Components\Actions; use Filament\Schemas\Components\Fieldset; use Filament\Schemas\Components\Grid; @@ -51,7 +50,6 @@ use Illuminate\Support\Facades\Http; use Illuminate\Support\HtmlString; use Phiki\Grammar\Grammar; -use Filament\Resources\Resource; use Throwable; class NodeResource extends Resource diff --git a/app/Filament/Admin/Resources/Nodes/Pages/EditNode.php b/app/Filament/Admin/Resources/Nodes/Pages/EditNode.php index 4619543cb8..3255e62c34 100644 --- a/app/Filament/Admin/Resources/Nodes/Pages/EditNode.php +++ b/app/Filament/Admin/Resources/Nodes/Pages/EditNode.php @@ -8,7 +8,6 @@ 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\Notifications\Notification; @@ -25,12 +24,10 @@ class EditNode extends EditRecord protected static string $resource = NodeResource::class; private DaemonSystemRepository $daemonSystemRepository; - private NodeUpdateService $nodeUpdateService; public function boot(DaemonSystemRepository $daemonSystemRepository, NodeUpdateService $nodeUpdateService): void { $this->daemonSystemRepository = $daemonSystemRepository; - $this->nodeUpdateService = $nodeUpdateService; } public function form(Schema $schema): Schema @@ -110,15 +107,18 @@ protected function afterSave(): void ->send(); } } + protected function getSavedNotification(): ?Notification { return null; } - protected function getColumnSpan() { + + protected function getColumnSpan(): ?int + { return null; } - protected function getColumnStart(): ?int + protected function getColumnStart(): ?int { return null; } diff --git a/app/Filament/Admin/Resources/Nodes/Pages/ListNodes.php b/app/Filament/Admin/Resources/Nodes/Pages/ListNodes.php index 5609b640ef..bea18513af 100644 --- a/app/Filament/Admin/Resources/Nodes/Pages/ListNodes.php +++ b/app/Filament/Admin/Resources/Nodes/Pages/ListNodes.php @@ -28,8 +28,8 @@ class ListNodes extends ListRecords public function table(Table $table): Table { return $table - ->recordUrl(fn (Node $node) => user()?->can('update node', $node) - ? NodeResource::getUrl('edit', ['record' => $node]) + ->recordUrl(fn (Node $node) => user()?->can('update node', $node) + ? NodeResource::getUrl('edit', ['record' => $node]) : NodeResource::getUrl('view', ['record' => $node]) ) ->searchable(false) diff --git a/app/Filament/Admin/Resources/Nodes/Pages/ViewNode.php b/app/Filament/Admin/Resources/Nodes/Pages/ViewNode.php index 1d0a36a3ac..47633bf7b3 100644 --- a/app/Filament/Admin/Resources/Nodes/Pages/ViewNode.php +++ b/app/Filament/Admin/Resources/Nodes/Pages/ViewNode.php @@ -30,11 +30,14 @@ public function getRelationManagers(): array ServersRelationManager::class, ]; } + protected function getSavedNotification(): ?Notification { return null; } - protected function getColumnSpan() { + + protected function getColumnSpan(): ?int + { return null; } @@ -42,4 +45,4 @@ protected function getColumnStart(): ?int { return null; } -} \ No newline at end of file +} diff --git a/app/Filament/Admin/Resources/Servers/Pages/EditServer.php b/app/Filament/Admin/Resources/Servers/Pages/EditServer.php index 8236282067..1e7d4c65d7 100644 --- a/app/Filament/Admin/Resources/Servers/Pages/EditServer.php +++ b/app/Filament/Admin/Resources/Servers/Pages/EditServer.php @@ -2,13 +2,11 @@ namespace App\Filament\Admin\Resources\Servers\Pages; -use App\Filament\Admin\Resources\Servers\Pages\ListServers; use App\Filament\Admin\Resources\Servers\RelationManagers\AllocationsRelationManager; use App\Filament\Admin\Resources\Servers\RelationManagers\DatabasesRelationManager; use App\Filament\Admin\Resources\Servers\ServerResource; use App\Filament\Server\Pages\Console; use App\Models\Server; -use App\Repositories\Daemon\DaemonServerRepository; use App\Services\Servers\ServerDeletionService; use App\Traits\Filament\CanCustomizeHeaderActions; use App\Traits\Filament\CanCustomizeHeaderWidgets; @@ -26,14 +24,8 @@ class EditServer extends EditRecord { use CanCustomizeHeaderActions; use CanCustomizeHeaderWidgets; - protected static string $resource = ServerResource::class; - - private DaemonServerRepository $daemonServerRepository; - public function boot(DaemonServerRepository $daemonServerRepository): void - { - $this->daemonServerRepository = $daemonServerRepository; - } + protected static string $resource = ServerResource::class; /** * @throws RandomException @@ -111,6 +103,7 @@ protected function getFormActions(): array { return []; } + public function getRelationManagers(): array { return [ diff --git a/app/Filament/Admin/Resources/Servers/Pages/ListServers.php b/app/Filament/Admin/Resources/Servers/Pages/ListServers.php index 77187d1ba1..b46921bbeb 100644 --- a/app/Filament/Admin/Resources/Servers/Pages/ListServers.php +++ b/app/Filament/Admin/Resources/Servers/Pages/ListServers.php @@ -30,8 +30,8 @@ class ListServers extends ListRecords public function table(Table $table): Table { return $table - ->recordUrl(fn (Server $server) => user()?->can('update server', $server) - ? ServerResource::getUrl('edit', ['record' => $server]) + ->recordUrl(fn (Server $server) => user()?->can('update server', $server) + ? ServerResource::getUrl('edit', ['record' => $server]) : ServerResource::getUrl('view', ['record' => $server]) ) ->searchable(false) diff --git a/app/Filament/Admin/Resources/Servers/Pages/ViewServer.php b/app/Filament/Admin/Resources/Servers/Pages/ViewServer.php index e28e3d1e4b..9b83a85f16 100644 --- a/app/Filament/Admin/Resources/Servers/Pages/ViewServer.php +++ b/app/Filament/Admin/Resources/Servers/Pages/ViewServer.php @@ -2,13 +2,11 @@ namespace App\Filament\Admin\Resources\Servers\Pages; -use App\Filament\Admin\Resources\Servers\Pages\ListServers; use App\Filament\Admin\Resources\Servers\RelationManagers\AllocationsRelationManager; use App\Filament\Admin\Resources\Servers\RelationManagers\DatabasesRelationManager; use App\Filament\Admin\Resources\Servers\ServerResource; use App\Filament\Server\Pages\Console; use App\Models\Server; -use App\Repositories\Daemon\DaemonServerRepository; use App\Services\Servers\ServerDeletionService; use App\Traits\Filament\CanCustomizeHeaderActions; use App\Traits\Filament\CanCustomizeHeaderWidgets; @@ -29,14 +27,6 @@ class ViewServer extends ViewRecord protected static string $resource = ServerResource::class; - private DaemonServerRepository $daemonServerRepository; - - public function boot(DaemonServerRepository $daemonServerRepository): void - { - $this->daemonServerRepository = $daemonServerRepository; - - } - /** * @throws \Random\RandomException * @throws \Exception @@ -117,4 +107,24 @@ protected function getDefaultHeaderActions(): array ->iconButton()->iconSize(IconSize::ExtraLarge), ]; } + + /** @return array */ + protected function getFormActions(): array + { + return []; + } + + /** @param array $data + * @return array + */ + protected function mutateFormDataBeforeSave(array $data): array + { + if (!isset($data['description'])) { + $data['description'] = ''; + } + + unset($data['docker'], $data['status'], $data['allocation_id']); + + return $data; + } } diff --git a/app/Filament/Admin/Resources/Servers/ServerResource.php b/app/Filament/Admin/Resources/Servers/ServerResource.php index daeea11250..672ce53082 100644 --- a/app/Filament/Admin/Resources/Servers/ServerResource.php +++ b/app/Filament/Admin/Resources/Servers/ServerResource.php @@ -4,7 +4,6 @@ use App\Enums\CustomizationKey; use App\Enums\SuspendAction; -use App\Filament\Admin\Resources\DatabaseHosts\RelationManagers\DatabasesRelationManager; use App\Filament\Admin\Resources\Servers\Pages\CreateServer; use App\Filament\Admin\Resources\Servers\Pages\EditServer; use App\Filament\Admin\Resources\Servers\Pages\ListServers; @@ -13,35 +12,23 @@ use App\Filament\Components\Actions\PreviewStartupAction; use App\Filament\Components\Forms\Fields\StartupVariable; use App\Filament\Components\StateCasts\ServerConditionStateCast; -use App\Models\Mount; -use App\Models\Server; -use App\Traits\Filament\CanCustomizePages; -use App\Traits\Filament\CanCustomizeRelations; -use Exception; -use Filament\Forms\Components\CheckboxList; -use Filament\Resources\Pages\PageRegistration; -use Filament\Resources\RelationManagers\RelationManager; -use Filament\Resources\Resource; -use Filament\Schemas\Components\Utilities\Get; -use App\Filament\Server\Pages\Console; use App\Models\Allocation; use App\Models\Egg; +use App\Models\Mount; +use App\Models\Server; use App\Models\User; use App\Repositories\Daemon\DaemonServerRepository; use App\Services\Eggs\EggChangerService; use App\Services\Servers\RandomWordService; use App\Services\Servers\ReinstallServerService; -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 App\Traits\Filament\CanCustomizePages; +use App\Traits\Filament\CanCustomizeRelations; +use Exception; use Filament\Actions\Action; -use Filament\Actions\ActionGroup; -use Filament\Actions\CreateAction; -use Filament\Actions\EditAction; -use Filament\Actions\ViewAction; +use Filament\Forms\Components\CheckboxList; use Filament\Forms\Components\CodeEditor; use Filament\Forms\Components\FileUpload; use Filament\Forms\Components\Hidden; @@ -55,7 +42,9 @@ use Filament\Forms\Components\ToggleButtons; use Filament\Infolists\Components\TextEntry; use Filament\Notifications\Notification; -use Filament\Resources\Pages\ListRecords; +use Filament\Resources\Pages\PageRegistration; +use Filament\Resources\RelationManagers\RelationManager; +use Filament\Resources\Resource; use Filament\Schemas\Components\Actions; use Filament\Schemas\Components\Fieldset; use Filament\Schemas\Components\Grid; @@ -63,25 +52,24 @@ use Filament\Schemas\Components\StateCasts\BooleanStateCast; 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\Schemas\Schema; use Filament\Support\Enums\Alignment; use Filament\Support\Enums\IconSize; -use Filament\Tables\Columns\SelectColumn; -use Filament\Tables\Columns\TextColumn; -use Filament\Tables\Grouping\Group; -use Filament\Tables\Table; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\HtmlString; use LogicException; use Pest\Support\Arr; use Predis\Connection\ConnectionException; -use Filament\Schemas\Components\Utilities\Set; class ServerResource extends Resource { use CanCustomizePages; use CanCustomizeRelations; + protected DaemonServerRepository $daemonServerRepository; + protected static ?string $model = Server::class; protected static string|\BackedEnum|null $navigationIcon = 'tabler-brand-docker'; @@ -112,6 +100,7 @@ public static function getNavigationBadge(): ?string { return (string) static::getEloquentQuery()->count() ?: null; } + public static function schema(Schema $schema): Schema { return $schema @@ -1022,7 +1011,7 @@ public static function schema(Schema $schema): Schema ->label(trans('admin/server.transfer')) ->disabled(fn (Server $server) => user()?->accessibleNodes()->count() <= 1 || $server->isInConflictState()) ->modalHeading(trans('admin/server.transfer')) - ->schema(static::transferServer()) + ->schema(fn () => self::transferServer()) ->action(function (TransferServerService $transfer, Server $server, $data) { try { $transfer->handle($server, Arr::get($data, 'node_id'), Arr::get($data, 'allocation_id'), Arr::get($data, 'allocation_additional', [])); @@ -1082,7 +1071,9 @@ public static function schema(Schema $schema): Schema ]); } - /** @return Component[] + /** + * @return array<\Filament\Forms\Components\Select> + * * @throws Exception */ protected static function transferServer(): array @@ -1118,122 +1109,6 @@ protected static function transferServer(): array ]; } - /** @return array */ - public function getDefaultHeaderActions(): array - { - /** @var Server $server */ - $server = $this->getRecord(); - - $canForceDelete = cache()->get("servers.$server->uuid.canForceDelete", false); - - return [ - Action::make('Delete') - ->color('danger') - ->label(trans('filament-actions::delete.single.label')) - ->modalHeading(trans('filament-actions::delete.single.modal.heading', ['label' => $server->name])) - ->modalSubmitActionLabel(trans('filament-actions::delete.single.label')) - ->requiresConfirmation() - ->action(function (Server $server, ServerDeletionService $service) { - try { - $service->handle($server); - - return redirect(ListServers::getUrl(panel: 'admin')); - } catch (ConnectionException) { - cache()->put("servers.$server->uuid.canForceDelete", true, now()->addMinutes(5)); - - return Notification::make() - ->title(trans('admin/server.notifications.error_server_delete')) - ->body(trans('admin/server.notifications.error_server_delete_body')) - ->color('warning') - ->icon('tabler-database') - ->warning() - ->send(); - } - }) - ->hidden(fn () => $canForceDelete) - ->authorize(fn (Server $server) => user()?->can('delete server', $server)) - ->icon('tabler-trash') - ->iconButton()->iconSize(IconSize::ExtraLarge), - Action::make('ForceDelete') - ->color('danger') - ->label(trans('filament-actions::force-delete.single.label')) - ->modalHeading(trans('filament-actions::force-delete.single.modal.heading', ['label' => $server->name])) - ->modalSubmitActionLabel(trans('filament-actions::force-delete.single.label')) - ->requiresConfirmation() - ->action(function (Server $server, ServerDeletionService $service) { - try { - $service->withForce()->handle($server); - - return redirect(ListServers::getUrl(panel: 'admin')); - } catch (ConnectionException) { - return cache()->forget("servers.$server->uuid.canForceDelete"); - } - }) - ->visible(fn () => $canForceDelete) - ->authorize(fn (Server $server) => user()?->can('delete server', $server)), - Action::make('console') - ->label(trans('admin/server.console')) - ->icon('tabler-terminal') - ->iconButton()->iconSize(IconSize::ExtraLarge) - ->url(fn (Server $server) => Console::getUrl(panel: 'server', tenant: $server)), - $this->getSaveFormAction()->formId('form') - ->iconButton()->iconSize(IconSize::ExtraLarge) - ->icon('tabler-device-floppy'), - ]; - - } - - protected function getFormActions(): array - { - return []; - } - - protected function mutateFormDataBeforeSave(array $data): array - { - if (!isset($data['description'])) { - $data['description'] = ''; - } - - unset($data['docker'], $data['status'], $data['allocation_id']); - - return $data; - } - - protected function afterSave(): void - { - /** @var Server $server */ - $server = $this->getRecord(); - - $changed = collect($server->getChanges())->except(['updated_at', 'name', 'owner_id', 'condition', 'description', 'external_id', 'tags', 'cpu_pinning', 'allocation_limit', 'database_limit', 'backup_limit', 'skip_scripts'])->all(); - - try { - if ($changed) { - $this->daemonServerRepository->setServer($server)->sync(); - } - parent::getSavedNotification()?->send(); - } catch (ConnectionException) { - Notification::make() - ->title(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name])) - ->body(trans('admin/server.notifications.error_connecting_description')) - ->color('warning') - ->icon('tabler-database') - ->warning() - ->send(); - } - } - - protected function getSavedNotification(): ?Notification - { - return null; - } - - public function getRelationManagers(): array - { - return [ - AllocationsRelationManager::class, - DatabasesRelationManager::class, - ]; - } /** * @throws Exception */ From a0528de467d13305cc2b6a06ad1615174753cefa Mon Sep 17 00:00:00 2001 From: JoanFo1456 <161775222+JoanFo1456@users.noreply.github.com> Date: Sat, 13 Dec 2025 16:23:48 +0000 Subject: [PATCH 05/10] Oops forgot this --- .../Resources/Servers/Pages/ViewServer.php | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/app/Filament/Admin/Resources/Servers/Pages/ViewServer.php b/app/Filament/Admin/Resources/Servers/Pages/ViewServer.php index 9b83a85f16..97dfa4046e 100644 --- a/app/Filament/Admin/Resources/Servers/Pages/ViewServer.php +++ b/app/Filament/Admin/Resources/Servers/Pages/ViewServer.php @@ -107,24 +107,4 @@ protected function getDefaultHeaderActions(): array ->iconButton()->iconSize(IconSize::ExtraLarge), ]; } - - /** @return array */ - protected function getFormActions(): array - { - return []; - } - - /** @param array $data - * @return array - */ - protected function mutateFormDataBeforeSave(array $data): array - { - if (!isset($data['description'])) { - $data['description'] = ''; - } - - unset($data['docker'], $data['status'], $data['allocation_id']); - - return $data; - } } From e31249260acb455fd7b46af322cf3951bb8e0292 Mon Sep 17 00:00:00 2001 From: JoanFo1456 <161775222+JoanFo1456@users.noreply.github.com> Date: Sat, 13 Dec 2025 16:28:14 +0000 Subject: [PATCH 06/10] Permission fix 2 --- .../Servers/RelationManagers/AllocationsRelationManager.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/Filament/Admin/Resources/Servers/RelationManagers/AllocationsRelationManager.php b/app/Filament/Admin/Resources/Servers/RelationManagers/AllocationsRelationManager.php index f0090b0e45..e6af0255e9 100644 --- a/app/Filament/Admin/Resources/Servers/RelationManagers/AllocationsRelationManager.php +++ b/app/Filament/Admin/Resources/Servers/RelationManagers/AllocationsRelationManager.php @@ -72,17 +72,17 @@ public function table(Table $table): Table Action::make('make-primary') ->label(trans('admin/server.make_primary')) ->action(fn (Allocation $allocation) => $this->getOwnerRecord()->update(['allocation_id' => $allocation->id]) && $this->deselectAllTableRecords()) - ->disabled(fn () => !user()?->can('edit server', $this->getOwnerRecord())) + ->disabled(fn () => !user()?->can('update server', $this->getOwnerRecord())) ->hidden(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id), Action::make('lock') ->label(trans('admin/server.lock')) ->action(fn (Allocation $allocation) => $allocation->update(['is_locked' => true]) && $this->deselectAllTableRecords()) - ->disabled(fn () => !user()?->can('edit server', $this->getOwnerRecord())) + ->disabled(fn () => !user()?->can('update server', $this->getOwnerRecord())) ->hidden(fn (Allocation $allocation) => $allocation->is_locked), Action::make('unlock') ->label(trans('admin/server.unlock')) ->action(fn (Allocation $allocation) => $allocation->update(['is_locked' => false]) && $this->deselectAllTableRecords()) - ->disabled(fn () => !user()?->can('edit server', $this->getOwnerRecord())) + ->disabled(fn () => !user()?->can('update server', $this->getOwnerRecord())) ->visible(fn (Allocation $allocation) => $allocation->is_locked), DissociateAction::make() ->after(function (Allocation $allocation) { From 89c25c6ce7dfbab529b31241e591260cbbaeb156 Mon Sep 17 00:00:00 2001 From: JoanFo1456 <161775222+JoanFo1456@users.noreply.github.com> Date: Sat, 13 Dec 2025 16:40:01 +0000 Subject: [PATCH 07/10] Fixed Actions working on view --- .../Admin/Resources/Servers/ServerResource.php | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/app/Filament/Admin/Resources/Servers/ServerResource.php b/app/Filament/Admin/Resources/Servers/ServerResource.php index 672ce53082..7f3db2f766 100644 --- a/app/Filament/Admin/Resources/Servers/ServerResource.php +++ b/app/Filament/Admin/Resources/Servers/ServerResource.php @@ -902,7 +902,7 @@ public static function schema(Schema $schema): Schema Actions::make([ Action::make('toggleInstall') ->label(trans('admin/server.toggle_install')) - ->disabled(fn (Server $server) => $server->isSuspended()) + ->disabled(fn (Server $server) => $server->isSuspended() || !user()?->can('update server', $server)) ->modal(fn (Server $server) => $server->isFailedInstall()) ->modalHeading(trans('admin/server.toggle_install_failed_header')) ->modalDescription(trans('admin/server.toggle_install_failed_desc')) @@ -954,6 +954,7 @@ public static function schema(Schema $schema): Schema Action::make('toggleSuspend') ->label(trans('admin/server.suspend')) ->color('warning') + ->disabled(fn (Server $server) => !user()?->can('update server', $server)) ->hidden(fn (Server $server) => $server->isSuspended()) ->action(function (SuspensionService $suspensionService, Server $server) { try { @@ -975,6 +976,7 @@ public static function schema(Schema $schema): Schema Action::make('toggleUnsuspend') ->label(trans('admin/server.unsuspend')) ->color('success') + ->disabled(fn (Server $server) => !user()?->can('update server', $server)) ->hidden(fn (Server $server) => !$server->isSuspended()) ->action(function (SuspensionService $suspensionService, Server $server) { try { @@ -996,11 +998,11 @@ public static function schema(Schema $schema): Schema ])->fullWidth(), ToggleButtons::make('server_suspend') ->hiddenLabel() - ->hidden(fn (Server $server) => $server->isSuspended()) + ->hidden(fn (Server $server) => $server->isSuspended() && !user()?->can('update server', $server)) ->hint(trans('admin/server.notifications.server_suspend_help')), ToggleButtons::make('server_unsuspend') ->hiddenLabel() - ->hidden(fn (Server $server) => !$server->isSuspended()) + ->hidden(fn (Server $server) => !$server->isSuspended() && !user()?->can('update server', $server)) ->hint(trans('admin/server.notifications.server_unsuspend_help')), ]), Grid::make() @@ -1009,7 +1011,7 @@ public static function schema(Schema $schema): Schema Actions::make([ Action::make('transfer') ->label(trans('admin/server.transfer')) - ->disabled(fn (Server $server) => user()?->accessibleNodes()->count() <= 1 || $server->isInConflictState()) + ->disabled(fn (Server $server) => user()?->accessibleNodes()->count() <= 1 || $server->isInConflictState() || !user()?->can('update server', $server)) ->modalHeading(trans('admin/server.transfer')) ->schema(fn () => self::transferServer()) ->action(function (TransferServerService $transfer, Server $server, $data) { @@ -1043,7 +1045,7 @@ public static function schema(Schema $schema): Schema ->requiresConfirmation() ->modalHeading(trans('admin/server.reinstall_modal_heading')) ->modalDescription(trans('admin/server.reinstall_modal_description')) - ->disabled(fn (Server $server) => $server->isSuspended()) + ->disabled(fn (Server $server) => $server->isSuspended() || !user()?->can('update server', $server)) ->action(function (ReinstallServerService $service, Server $server) { try { $service->handle($server); @@ -1089,7 +1091,7 @@ protected static function transferServer(): array ->options(fn (Server $server) => user()?->accessibleNodes()->whereNot('id', $server->node->id)->pluck('name', 'id')->all()), Select::make('allocation_id') ->label(trans('admin/server.primary_allocation')) - ->disabled(fn (Get $get, Server $server) => !$get('node_id') || !$server->allocation_id) + ->disabled(fn (Get $get, Server $server) => !$get('node_id') || !$server->allocation_id || !user()?->can('update server', $server)) ->required(fn (Server $server) => $server->allocation_id) ->prefixIcon('tabler-network') ->options(fn (Get $get) => Allocation::where('node_id', $get('node_id'))->whereNull('server_id')->get()->mapWithKeys(fn (Allocation $allocation) => [$allocation->id => $allocation->address])) @@ -1097,10 +1099,10 @@ protected static function transferServer(): array ->placeholder(trans('admin/server.select_allocation')), Select::make('allocation_additional') ->label(trans('admin/server.additional_allocations')) - ->disabled(fn (Get $get, Server $server) => !$get('node_id') || $server->allocations->count() <= 1) + ->disabled(fn (Get $get, Server $server) => !$get('node_id') || $server->allocations->count() <= 1 || !user()?->can('update server', $server)) ->multiple() ->minItems(fn (Select $select) => $select->getMaxItems()) - ->maxItems(fn (Select $select, Server $server) => $select->isDisabled() ? null : $server->allocations->count() - 1) + ->maxItems(fn (Select $select, Server $server) => $select->isDisabled() ? null : $server->allocations->count() - 1 || !user()?->can('update server', $server)) ->prefixIcon('tabler-network') ->required(fn (Server $server) => $server->allocations->count() > 1) ->options(fn (Get $get) => Allocation::where('node_id', $get('node_id'))->whereNull('server_id')->when($get('allocation_id'), fn ($query) => $query->whereNot('id', $get('allocation_id')))->get()->mapWithKeys(fn (Allocation $allocation) => [$allocation->id => $allocation->address])) From 3568d341cc6cc11ccbbb8fd336e2d463a35b98d5 Mon Sep 17 00:00:00 2001 From: JoanFo1456 <161775222+JoanFo1456@users.noreply.github.com> Date: Sat, 13 Dec 2025 16:48:38 +0000 Subject: [PATCH 08/10] Star was permitted phpstan and pint --- .../AllocationsRelationManager.php | 17 +++++++++++++++-- .../Admin/Resources/Servers/ServerResource.php | 14 +++++++------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/app/Filament/Admin/Resources/Servers/RelationManagers/AllocationsRelationManager.php b/app/Filament/Admin/Resources/Servers/RelationManagers/AllocationsRelationManager.php index e6af0255e9..5236aedcb6 100644 --- a/app/Filament/Admin/Resources/Servers/RelationManagers/AllocationsRelationManager.php +++ b/app/Filament/Admin/Resources/Servers/RelationManagers/AllocationsRelationManager.php @@ -59,7 +59,14 @@ public function table(Table $table): Table default => 'gray', }) ->tooltip(fn (Allocation $allocation) => trans('admin/server.' . ($allocation->id === $this->getOwnerRecord()->allocation_id ? 'already' : 'make') . '_primary')) - ->action(fn (Allocation $allocation) => $this->getOwnerRecord()->update(['allocation_id' => $allocation->id]) && $this->deselectAllTableRecords()) + ->action(function (Allocation $allocation) { + if (!user()?->can('update server', $this->getOwnerRecord())) { + return; + } + + $this->getOwnerRecord()->update(['allocation_id' => $allocation->id]); + $this->deselectAllTableRecords(); + }) ->default(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id) ->label(trans('admin/server.primary')), IconColumn::make('is_locked') @@ -71,7 +78,13 @@ public function table(Table $table): Table ->recordActions([ Action::make('make-primary') ->label(trans('admin/server.make_primary')) - ->action(fn (Allocation $allocation) => $this->getOwnerRecord()->update(['allocation_id' => $allocation->id]) && $this->deselectAllTableRecords()) + ->action(function (Allocation $allocation) { + if (!user()?->can('update server', $this->getOwnerRecord())) { + return; + } + $this->getOwnerRecord()->update(['allocation_id' => $allocation->id]); + $this->deselectAllTableRecords(); + }) ->disabled(fn () => !user()?->can('update server', $this->getOwnerRecord())) ->hidden(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id), Action::make('lock') diff --git a/app/Filament/Admin/Resources/Servers/ServerResource.php b/app/Filament/Admin/Resources/Servers/ServerResource.php index 7f3db2f766..89b5d002d7 100644 --- a/app/Filament/Admin/Resources/Servers/ServerResource.php +++ b/app/Filament/Admin/Resources/Servers/ServerResource.php @@ -998,11 +998,11 @@ public static function schema(Schema $schema): Schema ])->fullWidth(), ToggleButtons::make('server_suspend') ->hiddenLabel() - ->hidden(fn (Server $server) => $server->isSuspended() && !user()?->can('update server', $server)) + ->hidden(fn (Server $server) => $server->isSuspended() && !user()?->can('update server', $server)) ->hint(trans('admin/server.notifications.server_suspend_help')), ToggleButtons::make('server_unsuspend') ->hiddenLabel() - ->hidden(fn (Server $server) => !$server->isSuspended() && !user()?->can('update server', $server)) + ->hidden(fn (Server $server) => !$server->isSuspended() && !user()?->can('update server', $server)) ->hint(trans('admin/server.notifications.server_unsuspend_help')), ]), Grid::make() @@ -1011,7 +1011,7 @@ public static function schema(Schema $schema): Schema Actions::make([ Action::make('transfer') ->label(trans('admin/server.transfer')) - ->disabled(fn (Server $server) => user()?->accessibleNodes()->count() <= 1 || $server->isInConflictState() || !user()?->can('update server', $server)) + ->disabled(fn (Server $server) => user()?->accessibleNodes()->count() <= 1 || $server->isInConflictState() || !user()->can('update server', $server)) ->modalHeading(trans('admin/server.transfer')) ->schema(fn () => self::transferServer()) ->action(function (TransferServerService $transfer, Server $server, $data) { @@ -1045,7 +1045,7 @@ public static function schema(Schema $schema): Schema ->requiresConfirmation() ->modalHeading(trans('admin/server.reinstall_modal_heading')) ->modalDescription(trans('admin/server.reinstall_modal_description')) - ->disabled(fn (Server $server) => $server->isSuspended() || !user()?->can('update server', $server)) + ->disabled(fn (Server $server) => $server->isSuspended() || !user()?->can('update server', $server)) ->action(function (ReinstallServerService $service, Server $server) { try { $service->handle($server); @@ -1091,7 +1091,7 @@ protected static function transferServer(): array ->options(fn (Server $server) => user()?->accessibleNodes()->whereNot('id', $server->node->id)->pluck('name', 'id')->all()), Select::make('allocation_id') ->label(trans('admin/server.primary_allocation')) - ->disabled(fn (Get $get, Server $server) => !$get('node_id') || !$server->allocation_id || !user()?->can('update server', $server)) + ->disabled(fn (Get $get, Server $server) => !$get('node_id') || !$server->allocation_id || !user()?->can('update server', $server)) ->required(fn (Server $server) => $server->allocation_id) ->prefixIcon('tabler-network') ->options(fn (Get $get) => Allocation::where('node_id', $get('node_id'))->whereNull('server_id')->get()->mapWithKeys(fn (Allocation $allocation) => [$allocation->id => $allocation->address])) @@ -1099,10 +1099,10 @@ protected static function transferServer(): array ->placeholder(trans('admin/server.select_allocation')), Select::make('allocation_additional') ->label(trans('admin/server.additional_allocations')) - ->disabled(fn (Get $get, Server $server) => !$get('node_id') || $server->allocations->count() <= 1 || !user()?->can('update server', $server)) + ->disabled(fn (Get $get, Server $server) => !$get('node_id') || $server->allocations->count() <= 1 || !user()?->can('update server', $server)) ->multiple() ->minItems(fn (Select $select) => $select->getMaxItems()) - ->maxItems(fn (Select $select, Server $server) => $select->isDisabled() ? null : $server->allocations->count() - 1 || !user()?->can('update server', $server)) + ->maxItems(fn (Select $select, Server $server) => $select->isDisabled() ? null : $server->allocations->count() - 1 || !user()?->can('update server', $server)) ->prefixIcon('tabler-network') ->required(fn (Server $server) => $server->allocations->count() > 1) ->options(fn (Get $get) => Allocation::where('node_id', $get('node_id'))->whereNull('server_id')->when($get('allocation_id'), fn ($query) => $query->whereNot('id', $get('allocation_id')))->get()->mapWithKeys(fn (Allocation $allocation) => [$allocation->id => $allocation->address])) From bd3ad34ed7b6b62790e81862942b7d7096af002d Mon Sep 17 00:00:00 2001 From: JoanFo1456 <161775222+JoanFo1456@users.noreply.github.com> Date: Sat, 13 Dec 2025 17:00:48 +0000 Subject: [PATCH 09/10] Rabbit suggestions applied --- app/Filament/Admin/Resources/Nodes/Pages/ViewNode.php | 2 +- app/Filament/Admin/Resources/Servers/ServerResource.php | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/Filament/Admin/Resources/Nodes/Pages/ViewNode.php b/app/Filament/Admin/Resources/Nodes/Pages/ViewNode.php index 47633bf7b3..c74170871b 100644 --- a/app/Filament/Admin/Resources/Nodes/Pages/ViewNode.php +++ b/app/Filament/Admin/Resources/Nodes/Pages/ViewNode.php @@ -2,9 +2,9 @@ namespace App\Filament\Admin\Resources\Nodes\Pages; -use App\Filament\Admin\Resources\Eggs\RelationManagers\ServersRelationManager; use App\Filament\Admin\Resources\Nodes\NodeResource; use App\Filament\Admin\Resources\Nodes\RelationManagers\AllocationsRelationManager; +use App\Filament\Admin\Resources\Nodes\RelationManagers\ServersRelationManager; use App\Traits\Filament\CanCustomizeHeaderActions; use App\Traits\Filament\CanCustomizeHeaderWidgets; use Filament\Notifications\Notification; diff --git a/app/Filament/Admin/Resources/Servers/ServerResource.php b/app/Filament/Admin/Resources/Servers/ServerResource.php index 89b5d002d7..de25b88dd6 100644 --- a/app/Filament/Admin/Resources/Servers/ServerResource.php +++ b/app/Filament/Admin/Resources/Servers/ServerResource.php @@ -60,7 +60,7 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\HtmlString; use LogicException; -use Pest\Support\Arr; +use Illuminate\Support\Arr; use Predis\Connection\ConnectionException; class ServerResource extends Resource @@ -68,8 +68,6 @@ class ServerResource extends Resource use CanCustomizePages; use CanCustomizeRelations; - protected DaemonServerRepository $daemonServerRepository; - protected static ?string $model = Server::class; protected static string|\BackedEnum|null $navigationIcon = 'tabler-brand-docker'; From 5b69856e877f0bd266b7c255c10ddfaf0cadfaf3 Mon Sep 17 00:00:00 2001 From: JoanFo1456 <161775222+JoanFo1456@users.noreply.github.com> Date: Sat, 13 Dec 2025 17:02:35 +0000 Subject: [PATCH 10/10] Really? --- app/Filament/Admin/Resources/Servers/ServerResource.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Filament/Admin/Resources/Servers/ServerResource.php b/app/Filament/Admin/Resources/Servers/ServerResource.php index de25b88dd6..cf30d48acb 100644 --- a/app/Filament/Admin/Resources/Servers/ServerResource.php +++ b/app/Filament/Admin/Resources/Servers/ServerResource.php @@ -58,9 +58,9 @@ use Filament\Support\Enums\Alignment; use Filament\Support\Enums\IconSize; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Arr; use Illuminate\Support\HtmlString; use LogicException; -use Illuminate\Support\Arr; use Predis\Connection\ConnectionException; class ServerResource extends Resource