diff --git a/app/Livewire/Project/Shared/ExecuteContainerCommand.php b/app/Livewire/Project/Shared/ExecuteContainerCommand.php index 4eb05ad0ad..d8ce2e03fc 100644 --- a/app/Livewire/Project/Shared/ExecuteContainerCommand.php +++ b/app/Livewire/Project/Shared/ExecuteContainerCommand.php @@ -31,6 +31,13 @@ class ExecuteContainerCommand extends Component public bool $containersLoaded = false; + /** + * Container picker entries. Alpine reads them through entangle, because containers load after the first render. + * + * @var list + */ + public array $containerOptions = []; + protected $rules = [ 'server' => 'required', 'container' => 'required', @@ -149,6 +156,10 @@ class ExecuteContainerCommand extends Component $this->containers = $this->containers->sortBy(function ($container) { return data_get($container, 'container.Names'); }); + $this->containerOptions = $this->containers->map(fn (array $container) => [ + 'value' => $this->containerTarget($container), + 'label' => data_get($container, 'container.Names').' · '.data_get($container, 'server.name'), + ])->values()->all(); if ($this->containers->count() === 1) { $this->selected_container = $this->containerTarget($this->containers->first()); 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 ef179a4ec9..2d974c0bb1 100644 --- a/resources/views/livewire/project/shared/execute-container-command.blade.php +++ b/resources/views/livewire/project/shared/execute-container-command.blade.php @@ -33,10 +33,6 @@ $consoleThemeKeys = collect($consoleThemes)->pluck('key')->values(); $consoleThemeNames = collect($consoleThemes)->pluck('name', 'key'); $consoleThemeAccents = collect($consoleThemes)->pluck('accent', 'key'); - $containerOptions = $containers->map(fn ($container) => [ - 'value' => data_get($container, 'server.uuid').':'.data_get($container, 'container.Names'), - 'label' => data_get($container, 'container.Names').' · '.data_get($container, 'server.name'), - ])->values(); @endphp @if (in_array($type, ['application', 'database', 'service', 'server'], true)) @@ -78,7 +74,7 @@ containerOpen: false, targetChosen: @js($selected_container !== 'default'), selectedContainer: @entangle('selected_container').live, - containerOptions: @js($containerOptions), + containerOptions: @entangle('containerOptions'), init() { const savedTheme = localStorage.getItem('coolify-console-theme'); this.consoleTheme = this.themeKeys.includes(savedTheme) ? savedTheme : 'system'; diff --git a/tests/v4/Browser/TerminalContainerPickerTest.php b/tests/v4/Browser/TerminalContainerPickerTest.php new file mode 100644 index 0000000000..d1a49338cd --- /dev/null +++ b/tests/v4/Browser/TerminalContainerPickerTest.php @@ -0,0 +1,67 @@ +set('app.maintenance.store', 'array'); + config()->set('constants.ssh.mux_enabled', false); + + $stack = seedBrowserResourceStack(); + $stack['server']->settings->update(['is_terminal_enabled' => true]); + $this->application = createBrowserApplication($stack, ['uuid' => 'app-terminal-picker']); + $this->terminalPath = "/project/{$stack['project']->uuid}/environment/{$stack['environment']->uuid}/application/{$this->application->uuid}/terminal"; + Cache::flush(); + + // An open WebSocket that accepts the connection and waits (it only reacts to a client frame). + $this->terminalServer = new SymfonyProcess(['node', base_path('tests/Fixtures/terminal-websocket-rejection-server.mjs'), 'reject-token']); + $this->terminalServer->start(); + $this->terminalServer->waitUntil(fn (string $type, string $output) => str_contains($output, "\n")); + config()->set('constants.terminal.protocol', 'ws'); + config()->set('constants.terminal.host', '127.0.0.1'); + config()->set('constants.terminal.port', trim($this->terminalServer->getOutput())); +}); + +afterEach(function () { + $this->terminalServer?->stop(0); +}); + +it('lists the lazily loaded containers when the user must choose one', function () { + Process::fake(function ($process) { + $command = (string) $process->command; + + if (str_contains($command, 'docker ps -a')) { + return Process::result(output: collect(['app-terminal-picker-web', 'app-terminal-picker-worker']) + ->map(fn (string $name) => json_encode(['ID' => md5($name), 'Names' => $name, 'State' => 'running', 'Labels' => ''])) + ->implode("\n")); + } + + // The chosen container stops before the session starts, so no WebSocket session is needed. + if (str_contains($command, 'docker inspect')) { + return Process::result(output: json_encode(['State' => ['Status' => 'exited']])); + } + + return Process::result(); + }); + + $page = visit('/login') + ->fill('email', 'test@example.com') + ->fill('password', 'password') + ->click('Login') + ->assertSee('Dashboard') + ->navigate($this->terminalPath) + ->assertSee('Start a terminal session') + ->assertSee('app-terminal-picker-web') + ->assertSee('app-terminal-picker-worker') + ->screenshot(filename: 'terminal-container-picker-list') + ->click('[data-terminal-target-picker="launcher"] button:nth-of-type(2)') + ->assertSee('The container is not running.') + ->assertDontSee('Start a terminal session') + ->assertSee('app-terminal-picker-worker · localhost'); + + $page->screenshot(filename: 'terminal-container-picker-selected'); +});