diff --git a/app/Livewire/Project/Shared/ExecuteContainerCommand.php b/app/Livewire/Project/Shared/ExecuteContainerCommand.php index ce6314d63..9891808b3 100644 --- a/app/Livewire/Project/Shared/ExecuteContainerCommand.php +++ b/app/Livewire/Project/Shared/ExecuteContainerCommand.php @@ -29,6 +29,16 @@ class ExecuteContainerCommand extends Component public bool $hasShell = true; + public bool $containersLoaded = false; + + public bool $autoConnectAttempted = false; + + public bool $isConnecting = false; + + public bool $isConnected = false; + + public string $connectionStatus = 'Loading containers...'; + protected $rules = [ 'server' => 'required', 'container' => 'required', @@ -87,6 +97,8 @@ class ExecuteContainerCommand extends Component $this->type = 'server'; $this->resource = Server::ownedByCurrentTeam()->whereUuid($this->parameters['server_uuid'])->firstOrFail(); $this->server = $this->resource; + $this->containersLoaded = true; // Server doesn't need container loading + $this->connectionStatus = 'Waiting for terminal to be ready...'; } } @@ -151,6 +163,49 @@ class ExecuteContainerCommand extends Component if ($this->containers->count() === 1) { $this->selected_container = data_get($this->containers->first(), 'container.Names'); } + + $this->containersLoaded = true; + $this->connectionStatus = 'Waiting for terminal to be ready...'; + } + + #[On('initializeTerminalConnection')] + public function initializeTerminalConnection() + { + // Only auto-connect if containers are loaded and we haven't attempted before + if (! $this->containersLoaded || $this->autoConnectAttempted) { + return; + } + + $this->autoConnectAttempted = true; + $this->isConnecting = true; + + if ($this->type === 'server') { + $this->connectionStatus = 'Establishing connection to server terminal...'; + $this->connectToServer(); + } elseif ($this->containers->count() === 1) { + $this->connectionStatus = 'Establishing connection to container terminal...'; + $this->connectToContainer(); + } else { + $this->isConnecting = false; + $this->connectionStatus = ''; + } + } + + #[On('terminalConnected')] + public function terminalConnected() + { + $this->isConnected = true; + $this->isConnecting = false; + $this->connectionStatus = ''; + } + + #[On('terminalDisconnected')] + public function terminalDisconnected() + { + $this->isConnected = false; + $this->isConnecting = false; + $this->autoConnectAttempted = false; + $this->connectionStatus = 'Connection lost. Click Reconnect to try again.'; } private function checkShellAvailability(Server $server, string $container): bool @@ -179,6 +234,8 @@ class ExecuteContainerCommand extends Component throw new \RuntimeException('Terminal access is disabled on this server.'); } $this->hasShell = true; + $this->isConnecting = true; + $this->connectionStatus = 'Establishing connection to server terminal...'; $this->dispatch( 'send-terminal-command', false, @@ -186,6 +243,9 @@ class ExecuteContainerCommand extends Component data_get($this->server, 'uuid') ); } catch (\Throwable $e) { + $this->isConnecting = false; + $this->connectionStatus = 'Connection failed.'; + return handleError($e, $this); } } @@ -234,9 +294,14 @@ class ExecuteContainerCommand extends Component $this->hasShell = $this->checkShellAvailability($server, data_get($container, 'container.Names')); if (! $this->hasShell) { + $this->isConnecting = false; + $this->connectionStatus = 'Shell not available in container.'; + return; } + $this->isConnecting = true; + $this->connectionStatus = 'Establishing connection to container terminal...'; $this->dispatch( 'send-terminal-command', true, @@ -244,6 +309,9 @@ class ExecuteContainerCommand extends Component data_get($container, 'server.uuid') ); } catch (\Throwable $e) { + $this->isConnecting = false; + $this->connectionStatus = 'Connection failed.'; + return handleError($e, $this); } } diff --git a/resources/js/terminal.js b/resources/js/terminal.js index 3161a0f32..1c7c7af1d 100644 --- a/resources/js/terminal.js +++ b/resources/js/terminal.js @@ -42,7 +42,7 @@ export function initializeTerminalComponent() { this.setupTerminalEventListeners(); this.$wire.on('send-back-command', (command) => { - this.sendMessage({ command: command }); + this.sendCommandWhenReady({ command: command }); }); this.keepAliveInterval = setInterval(this.keepAlive.bind(this), 30000); @@ -99,6 +99,9 @@ export function initializeTerminalComponent() { this.paused = false; this.commandBuffer = ''; + // Notify parent component that terminal disconnected + this.$wire.dispatch('terminalDisconnected'); + // Force a refresh this.$nextTick(() => { this.resizeTerminal(); @@ -205,6 +208,9 @@ export function initializeTerminalComponent() { // Start ping timeout monitoring this.resetPingTimeout(); + + // Notify that WebSocket is ready for auto-connection + this.dispatchEvent('terminal-websocket-ready'); }, handleSocketError(error) { @@ -277,6 +283,12 @@ export function initializeTerminalComponent() { } }, + sendCommandWhenReady(message) { + if (this.isWebSocketReady()) { + this.sendMessage(message); + } + }, + handleSocketMessage(event) { // Handle pong responses if (event.data === 'pong') { @@ -297,14 +309,23 @@ export function initializeTerminalComponent() { this.term.focus(); document.querySelector('.xterm-viewport').classList.add('scrollbar', 'rounded-sm'); this.resizeTerminal(); + + // Notify parent component that terminal is connected + this.$wire.dispatch('terminalConnected'); } else if (event.data === 'unprocessable') { if (this.term) this.term.reset(); this.terminalActive = false; this.message = '(sorry, something went wrong, please try again)'; + + // Notify parent component that terminal connection failed + this.$wire.dispatch('terminalDisconnected'); } else if (event.data === 'pty-exited') { this.terminalActive = false; this.term.reset(); this.commandBuffer = ''; + + // Notify parent component that terminal disconnected + this.$wire.dispatch('terminalDisconnected'); } else { try { this.pendingWrites++; @@ -441,6 +462,22 @@ export function initializeTerminalComponent() { lastPingTime: this.lastPingTime, heartbeatMissed: this.heartbeatMissed }; + }, + + // Helper method to dispatch custom events + dispatchEvent(eventName, detail = null) { + const event = new CustomEvent(eventName, { + detail: detail, + bubbles: true + }); + this.$el.dispatchEvent(event); + }, + + // Check if WebSocket is ready for commands + isWebSocketReady() { + return this.connectionState === 'connected' && + this.socket && + this.socket.readyState === WebSocket.OPEN; } }; } diff --git a/resources/views/livewire/project/shared/execute-container-command.blade.php b/resources/views/livewire/project/shared/execute-container-command.blade.php index 16b6ac015..f40a1c82f 100644 --- a/resources/views/livewire/project/shared/execute-container-command.blade.php +++ b/resources/views/livewire/project/shared/execute-container-command.blade.php @@ -17,7 +17,6 @@ @endif -

Terminal

@if (!$hasShell)
@@ -37,10 +36,19 @@ @else @if ($type === 'server') @if ($server->isTerminalEnabled()) -
- Reconnect + +

Terminal

+ + Reconnect +
+ + {{-- Loading indicator for all connection states --}} + @if (!$containersLoaded || $isConnecting || $connectionStatus) + {{ $connectionStatus }} + @endif +
@@ -52,10 +60,18 @@
No containers are running on this server or terminal access is disabled.
@else @if (count($containers) === 1) -
- Reconnect + +

Terminal

+ + Reconnect +
+ + {{-- Loading indicator for all connection states --}} + @if (!$containersLoaded || $isConnecting || $connectionStatus) + {{ $connectionStatus }} + @endif @else
@@ -69,8 +85,15 @@ @endforeach - Connect + + {{ $isConnecting ? 'Connecting...' : 'Connect' }} +
+ + {{-- Loading indicator for manual connection --}} + @if ($isConnecting || $connectionStatus) + {{ $connectionStatus }} + @endif @endif
@@ -78,4 +101,41 @@ @endif @endif @endif + + @script + + @endscript