From e6c4a68e4a0974bb19f59bcad2337d0dadd37d17 Mon Sep 17 00:00:00 2001 From: Matthew Penner Date: Wed, 16 Dec 2020 09:34:47 -0700 Subject: [PATCH] Update logic for tracking a server's transfer state --- .../Servers/ServerTransferController.php | 2 +- .../Servers/ServerTransferController.php | 27 +++++---- .../Remote/SftpAuthenticationController.php | 4 +- .../Server/AuthenticateServerAccess.php | 23 +++++--- .../Server/AccessingValidServer.php | 8 +++ app/Models/Server.php | 2 +- app/Models/ServerTransfer.php | 4 +- .../Servers/GetUserPermissionsService.php | 1 + app/Services/Servers/SuspensionService.php | 5 +- .../Api/Application/ServerTransformer.php | 3 +- .../Api/Client/ServerTransformer.php | 1 + ...uccessful_nullable_in_server_transfers.php | 32 ++++++++++ resources/scripts/api/server/getServer.ts | 2 + .../components/dashboard/ServerRow.tsx | 23 +++++--- .../components/elements/ProgressBar.tsx | 2 +- .../scripts/components/server/Console.tsx | 30 ++++++++-- .../components/server/ServerConsole.tsx | 21 +++++-- .../components/server/ServerDetailsBlock.tsx | 13 ++-- resources/scripts/routers/ServerRouter.tsx | 18 +++++- .../views/admin/servers/view/manage.blade.php | 59 +++++++++++++------ 20 files changed, 206 insertions(+), 74 deletions(-) create mode 100644 database/migrations/2020_12_14_013707_make_successful_nullable_in_server_transfers.php diff --git a/app/Http/Controllers/Admin/Servers/ServerTransferController.php b/app/Http/Controllers/Admin/Servers/ServerTransferController.php index 323ffb7bc..62971bd99 100644 --- a/app/Http/Controllers/Admin/Servers/ServerTransferController.php +++ b/app/Http/Controllers/Admin/Servers/ServerTransferController.php @@ -117,7 +117,7 @@ class ServerTransferController extends Controller $this->daemonConfigurationRepository->setNode($node)->getSystemInformation(); // Suspend the server and request an archive to be created. - $this->suspensionService->toggle($server, 'suspend'); + //$this->suspensionService->toggle($server, 'suspend'); // Create a new ServerTransfer entry. $transfer = new ServerTransfer; diff --git a/app/Http/Controllers/Api/Remote/Servers/ServerTransferController.php b/app/Http/Controllers/Api/Remote/Servers/ServerTransferController.php index bd8827a6e..20f396805 100644 --- a/app/Http/Controllers/Api/Remote/Servers/ServerTransferController.php +++ b/app/Http/Controllers/Api/Remote/Servers/ServerTransferController.php @@ -120,9 +120,12 @@ class ServerTransferController extends Controller // Unsuspend the server and don't continue the transfer. if (! $request->input('successful')) { - $this->suspensionService->toggle($server, 'unsuspend'); + //$this->suspensionService->toggle($server, 'unsuspend'); + $server->transfer->forceFill([ + 'successful' => false, + ])->saveOrFail(); - return JsonResponse::create([], Response::HTTP_NO_CONTENT); + return new JsonResponse([], Response::HTTP_NO_CONTENT); } $server->node_id = $server->transfer->new_node; @@ -151,21 +154,23 @@ class ServerTransferController extends Controller // because setServer() tells the repository to use the server's node and not the one // we want to specify. try { + /** @var \Pterodactyl\Models\Node $newNode */ + $newNode = $this->nodeRepository->find($server->transfer->new_node); + $this->daemonTransferRepository ->setServer($server) - ->setNode($this->nodeRepository->find($server->transfer->new_node)) + ->setNode($newNode) ->notify($server, $data, $server->node, $token->__toString()); } catch (DaemonConnectionException $exception) { throw $exception; } - return JsonResponse::create([], Response::HTTP_NO_CONTENT); + return new JsonResponse([], Response::HTTP_NO_CONTENT); } /** * The daemon notifies us about a transfer failure. * - * @param \Illuminate\Http\Request $request * @param string $uuid * @return \Illuminate\Http\JsonResponse * @@ -183,9 +188,9 @@ class ServerTransferController extends Controller $this->allocationRepository->updateWhereIn('id', $allocationIds, ['server_id' => null]); // Unsuspend the server. - $this->suspensionService->toggle($server, 'unsuspend'); + //$this->suspensionService->toggle($server, 'unsuspend'); - return JsonResponse::create([], Response::HTTP_NO_CONTENT); + return new JsonResponse([], Response::HTTP_NO_CONTENT); } /** @@ -213,11 +218,11 @@ class ServerTransferController extends Controller // Update the server's allocation_id and node_id. $server->allocation_id = $transfer->new_allocation; $server->node_id = $transfer->new_node; - $server->save(); + $server->saveOrFail(); // Mark the transfer as successful. $transfer->successful = true; - $transfer->save(); + $transfer->saveOrFail(); // Commit the transaction. $this->connection->commit(); @@ -231,8 +236,8 @@ class ServerTransferController extends Controller // Unsuspend the server $server->load('node'); - $this->suspensionService->toggle($server, $this->suspensionService::ACTION_UNSUSPEND); + //$this->suspensionService->toggle($server, $this->suspensionService::ACTION_UNSUSPEND); - return JsonResponse::create([], Response::HTTP_NO_CONTENT); + return new JsonResponse([], Response::HTTP_NO_CONTENT); } } diff --git a/app/Http/Controllers/Api/Remote/SftpAuthenticationController.php b/app/Http/Controllers/Api/Remote/SftpAuthenticationController.php index b62d6e5c1..7fb5625f4 100644 --- a/app/Http/Controllers/Api/Remote/SftpAuthenticationController.php +++ b/app/Http/Controllers/Api/Remote/SftpAuthenticationController.php @@ -112,13 +112,13 @@ class SftpAuthenticationController extends Controller // Remeber, for security purposes, only reveal the existence of the server to people that // have provided valid credentials, and have permissions to know about it. - if ($server->installed !== 1 || $server->suspended) { + if ($server->installed !== 1 || $server->suspended || $server->transfer !== null) { throw new BadRequestHttpException( 'Server is not installed or is currently suspended.' ); } - return JsonResponse::create([ + return new JsonResponse([ 'server' => $server->uuid, // Deprecated, but still needed at the moment for Wings. 'token' => '', diff --git a/app/Http/Middleware/Api/Client/Server/AuthenticateServerAccess.php b/app/Http/Middleware/Api/Client/Server/AuthenticateServerAccess.php index 505f1a305..eee798198 100644 --- a/app/Http/Middleware/Api/Client/Server/AuthenticateServerAccess.php +++ b/app/Http/Middleware/Api/Client/Server/AuthenticateServerAccess.php @@ -9,7 +9,6 @@ use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Symfony\Component\HttpKernel\Exception\ConflictHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; -use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; class AuthenticateServerAccess { @@ -24,7 +23,6 @@ class AuthenticateServerAccess * @var string[] */ protected $except = [ - 'api:client:server.view', 'api:client:server.ws', ]; @@ -65,17 +63,26 @@ class AuthenticateServerAccess } } - if ($server->suspended && !$request->routeIs('api:client:server.resources')) { + if ($server->suspended && ! $request->routeIs('api:client:server.resources')) { throw new BadRequestHttpException( 'This server is currently suspended and the functionality requested is unavailable.' ); } - if (! $server->isInstalled()) { - // Throw an exception for all server routes; however if the user is an admin and requesting the - // server details, don't throw the exception for them. - if (! $user->root_admin || ($user->root_admin && ! $request->routeIs($this->except))) { - throw new ConflictHttpException('Server has not completed the installation process.'); + // Still allow users to get information about there server if it is installing or being transferred. + if (! $request->routeIs('api:client:server.view')) { + if (! $server->isInstalled()) { + // Throw an exception for all server routes; however if the user is an admin and requesting the + // server details, don't throw the exception for them. + if (! $user->root_admin || ($user->root_admin && ! $request->routeIs($this->except))) { + throw new ConflictHttpException('Server has not completed the installation process.'); + } + } + + if ($server->transfer !== null) { + if (! $user->root_admin || ($user->root_admin && ! $request->routeIs($this->except))) { + throw new ConflictHttpException('Server is currently being transferred.'); + } } } diff --git a/app/Http/Middleware/Server/AccessingValidServer.php b/app/Http/Middleware/Server/AccessingValidServer.php index 1f464f7df..1bed5f556 100644 --- a/app/Http/Middleware/Server/AccessingValidServer.php +++ b/app/Http/Middleware/Server/AccessingValidServer.php @@ -80,6 +80,14 @@ class AccessingValidServer return $this->response->view('errors.installing', [], 409); } + if ($server->transfer !== null) { + if ($isApiRequest) { + throw new ConflictHttpException('Server is currently being transferred.'); + } + + return $this->response->view('errors.transferring', [], 409); + } + // Add server to the request attributes. This will replace sessions // as files are updated. $request->attributes->set('server', $server); diff --git a/app/Models/Server.php b/app/Models/Server.php index c8b8615fe..aace86d0b 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -306,7 +306,7 @@ class Server extends Model */ public function transfer() { - return $this->hasOne(ServerTransfer::class)->orderByDesc('id'); + return $this->hasOne(ServerTransfer::class)->whereNull('successful')->orderByDesc('id'); } /** diff --git a/app/Models/ServerTransfer.php b/app/Models/ServerTransfer.php index 8d7120645..48f6b8de8 100644 --- a/app/Models/ServerTransfer.php +++ b/app/Models/ServerTransfer.php @@ -11,7 +11,7 @@ namespace Pterodactyl\Models; * @property int $new_allocation * @property string $old_additional_allocations * @property string $new_additional_allocations - * @property bool $successful + * @property bool|null $successful * @property \Carbon\Carbon $created_at * @property \Carbon\Carbon $updated_at * @@ -66,7 +66,7 @@ class ServerTransfer extends Model 'new_allocation' => 'required|numeric', 'old_additional_allocations' => 'nullable', 'new_additional_allocations' => 'nullable', - 'successful' => 'sometimes|boolean', + 'successful' => 'sometimes|nullable|boolean', ]; /** diff --git a/app/Services/Servers/GetUserPermissionsService.php b/app/Services/Servers/GetUserPermissionsService.php index e0ea20373..80ce35b52 100644 --- a/app/Services/Servers/GetUserPermissionsService.php +++ b/app/Services/Servers/GetUserPermissionsService.php @@ -24,6 +24,7 @@ class GetUserPermissionsService if ($user->root_admin) { $permissions[] = 'admin.websocket.errors'; $permissions[] = 'admin.websocket.install'; + $permissions[] = 'admin.websocket.transfer'; } return $permissions; diff --git a/app/Services/Servers/SuspensionService.php b/app/Services/Servers/SuspensionService.php index 6497d73ed..200ef9b65 100644 --- a/app/Services/Servers/SuspensionService.php +++ b/app/Services/Servers/SuspensionService.php @@ -61,7 +61,10 @@ class SuspensionService 'suspended' => $action === self::ACTION_SUSPEND, ]); - $this->daemonServerRepository->setServer($server)->suspend($action === self::ACTION_UNSUSPEND); + // Only send the suspension request to wings if the server is not currently being transferred. + if ($server->transfer === null) { + $this->daemonServerRepository->setServer($server)->suspend($action === self::ACTION_UNSUSPEND); + } }); } } diff --git a/app/Transformers/Api/Application/ServerTransformer.php b/app/Transformers/Api/Application/ServerTransformer.php index e2c32eb76..10c343d8c 100644 --- a/app/Transformers/Api/Application/ServerTransformer.php +++ b/app/Transformers/Api/Application/ServerTransformer.php @@ -28,6 +28,7 @@ class ServerTransformer extends BaseTransformer 'location', 'node', 'databases', + 'transfer', ]; /** @@ -55,8 +56,6 @@ class ServerTransformer extends BaseTransformer * * @param \Pterodactyl\Models\Server $server * @return array - * - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function transform(Server $server): array { diff --git a/app/Transformers/Api/Client/ServerTransformer.php b/app/Transformers/Api/Client/ServerTransformer.php index 9897f8517..766b3b726 100644 --- a/app/Transformers/Api/Client/ServerTransformer.php +++ b/app/Transformers/Api/Client/ServerTransformer.php @@ -72,6 +72,7 @@ class ServerTransformer extends BaseClientTransformer ], 'is_suspended' => $server->suspended, 'is_installing' => $server->installed !== 1, + 'is_transferring' => $server->transfer !== null, ]; } diff --git a/database/migrations/2020_12_14_013707_make_successful_nullable_in_server_transfers.php b/database/migrations/2020_12_14_013707_make_successful_nullable_in_server_transfers.php new file mode 100644 index 000000000..0a2885284 --- /dev/null +++ b/database/migrations/2020_12_14_013707_make_successful_nullable_in_server_transfers.php @@ -0,0 +1,32 @@ +boolean('successful')->nullable()->default(null)->change(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('server_transfers', function (Blueprint $table) { + $table->boolean('successful')->default(0)->change(); + }); + } +} diff --git a/resources/scripts/api/server/getServer.ts b/resources/scripts/api/server/getServer.ts index d9b76b400..959149f9d 100644 --- a/resources/scripts/api/server/getServer.ts +++ b/resources/scripts/api/server/getServer.ts @@ -40,6 +40,7 @@ export interface Server { }; isSuspended: boolean; isInstalling: boolean; + isTransferring: boolean; variables: ServerEggVariable[]; allocations: Allocation[]; } @@ -62,6 +63,7 @@ export const rawDataToServerObject = ({ attributes: data }: FractalResponseData) featureLimits: { ...data.feature_limits }, isSuspended: data.is_suspended, isInstalling: data.is_installing, + isTransferring: data.is_transferring, variables: ((data.relationships?.variables as FractalResponseList | undefined)?.data || []).map(rawDataToServerEggVariable), allocations: ((data.relationships?.allocations as FractalResponseList | undefined)?.data || []).map(rawDataToServerAllocation), }); diff --git a/resources/scripts/components/dashboard/ServerRow.tsx b/resources/scripts/components/dashboard/ServerRow.tsx index d42b3e434..7db977311 100644 --- a/resources/scripts/components/dashboard/ServerRow.tsx +++ b/resources/scripts/components/dashboard/ServerRow.tsx @@ -26,14 +26,14 @@ const IconDescription = styled.p<{ $alarm: boolean }>` const StatusIndicatorBox = styled(GreyRowBox)<{ $status: ServerPowerState | undefined }>` ${tw`grid grid-cols-12 gap-4 relative`}; - + & .status-bar { ${tw`w-2 bg-red-500 absolute right-0 z-20 rounded-full m-1 opacity-50 transition-all duration-150`}; height: calc(100% - 0.5rem); - + ${({ $status }) => (!$status || $status === 'offline') ? tw`bg-red-500` : ($status === 'running' ? tw`bg-green-500` : tw`bg-yellow-500`)}; } - + &:hover .status-bar { ${tw`opacity-75`}; } @@ -74,8 +74,8 @@ export default ({ server, className }: { server: Server; className?: string }) = alarms.disk = server.limits.disk === 0 ? false : isAlarmState(stats.diskUsageInBytes, server.limits.disk); } - const disklimit = server.limits.disk !== 0 ? megabytesToHuman(server.limits.disk) : 'Unlimited'; - const memorylimit = server.limits.memory !== 0 ? megabytesToHuman(server.limits.memory) : 'Unlimited'; + const diskLimit = server.limits.disk !== 0 ? megabytesToHuman(server.limits.disk) : 'Unlimited'; + const memoryLimit = server.limits.memory !== 0 ? megabytesToHuman(server.limits.memory) : 'Unlimited'; return ( @@ -118,7 +118,14 @@ export default ({ server, className }: { server: Server; className?: string }) = : - + server.isTransferring ? +
+ + Transferring + +
+ : + : -

of {memorylimit}

+

of {memoryLimit}

@@ -143,7 +150,7 @@ export default ({ server, className }: { server: Server; className?: string }) = {bytesToHuman(stats.diskUsageInBytes)}
-

of {disklimit}

+

of {diskLimit}

} diff --git a/resources/scripts/components/elements/ProgressBar.tsx b/resources/scripts/components/elements/ProgressBar.tsx index 64ee732b7..fe98579bd 100644 --- a/resources/scripts/components/elements/ProgressBar.tsx +++ b/resources/scripts/components/elements/ProgressBar.tsx @@ -59,7 +59,7 @@ export default () => { }, [ progress, continuous ]); return ( -
+
{ (prelude ? TERMINAL_PRELUDE : '') + line.replace(/(?:\r\n|\r|\n)$/im, '') + '\u001b[0m', ); + const handleTransferStatus = (status: string) => { + switch (status) { + // Sent by either the source or target node if a failure occurs. + case 'failure': + terminal.writeln(TERMINAL_PRELUDE + 'Transfer has failed.\u001b[0m'); + return; + + // Sent by the source node whenever the server was archived successfully. + case 'archive': + terminal.writeln(TERMINAL_PRELUDE + 'Server has been archived successfully, attempting connection to target node..\u001b[0m'); + // TODO: Get WebSocket information for the target node. + return; + } + }; + const handleDaemonErrorOutput = (line: string) => terminal.writeln( TERMINAL_PRELUDE + '\u001b[1m\u001b[41m' + line.replace(/(?:\r\n|\r|\n)$/im, '') + '\u001b[0m', ); @@ -122,20 +137,23 @@ export default () => { // Add support for capturing keys terminal.attachCustomKeyEventHandler((e: KeyboardEvent) => { - // Ctrl + C ( Copy ) + // Ctrl + C (Copy) if (e.ctrlKey && e.key === 'c') { document.execCommand('copy'); return false; } + // Ctrl + F (Find) if (e.ctrlKey && e.key === 'f') { searchBar.show(); return false; } + // Escape if (e.key === 'Escape') { searchBar.hidden(); } + return true; }); } @@ -154,17 +172,21 @@ export default () => { instance.addListener('status', handlePowerChangeEvent); instance.addListener('console output', handleConsoleOutput); instance.addListener('install output', handleConsoleOutput); + instance.addListener('transfer logs', handleConsoleOutput); + instance.addListener('transfer status', handleTransferStatus); instance.addListener('daemon message', line => handleConsoleOutput(line, true)); instance.addListener('daemon error', handleDaemonErrorOutput); instance.send('send logs'); } return () => { - instance && instance.removeListener('console output', handleConsoleOutput) + instance && instance.removeListener('status', handlePowerChangeEvent) + .removeListener('console output', handleConsoleOutput) .removeListener('install output', handleConsoleOutput) + .removeListener('transfer logs', handleConsoleOutput) + .removeListener('transfer status', handleTransferStatus) .removeListener('daemon message', line => handleConsoleOutput(line, true)) - .removeListener('daemon error', handleDaemonErrorOutput) - .removeListener('status', handlePowerChangeEvent); + .removeListener('daemon error', handleDaemonErrorOutput); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [ connected, instance ]); diff --git a/resources/scripts/components/server/ServerConsole.tsx b/resources/scripts/components/server/ServerConsole.tsx index 4735488fc..d090e7a95 100644 --- a/resources/scripts/components/server/ServerConsole.tsx +++ b/resources/scripts/components/server/ServerConsole.tsx @@ -17,6 +17,7 @@ const ChunkedStatGraphs = lazy(() => import(/* webpackChunkName: "graphs" */'@/c const ServerConsole = () => { const isInstalling = ServerContext.useStoreState(state => state.server.data!.isInstalling); + const isTransferring = ServerContext.useStoreState(state => state.server.data!.isTransferring); // @ts-ignore const eggFeatures: string[] = ServerContext.useStoreState(state => state.server.data!.eggFeatures, isEqual); @@ -24,11 +25,7 @@ const ServerConsole = () => {
- {!isInstalling ? - - - - : + {isInstalling ?

@@ -37,6 +34,20 @@ const ServerConsole = () => {

+ : + isTransferring ? +
+ +

+ This server is currently being transferred to another node and all actions + are unavailable. +

+
+
+ : + + + }
diff --git a/resources/scripts/components/server/ServerDetailsBlock.tsx b/resources/scripts/components/server/ServerDetailsBlock.tsx index 0d87089c1..11f41f690 100644 --- a/resources/scripts/components/server/ServerDetailsBlock.tsx +++ b/resources/scripts/components/server/ServerDetailsBlock.tsx @@ -65,13 +65,14 @@ const ServerDetailsBlock = () => { const name = ServerContext.useStoreState(state => state.server.data!.name); const isInstalling = ServerContext.useStoreState(state => state.server.data!.isInstalling); + const isTransferring = ServerContext.useStoreState(state => state.server.data!.isTransferring); const limits = ServerContext.useStoreState(state => state.server.data!.limits); const primaryAllocation = ServerContext.useStoreState(state => state.server.data!.allocations.filter(alloc => alloc.isDefault).map( allocation => (allocation.alias || allocation.ip) + ':' + allocation.port )).toString(); - const disklimit = limits.disk ? megabytesToHuman(limits.disk) : 'Unlimited'; - const memorylimit = limits.memory ? megabytesToHuman(limits.memory) : 'Unlimited'; + const diskLimit = limits.disk ? megabytesToHuman(limits.disk) : 'Unlimited'; + const memoryLimit = limits.memory ? megabytesToHuman(limits.memory) : 'Unlimited'; return ( @@ -81,10 +82,10 @@ const ServerDetailsBlock = () => { fixedWidth css={[ tw`mr-1`, - statusToColor(status, isInstalling), + statusToColor(status, isInstalling || isTransferring), ]} /> -  {!status ? 'Connecting...' : (isInstalling ? 'Installing' : status)} +  {!status ? 'Connecting...' : (isInstalling ? 'Installing' : (isTransferring) ? 'Transferring' : status)}

@@ -97,11 +98,11 @@ const ServerDetailsBlock = () => {

{bytesToHuman(stats.memory)} - / {memorylimit} + / {memoryLimit}

 {bytesToHuman(stats.disk)} - / {disklimit} + / {diskLimit}

); diff --git a/resources/scripts/routers/ServerRouter.tsx b/resources/scripts/routers/ServerRouter.tsx index 0a09774aa..7405a6e4e 100644 --- a/resources/scripts/routers/ServerRouter.tsx +++ b/resources/scripts/routers/ServerRouter.tsx @@ -35,10 +35,12 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) const rootAdmin = useStoreState(state => state.user.data!.rootAdmin); const [ error, setError ] = useState(''); const [ installing, setInstalling ] = useState(false); + const [ transferring, setTransferring ] = useState(false); const id = ServerContext.useStoreState(state => state.server.data?.id); const uuid = ServerContext.useStoreState(state => state.server.data?.uuid); const isInstalling = ServerContext.useStoreState(state => state.server.data?.isInstalling); + const isTransferring = ServerContext.useStoreState(state => state.server.data?.isTransferring); const serverId = ServerContext.useStoreState(state => state.server.data?.internalId); const getServer = ServerContext.useStoreActions(actions => actions.server.getServer); const clearServerState = ServerContext.useStoreActions(actions => actions.clearServerState); @@ -51,13 +53,23 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) setInstalling(!!isInstalling); }, [ isInstalling ]); + useEffect(() => { + setTransferring(!!isTransferring); + }, [ isTransferring ]); + useEffect(() => { setError(''); setInstalling(false); + setTransferring(false); + getServer(match.params.id) .catch(error => { if (error.response?.status === 409) { - setInstalling(true); + if (error.response.data?.errors[0]?.detail?.includes('transfer')) { + setTransferring(true); + } else { + setInstalling(true); + } } else { console.error(error); setError(httpErrorToHuman(error)); @@ -117,9 +129,9 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) - {(installing && (!rootAdmin || (rootAdmin && !location.pathname.endsWith(`/server/${id}`)))) ? + {((installing || transferring) && (!rootAdmin || (rootAdmin && !location.pathname.endsWith(`/server/${id}`)))) ? diff --git a/resources/views/admin/servers/view/manage.blade.php b/resources/views/admin/servers/view/manage.blade.php index 93d7b6390..7ddff9a06 100644 --- a/resources/views/admin/servers/view/manage.blade.php +++ b/resources/views/admin/servers/view/manage.blade.php @@ -58,6 +58,7 @@
+ @if(! $server->suspended)
@@ -96,28 +97,48 @@
@endif -
-
-
-

Transfer Server

-
-
-

- Transfer this server to another node connected to this panel. - Warning! This feature has not been fully tested and may have bugs. -

-
+ @if($server->transfer === null) +
+
+
+

Transfer Server

+
+
+

+ Transfer this server to another node connected to this panel. + Warning! This feature has not been fully tested and may have bugs. +

+
-
-
+ @else +
+
+
+

Transfer Server

+
+
+

+ This server is currently being transferred to another node. + Transfer was initiated at {{ $server->transfer->created_at }} +

+
+ + +
+
+ @endif