daemonServerRepository = $daemonServerRepository; } public function form(Form $form): Form { return $form ->schema([ Tabs::make('Tabs') ->persistTabInQueryString() ->columns([ 'default' => 2, 'sm' => 2, 'md' => 4, 'lg' => 6, ]) ->columnSpanFull() ->tabs([ Tab::make(trans('admin/server.tabs.information')) ->icon('tabler-info-circle') ->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()]) ->columnSpan([ 'default' => 2, 'sm' => 1, 'md' => 1, 'lg' => 1, ]), Textarea::make('description') ->label(trans('admin/server.description')) ->columnSpanFull(), TextInput::make('uuid') ->label(trans('admin/server.uuid')) ->suffixAction(fn () => request()->isSecure() ? CopyAction::make() : null) ->columnSpan([ 'default' => 2, 'sm' => 1, 'md' => 2, 'lg' => 3, ]) ->readOnly() ->dehydrated(false), TextInput::make('uuid_short') ->label(trans('admin/server.short_uuid')) ->suffixAction(fn () => request()->isSecure() ? CopyAction::make() : null) ->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(ignoreRecord: true) ->maxLength(255), Select::make('node_id') ->label(trans('admin/server.node')) ->relationship('node', 'name', fn (Builder $query) => $query->whereIn('id', auth()->user()->accessibleNodes()->pluck('id'))) ->columnSpan([ 'default' => 2, 'sm' => 1, 'md' => 2, 'lg' => 3, ]) ->disabled(), ]), Tab::make(trans('admin/server.tabs.environment_configuration')) ->icon('tabler-brand-docker') ->schema([ Fieldset::make(trans('admin/server.resource_limits')) ->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() ->options([ true => trans('admin/server.unlimited'), false => trans('admin/server.limited'), ]) ->colors([ true => 'primary', false => 'warning', ]) ->columnSpan(2), TextInput::make('cpu') ->dehydratedWhenHidden() ->hidden(fn (Get $get) => $get('unlimited_cpu')) ->label(trans('admin/server.cpu_limit'))->inlineLabel() ->suffix('%') ->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() ->options([ true => trans('admin/server.unlimited'), false => trans('admin/server.limited'), ]) ->colors([ true => 'primary', false => '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') ->hintIconToolTip(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) ->options([ true => trans('admin/server.unlimited'), false => trans('admin/server.limited'), ]) ->colors([ true => 'primary', false => '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')) ->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(false) ->afterStateUpdated(fn (Set $set) => $set('threads', [])) ->formatStateUsing(fn (Get $get) => !empty($get('threads'))) ->live() ->options([ false => trans('admin/server.disabled'), true => trans('admin/server.enabled'), ]) ->colors([ false => 'success', true => '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') ->label(trans('admin/server.oom'))->inlineLabel()->inline() ->columnSpan(2) ->options([ false => trans('admin/server.disabled'), true => trans('admin/server.enabled'), ]) ->colors([ false => 'success', true => 'danger', ]), ]), ]), Fieldset::make(trans('admin/server.feature_limits')) ->inlineLabel() ->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')) ->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(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['keepOldVariables']); // Use redirect instead of fillForm to prevent server variables from duplicating $this->redirect($this->getUrl(['record' => $server, 'tab' => '-egg-tab']), true); }) ->form(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('keepOldVariables') ->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, ]) ->options([ false => trans('admin/server.yes'), true => trans('admin/server.skip'), ]) ->colors([ false => 'primary', true => 'danger', ]) ->icons([ false => 'tabler-code', true => 'tabler-code-off', ]) ->required(), Hidden::make('previewing') ->default(false), Textarea::make('startup') ->label(trans('admin/server.startup_cmd')) ->required() ->columnSpan(6) ->autosize() ->hintAction(PreviewStartupAction::make('preview')), Textarea::make('defaultStartup') ->hintAction(fn () => request()->isSecure() ? CopyAction::make() : null) ->label(trans('admin/server.default_startup')) ->disabled() ->autosize() ->columnSpan(6) ->formatStateUsing(function ($state, Get $get) { $egg = Egg::query()->find($get('egg_id')); return $egg->startup; }), Repeater::make('server_variables') ->label('') ->relationship('serverVariables', function (Builder $query) { /** @var Server $server */ $server = $this->getRecord(); foreach ($server->variables as $variable) { ServerVariable::query()->firstOrCreate([ 'server_id' => $server->id, 'variable_id' => $variable->id, ], [ 'variable_value' => $variable->server_value ?? '', ]); } return $query->orderByPowerJoins('variable.sort'); }) ->grid() ->mutateRelationshipDataBeforeSaveUsing(function (array &$data): array { foreach ($data as $key => $value) { if (!isset($data['variable_value'])) { $data['variable_value'] = ''; } } return $data; }) ->reorderable(false)->addable(false)->deletable(false) ->schema(function () { $text = TextInput::make('variable_value') ->hidden($this->shouldHideComponent(...)) ->required(fn (ServerVariable $serverVariable) => $serverVariable->variable->getRequiredAttribute()) ->rules([ fn (ServerVariable $serverVariable): Closure => function (string $attribute, $value, Closure $fail) use ($serverVariable) { $validator = Validator::make(['validatorkey' => $value], [ 'validatorkey' => $serverVariable->variable->rules, ]); if ($validator->fails()) { $message = str($validator->errors()->first())->replace('validatorkey', $serverVariable->variable->name); $fail($message); } }, ]); $select = Select::make('variable_value') ->hidden($this->shouldHideComponent(...)) ->options($this->getSelectOptionsFromRules(...)) ->selectablePlaceholder(false); $components = [$text, $select]; foreach ($components as &$component) { $component = $component ->live(onBlur: true) ->hintIcon('tabler-code') ->label(fn (ServerVariable $serverVariable) => $serverVariable->variable->name) ->hintIconTooltip(fn (ServerVariable $serverVariable) => implode('|', $serverVariable->variable->rules)) ->prefix(fn (ServerVariable $serverVariable) => '{{' . $serverVariable->variable->env_variable . '}}') ->helperText(fn (ServerVariable $serverVariable) => empty($serverVariable->variable->description) ? '—' : $serverVariable->variable->description); } return $components; }) ->columnSpan(6), ]), Tab::make(trans('admin/server.mounts')) ->icon('tabler-layers-linked') ->schema(fn (Get $get) => [ ServerResource::getMountCheckboxList($get), ]), Tab::make(trans('admin/server.databases')) ->hidden(fn () => !auth()->user()->can('viewList database')) ->icon('tabler-database') ->columns(4) ->schema([ Repeater::make('databases') ->label('') ->grid() ->helperText(fn (Server $server) => $server->databases->isNotEmpty() ? '' : trans('admin/server.no_databases')) ->columns(2) ->schema([ TextInput::make('host') ->label(trans('admin/databasehost.table.host')) ->disabled() ->formatStateUsing(fn ($record) => $record->address()) ->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null) ->columnSpan(1), TextInput::make('database') ->label(trans('admin/databasehost.table.database')) ->disabled() ->formatStateUsing(fn ($record) => $record->database) ->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null) ->hintAction( Action::make('Delete') ->label(trans('filament-actions::delete.single.modal.actions.delete.label')) ->authorize(fn (Database $database) => auth()->user()->can('delete database', $database)) ->color('danger') ->icon('tabler-trash') ->requiresConfirmation() ->modalIcon('tabler-database-x') ->modalHeading(trans('admin/server.delete_db_heading')) ->modalSubmitActionLabel(fn (Get $get) => 'Delete ' . $get('database') . '?') ->modalDescription(fn (Get $get) => trans('admin/server.delete_db') . $get('database') . '?') ->action(function (DatabaseManagementService $databaseManagementService, $record) { $databaseManagementService->delete($record); $this->fillForm(); }) ), TextInput::make('username') ->label(trans('admin/databasehost.table.username')) ->disabled() ->formatStateUsing(fn ($record) => $record->username) ->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null) ->columnSpan(1), TextInput::make('password') ->label(trans('admin/databasehost.table.password')) ->disabled() ->password() ->revealable() ->columnSpan(1) ->hintAction(RotateDatabasePasswordAction::make()) ->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null) ->formatStateUsing(fn (Database $database) => $database->password), TextInput::make('remote') ->disabled() ->formatStateUsing(fn (Database $record) => $record->remote === '%' ? 'Anywhere ( % )' : $record->remote) ->columnSpan(1) ->label(trans('admin/databasehost.table.remote')), TextInput::make('max_connections') ->label(trans('admin/databasehost.table.max_connections')) ->disabled() ->formatStateUsing(fn (Database $record) => $record->max_connections === 0 ? 'Unlimited' : $record->max_connections) ->columnSpan(1), TextInput::make('jdbc') ->disabled() ->password() ->revealable() ->label(trans('admin/databasehost.table.connection_string')) ->columnSpan(2) ->formatStateUsing(fn (Database $record) => $record->jdbc) ->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null), ]) ->relationship('databases') ->deletable(false) ->addable(false) ->columnSpan(4), FormActions::make([ Action::make('createDatabase') ->authorize(fn () => auth()->user()->can('create database')) ->disabled(fn () => DatabaseHost::query()->count() < 1) ->label(fn () => DatabaseHost::query()->count() < 1 ? trans('admin/server.no_db_hosts') : trans('admin/server.create_database')) ->color(fn () => DatabaseHost::query()->count() < 1 ? 'danger' : 'primary') ->modalSubmitActionLabel(trans('admin/server.create_database')) ->action(function (array $data, DatabaseManagementService $service, Server $server, RandomWordService $randomWordService) { if (empty($data['database'])) { $data['database'] = $randomWordService->word() . random_int(1, 420); } if (empty($data['remote'])) { $data['remote'] = '%'; } $data['database'] = $service->generateUniqueDatabaseName($data['database'], $server->id); try { $service->setValidateDatabaseLimit(false)->create($server, $data); } catch (Exception $e) { Notification::make() ->title(trans('admin/server.failed_to_create')) ->body($e->getMessage()) ->danger() ->persistent()->send(); } $this->fillForm(); }) ->form([ Select::make('database_host_id') ->label(trans('admin/databasehost.table.name')) ->required() ->placeholder('Select Database Host') ->options(fn (Server $server) => DatabaseHost::query() ->whereHas('nodes', fn ($query) => $query->where('nodes.id', $server->node_id)) ->pluck('name', 'id') ) ->default(fn () => (DatabaseHost::query()->first())?->id) ->selectablePlaceholder(false), TextInput::make('database') ->label(trans('admin/server.name')) ->alphaDash() ->prefix(fn (Server $server) => 's' . $server->id . '_') ->hintIcon('tabler-question-mark') ->hintIconTooltip(trans('admin/databasehost.table.name_helper')), TextInput::make('remote') ->columnSpan(1) ->regex('/^[\w\-\/.%:]+$/') ->label(trans('admin/databasehost.table.remote')) ->hintIcon('tabler-question-mark') ->hintIconTooltip(trans('admin/databasehost.table.remote_helper')), ]), ])->alignCenter()->columnSpanFull(), ]), Tab::make(trans('admin/server.actions')) ->icon('tabler-settings') ->schema([ Fieldset::make(trans('admin/server.actions')) ->columns([ 'default' => 1, 'sm' => 2, 'md' => 2, 'lg' => 6, ]) ->schema([ Grid::make() ->columnSpan(3) ->schema([ FormActions::make([ Action::make('toggleInstall') ->label(trans('admin/server.toggle_install')) ->disabled(fn (Server $server) => $server->isSuspended()) ->modal(fn (Server $server) => $server->status === ServerState::InstallFailed) ->modalHeading(trans('admin/server.toggle_install_failed_header')) ->modalDescription(trans('admin/server.toggle_install_failed_desc')) ->modalSubmitActionLabel(trans('admin/server.reinstall')) ->action(function (ToggleInstallService $toggleService, ReinstallServerService $reinstallService, Server $server) { if ($server->status === ServerState::InstallFailed) { try { $reinstallService->handle($server); Notification::make() ->title(trans('admin/server.notifications.reinstall_started')) ->success() ->send(); $this->refreshFormData(['status', 'docker']); } catch (Exception) { Notification::make() ->title(trans('admin/server.notifications.reinstall_failed')) ->body(trans('admin/server.error_connecting', ['node' => $server->node->name])) ->danger() ->send(); } } else { try { $toggleService->handle($server); Notification::make() ->title(trans('admin/server.notifications.install_toggled')) ->success() ->send(); $this->refreshFormData(['status', 'docker']); } catch (Exception $exception) { Notification::make() ->title(trans('admin/server.notifications.install_toggle_failed')) ->body($exception->getMessage()) ->danger() ->send(); } } }), ])->fullWidth(), ToggleButtons::make('') ->hint(trans('admin/server.toggle_install_help')), ]), Grid::make() ->columnSpan(3) ->schema([ FormActions::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(); $this->refreshFormData(['status', 'docker']); } catch (Exception) { Notification::make() ->warning() ->title(trans('admin/server.notifications.server_suspension')) ->body(trans('admin/server.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(); $this->refreshFormData(['status', 'docker']); } catch (Exception) { Notification::make() ->warning() ->title(trans('admin/server.notifications.server_suspension')) ->body(trans('admin/server.error_connecting', ['node' => $server->node->name])) ->send(); } }), ])->fullWidth(), ToggleButtons::make('') ->hidden(fn (Server $server) => $server->isSuspended()) ->hint(trans('admin/server.notifications.server_suspend_help')), ToggleButtons::make('') ->hidden(fn (Server $server) => !$server->isSuspended()) ->hint(trans('admin/server.notifications.server_unsuspend_help')), ]), Grid::make() ->columnSpan(3) ->schema([ FormActions::make([ Action::make('transfer') ->label(trans('admin/server.transfer')) ->disabled(fn (Server $server) => Node::count() <= 1 || $server->isInConflictState()) ->modalheading(trans('admin/server.transfer')) ->form($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('Transfer started') ->success() ->send(); } catch (Exception $exception) { Notification::make() ->title('Transfer failed') ->body($exception->getMessage()) ->danger() ->send(); } }), ])->fullWidth(), ToggleButtons::make('') ->hint(new HtmlString(trans('admin/server.transfer_help'))), ]), Grid::make() ->columnSpan(3) ->schema([ FormActions::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(); $this->refreshFormData(['status', 'docker']); } catch (Exception) { Notification::make() ->title(trans('admin/server.notifications.reinstall_failed')) ->body(trans('admin/server.error_connecting', ['node' => $server->node->name])) ->danger() ->send(); } }), ])->fullWidth(), ToggleButtons::make('') ->hint(trans('admin/server.reinstall_help')), ]), ]), ]), ]), ]); } /** @return Component[] */ protected function transferServer(): array { return [ Select::make('node_id') ->label(trans('admin/server.node')) ->prefixIcon('tabler-server-2') ->selectablePlaceholder(false) ->default(fn (Server $server) => Node::whereNot('id', $server->node->id)->first()?->id) ->required() ->live() ->options(fn (Server $server) => Node::whereNot('id', $server->node->id)->pluck('name', 'id')->all()), Select::make('allocation_id') ->label(trans('admin/server.primary_allocation')) ->required() ->prefixIcon('tabler-network') ->disabled(fn (Get $get) => !$get('node_id')) ->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')) ->multiple() ->prefixIcon('tabler-network') ->disabled(fn (Get $get) => !$get('node_id')) ->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')), ]; } protected function getHeaderActions(): array { /** @var Server $server */ $server = $this->getRecord(); $canForceDelete = cache()->get("servers.$server->uuid.canForceDelete", false); return [ Actions\Action::make('Delete') ->color('danger') ->label(trans('filament-actions::delete.single.label')) ->modalHeading(trans('filament-actions::delete.single.modal.heading', ['label' => $this->getRecordTitle()])) ->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)); 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) => auth()->user()->can('delete server', $server)), Actions\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()])) ->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) { cache()->forget("servers.$server->uuid.canForceDelete"); } }) ->visible(fn () => $canForceDelete) ->authorize(fn (Server $server) => auth()->user()->can('delete server', $server)), Actions\Action::make('console') ->label(trans('admin/server.console')) ->icon('tabler-terminal') ->url(fn (Server $server) => Console::getUrl(panel: 'server', tenant: $server)), $this->getSaveFormAction()->formId('form'), ]; } 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 [ AllocationsRelationManager::class, ]; } private function shouldHideComponent(ServerVariable $serverVariable, Forms\Components\Component $component): bool { $containsRuleIn = array_first($serverVariable->variable->rules, fn ($value) => str($value)->startsWith('in:'), false); if ($component instanceof Select) { return !$containsRuleIn; } if ($component instanceof TextInput) { return $containsRuleIn; } throw new Exception('Component type not supported: ' . $component::class); } /** * @return array */ private function getSelectOptionsFromRules(ServerVariable $serverVariable): array { $inRule = array_first($serverVariable->variable->rules, fn ($value) => str($value)->startsWith('in:')); return str($inRule) ->after('in:') ->explode(',') ->each(fn ($value) => str($value)->trim()) ->mapWithKeys(fn ($value) => [$value => $value]) ->all(); } }