Repository: pelican-dev/panel
Pull request #1119: Add UI for server transfers
Author: login: Boy132
Created: 2025-03-18T09:13:24Z | Merged: 2025-03-23T21:02:22Z

Closes #289

Allocations probably need more validation but for now I think it's fine.
In the future we should replace the Selects with a Repeater that shows all old allocations and options to "connect" them to new allocations.

_Note: I have only tested the UI itself, not the actual transfer because I don't have a setup with more than one node._

Linked issue #289: Implement Server Transfers
This would add a new tab or section similar to the old interface
Linked issue comments: user: login: notAreYouScared
created_at: 2024-06-17T16:26:15Z
body: This is on hold until allocations are re-worked. 
author_association: MEMBER
user: login: gru2007
created_at: 2024-06-28T02:05:17Z
body: I see progress has been slowed down. What happend?
author_association: NONE
user: login: notAreYouScared
created_at: 2024-06-28T09:41:42Z
body: Server transfers are on hold until allocations are reworked.
author_association: MEMBER
user: login: ilmal
created_at: 2024-12-11T17:09:12Z
body: What is the status on this? Still on hold?
author_association: NONE

Diff:
diff --git a/app/Filament/Admin/Resources/ServerResource/Pages/EditServer.php b/app/Filament/Admin/Resources/ServerResource/Pages/EditServer.php
index 11ae0aea6a..f2139a0b92 100644
--- a/app/Filament/Admin/Resources/ServerResource/Pages/EditServer.php
+++ b/app/Filament/Admin/Resources/ServerResource/Pages/EditServer.php
@@ -8,10 +8,12 @@
 use App\Filament\Components\Forms\Actions\PreviewStartupAction;
 use App\Filament\Components\Forms\Actions\RotateDatabasePasswordAction;
 use App\Filament\Server\Pages\Console;
+use App\Models\Allocation;
 use App\Models\Database;
 use App\Models\DatabaseHost;
 use App\Models\Egg;
 use App\Models\Mount;
+use App\Models\Node;
 use App\Models\Server;
 use App\Models\ServerVariable;
 use App\Models\User;
@@ -52,6 +54,7 @@
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Http\Client\ConnectionException;
 use Illuminate\Support\Facades\Validator;
+use Illuminate\Support\HtmlString;
 use LogicException;
 use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
 
@@ -858,34 +861,53 @@ public function form(Form $form): Form
                                                 Forms\Components\Actions::make([
                                                     Action::make('transfer')
                                                         ->label(trans('admin/server.transfer'))
-                                                        // ->action(fn (TransferServerService $transfer, Server $server) => $transfer->handle($server, []))
-                                                        ->disabled() //TODO!
-                                                        ->form([ //TODO!
-                                                            Select::make('newNode')
-                                                                ->label('New Node')
+                                                        ->disabled(fn (Server $server) => Node::count() <= 1 || $server->isInConflictState())
+                                                        ->modalheading(trans('admin/server.transfer'))
+                                                        ->action(function (TransferServerService $transfer, Server $server, $data) {
+                                                            try {
+                                                                $transfer->handle($server, $data);
+
+                                                                Notification::make()
+                                                                    ->title('Transfer started')
+                                                                    ->success()
+                                                                    ->send();
+                                                            } catch (Exception $exception) {
+                                                                Notification::make()
+                                                                    ->title('Transfer failed')
+                                                                    ->body($exception->getMessage())
+                                                                    ->danger()
+                                                                    ->send();
+                                                            }
+                                                        })
+                                                        ->form([
+                                                            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()
-                                                                ->options([
-                                                                    true => 'on',
-                                                                    false => 'off',
-                                                                ]),
-                                                            Select::make('newMainAllocation')
-                                                                ->label('New Main Allocation')
+                                                                ->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()
-                                                                ->options([
-                                                                    true => 'on',
-                                                                    false => 'off',
-                                                                ]),
-                                                            Select::make('newAdditionalAllocation')
-                                                                ->label('New Additional Allocations')
-                                                                ->options([
-                                                                    true => 'on',
-                                                                    false => 'off',
-                                                                ]),
-                                                        ])
-                                                        ->modalheading(trans('admin/server.transfer')),
+                                                                ->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')),
+                                                        ]),
                                                 ])->fullWidth(),
                                                 ToggleButtons::make('')
-                                                    ->hint(trans('admin/server.transfer_help')),
+                                                    ->hint(new HtmlString(trans('admin/server.transfer_help'))),
                                             ]),
                                         Grid::make()
                                             ->columnSpan(3)
diff --git a/lang/en/admin/server.php b/lang/en/admin/server.php
index 893979f464..383c2ed899 100644
--- a/lang/en/admin/server.php
+++ b/lang/en/admin/server.php
@@ -70,7 +70,7 @@
     'toggle_install' => 'Toggle Install Status',
     'toggle_install_help' => 'If you need to change the install status from uninstalled to installed, or vice versa, you may do so with this button.',
     'transfer' => 'Transfer',
-    'transfer_help' => 'Transfer this server to another node connected to this panel. Warning! This feature has not been fully tested and may have bugs.',
+    'transfer_help' => 'Transfer this server to another node connected to this panel.<br/><b>Warning!</b> This feature is still experimental. Consider manually making a backup first to avoid data loss!',
     'condition' => 'Condition',
     'suspend_all' => 'Suspend All Servers',
     'unsuspend_all' => 'Unsuspend All Servers',


Reviews:
user: login: rmartinoscar
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T12:36:36Z
user: login: Boy132
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T13:31:23Z
user: login: rmartinoscar
body: We should fix the backend before merging this so we can push a beta before next year.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-18T13:43:44Z
user: login: lancepioch
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-18T15:25:47Z

Review comments:
diff_hunk: @@ -858,34 +861,53 @@ public function form(Form $form): Form
                                                 Forms\Components\Actions::make([
                                                     Action::make('transfer')
                                                         ->label(trans('admin/server.transfer'))
-                                                        // ->action(fn (TransferServerService $transfer, Server $server) => $transfer->handle($server, []))
-                                                        ->disabled() //TODO!
-                                                        ->form([ //TODO!
-                                                            Select::make('newNode')
-                                                                ->label('New Node')
+                                                        ->disabled(fn () => Node::count() <= 1)
path: app/Filament/Admin/Resources/ServerResource/Pages/EditServer.php
user: login: rmartinoscar
body: Disable it when a transfer has already been asked and possibly display new node name.
Or you can change the action to use `cancelTransfer()`
created_at: 2025-03-18T12:36:35Z
author_association: MEMBER
diff_hunk: @@ -858,34 +861,53 @@ public function form(Form $form): Form
                                                 Forms\Components\Actions::make([
                                                     Action::make('transfer')
                                                         ->label(trans('admin/server.transfer'))
-                                                        // ->action(fn (TransferServerService $transfer, Server $server) => $transfer->handle($server, []))
-                                                        ->disabled() //TODO!
-                                                        ->form([ //TODO!
-                                                            Select::make('newNode')
-                                                                ->label('New Node')
+                                                        ->disabled(fn () => Node::count() <= 1)
path: app/Filament/Admin/Resources/ServerResource/Pages/EditServer.php
user: login: Boy132
body: For now I added a check on the conflict state.
Later we can add a "Cancel Transfer" button.
created_at: 2025-03-18T13:31:23Z
author_association: MEMBER

PR comments:
user: login: SirEndii
created_at: 2025-03-18T09:22:22Z
body: I can test the transfer for you but I first need to build the docker image.
author_association: NONE
user: login: rmartinoscar
created_at: 2025-03-18T12:42:10Z
body: I created a server with `0.0.0.0:10000` then transferred it to my other node with allocation `0.0.0.0:2000` now the server has both
![image](https://github.com/user-attachments/assets/1a6d5271-688f-4f39-8487-92a9a787c731)
but Node is still the first one, the server `isInConflictState` but neither wings received any request on the transfer endpoint
![image](https://github.com/user-attachments/assets/5ffeb97a-aa52-4b9e-b319-96154e668300)
![image](https://github.com/user-attachments/assets/260310f7-abba-415f-a389-2cb632af8876)
![image](https://github.com/user-attachments/assets/20e91acc-dc47-4b9f-bc44-69478dd07320)
(It also has null status)
author_association: MEMBER
user: login: Boy132
created_at: 2025-03-18T13:28:35Z
body: > I created a server with `0.0.0.0:10000` then transferred it to my other node with allocation `0.0.0.0:2000` now the server has both ![image](https://private-user-images.githubusercontent.com/40749467/423980734-1a6d5271-688f-4f39-8487-92a9a787c731.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDIzMDQ3NTQsIm5iZiI6MTc0MjMwNDQ1NCwicGF0aCI6Ii80MDc0OTQ2Ny80MjM5ODA3MzQtMWE2ZDUyNzEtNjg4Zi00ZjM5LTg0ODctOTJhOWE3ODdjNzMxLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAzMTglMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMzE4VDEzMjczNFomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTcyYTBmNTU5ZjYzZDEyMzExYzA3MDQ1ZWE1Y2QwYzBjYTEyZTRjNTFmZWE4YzlmOWI4YTQyZWIwYTM1OTUyY2MmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.e6HPU6q4SdxXfiEZnFIWrT6jgZNR9muznZCcycGyBWg) but Node is still the first one, the server `isInConflictState` but neither wings received any request on the transfer endpoint ![image](https://private-user-images.githubusercontent.com/40749467/423982431-5ffeb97a-aa52-4b9e-b319-96154e668300.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDIzMDQ3NTQsIm5iZiI6MTc0MjMwNDQ1NCwicGF0aCI6Ii80MDc0OTQ2Ny80MjM5ODI0MzEtNWZmZWI5N2EtYWE1Mi00YjllLWIzMTktOTYxNTRlNjY4MzAwLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAzMTglMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMzE4VDEzMjczNFomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWYzYWZhMGQ5YWZhMTY4NjcwNDVjYzc5NWVkZDg4YmQ0NDU4MGVjMjFlYWRhMzcyNDEyMDE2MTA5NjUwMDdhODkmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.bIxujASeTHmbjq1rMoa1wujkFTgkqcRNWDEe_Ra8fKs) ![image](https://private-user-images.githubusercontent.com/40749467/423982551-260310f7-abba-415f-a389-2cb632af8876.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDIzMDQ3NTQsIm5iZiI6MTc0MjMwNDQ1NCwicGF0aCI6Ii80MDc0OTQ2Ny80MjM5ODI1NTEtMjYwMzEwZjctYWJiYS00MTVmLWEzODktMmNiNjMyYWY4ODc2LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAzMTglMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMzE4VDEzMjczNFomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTUyMmQ5OGYxM2U5ZTkyYWFjNzBmMDk3MDY4ODgzMDYwZDk4YmIwMTcwZTM1OWM4MTU5NjQ5ZWE5NzViNWU1NDkmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.sMgCOIsrxBENm48F5lA9hJ27KgHfn3XJKZf06Be8hAc) ![image](https://private-user-images.githubusercontent.com/40749467/423982719-20e91acc-dc47-4b9f-bc44-69478dd07320.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDIzMDQ3NTQsIm5iZiI6MTc0MjMwNDQ1NCwicGF0aCI6Ii80MDc0OTQ2Ny80MjM5ODI3MTktMjBlOTFhY2MtZGM0Ny00YjlmLWJjNDQtNjk0NzhkZDA3MzIwLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAzMTglMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMzE4VDEzMjczNFomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTdiMDJjMGYwYjg1NzVhZDFiN2Y4MmQzNTkxMDk5MGUxMzA4MTQwODE5ODY2OTBmMjM3OGI2M2M5OTdiZjJiZmQmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.vFaWLOqb03lz8fXNfw6PCqtmi9DMXb8Gk5fTD3LzWpM) (It also has null status)

Yeah, that's just the backend being broken. This PR is just the frontend.
author_association: MEMBER