diff --git a/app/Livewire/Storage/Create.php b/app/Livewire/Storage/Create.php index 64a6629f6..49da0ef68 100644 --- a/app/Livewire/Storage/Create.php +++ b/app/Livewire/Storage/Create.php @@ -71,34 +71,12 @@ class Create extends Component 'endpoint' => 'Endpoint', ]; - public function updatedEndpoint($value) - { - try { - if (empty($value)) { - return; - } - if (str($value)->contains('digitaloceanspaces.com')) { - $uri = Uri::of($value); - $host = $uri->host(); - - if (preg_match('/^(.+)\.([^.]+\.digitaloceanspaces\.com)$/', $host, $matches)) { - $host = $matches[2]; - $value = "https://{$host}"; - } - } - } finally { - if (! str($value)->startsWith('https://') && ! str($value)->startsWith('http://')) { - $value = 'https://'.$value; - } - $this->endpoint = $value; - } - } - public function submit() { try { $this->authorize('create', S3Storage::class); + $this->endpoint = $this->normalizeEndpoint($this->endpoint); $this->validate(); $this->storage = new S3Storage; $this->storage->name = $this->name; @@ -118,8 +96,42 @@ class Create extends Component return redirectRoute($this, 'storage.show', [$this->storage->uuid]); } catch (\Throwable $e) { - $this->dispatch('error', 'Failed to create storage.', $e->getMessage()); + $this->dispatch('error', 'Failed to create storage.', $this->connectionErrorDescription($e)); // return handleError($e, $this); } } + + private function connectionErrorDescription(\Throwable $exception): string + { + $settingsUrl = route('settings.advanced').'#endpoint-section'; + $description = e($exception->getMessage()); + + if (! str_contains($exception->getMessage(), $settingsUrl)) { + return $description; + } + + $link = 'Set them here.'; + + return str_replace(e($settingsUrl), $link, $description); + } + + private function normalizeEndpoint(string $endpoint): string + { + $endpoint = trim($endpoint); + + $hasScheme = preg_match('/^(?:https?:|[a-z][a-z0-9+.-]*:\/\/)/i', $endpoint) === 1; + if (! $hasScheme) { + $endpoint = 'https://'.$endpoint; + } + + if (str($endpoint)->contains('digitaloceanspaces.com')) { + $host = Uri::of($endpoint)->host(); + + if (preg_match('/^(.+)\.([^.]+\.digitaloceanspaces\.com)$/', $host, $matches)) { + return "https://{$matches[2]}"; + } + } + + return $endpoint; + } } diff --git a/resources/views/livewire/project/service/volume-backup/index.blade.php b/resources/views/livewire/project/service/volume-backup/index.blade.php index 28141d008..7fd085a8b 100644 --- a/resources/views/livewire/project/service/volume-backup/index.blade.php +++ b/resources/views/livewire/project/service/volume-backup/index.blade.php @@ -53,7 +53,6 @@ {{ data_get_str($service, 'name')->limit(10) }} > Backups | Coolify - 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 25e9b452f..e4a24f338 100644 --- a/resources/views/livewire/project/shared/execute-container-command.blade.php +++ b/resources/views/livewire/project/shared/execute-container-command.blade.php @@ -7,10 +7,8 @@ @elseif ($type === 'database') - @elseif ($type === 'service') - @else diff --git a/resources/views/livewire/project/shared/logs.blade.php b/resources/views/livewire/project/shared/logs.blade.php index 76f05ebea..24bda5619 100644 --- a/resources/views/livewire/project/shared/logs.blade.php +++ b/resources/views/livewire/project/shared/logs.blade.php @@ -3,9 +3,8 @@ {{ data_get_str($resource, 'name')->limit(10) }} > Logs | Coolify - - @if ($type === 'application') + @elseif ($type === 'database') diff --git a/resources/views/livewire/storage/create.blade.php b/resources/views/livewire/storage/create.blade.php index 32aeda095..dd6b1d687 100644 --- a/resources/views/livewire/storage/create.blade.php +++ b/resources/views/livewire/storage/create.blade.php @@ -7,7 +7,18 @@ - +
- + Validate Connection & Continue diff --git a/tests/Feature/FormsButtonLoadingTest.php b/tests/Feature/FormsButtonLoadingTest.php index 32b075be9..ba806a930 100644 --- a/tests/Feature/FormsButtonLoadingTest.php +++ b/tests/Feature/FormsButtonLoadingTest.php @@ -30,6 +30,12 @@ it('uses an explicit wire target for submit buttons without wire click', functio ->toContain('Validate and add'); }); +it('targets the S3 connection validation submit action', function () { + $view = file_get_contents(resource_path('views/livewire/storage/create.blade.php')); + + expect($view)->toContain(''); +}); + it('keeps method arguments when deriving the loading target', function () { $html = Blade::render( 'Use this key' diff --git a/tests/Feature/ResourceHeadingUnifiedNavbarTest.php b/tests/Feature/ResourceHeadingUnifiedNavbarTest.php index 51371f6f6..c49bd110c 100644 --- a/tests/Feature/ResourceHeadingUnifiedNavbarTest.php +++ b/tests/Feature/ResourceHeadingUnifiedNavbarTest.php @@ -334,6 +334,32 @@ it('renders configuration warnings as navbar popovers instead of floating notifi ->toContain("\$dispatch('open-configuration-diff')"); }); +it('renders only one configuration warning on database runtime logs', function () { + $logs = file_get_contents(resource_path('views/livewire/project/shared/logs.blade.php')); + $databaseHeading = file_get_contents(resource_path('views/livewire/project/database/heading.blade.php')); + + expect($logs) + ->not->toContain(" \n\n @if (\$type === 'application')") + ->and($databaseHeading) + ->toContain(''); +}); + +it('renders only one configuration warning on database and service terminal pages', function () { + $terminal = file_get_contents(resource_path('views/livewire/project/shared/execute-container-command.blade.php')); + + expect($terminal) + ->not->toContain("@elseif (\$type === 'database')\n ") + ->not->toContain("@elseif (\$type === 'service')\n "); +}); + +it('renders only one configuration warning on the service volume backups page', function () { + $volumeBackups = file_get_contents(resource_path('views/livewire/project/service/volume-backup/index.blade.php')); + + expect($volumeBackups) + ->not->toContain('') + ->toContain('invoke(new Create, $exception); + + expect($description) + ->toContain('href="'.$settingsUrl.'"') + ->toContain('Set them here.') + ->not->toContain('targets: '.$settingsUrl); +}); diff --git a/tests/Unit/S3StorageEndpointNormalizationTest.php b/tests/Unit/S3StorageEndpointNormalizationTest.php new file mode 100644 index 000000000..860fbe731 --- /dev/null +++ b/tests/Unit/S3StorageEndpointNormalizationTest.php @@ -0,0 +1,29 @@ +not->toContain('wire:model.blur="endpoint"') + ->toContain('x-on:blur') + ->toContain('const hasScheme') + ->toContain('dispatchEvent'); +}); + +it('normalizes endpoints again on the backend', function (string $endpoint, string $expected) { + $method = new ReflectionMethod(Create::class, 'normalizeEndpoint'); + + expect($method->invoke(new Create, $endpoint))->toBe($expected); +})->with([ + 'missing scheme' => ['s3.example.com', 'https://s3.example.com'], + 'existing HTTP scheme' => ['http://192.168.1.50:9000', 'http://192.168.1.50:9000'], + 'malformed HTTP scheme' => ['http:/192.168.1.50:9000', 'http:/192.168.1.50:9000'], + 'hostname with port' => ['minio.internal:9000', 'https://minio.internal:9000'], + 'unsupported scheme' => ['ftp://s3.example.com', 'ftp://s3.example.com'], + 'DigitalOcean bucket endpoint' => ['https://bucket.nyc3.digitaloceanspaces.com', 'https://nyc3.digitaloceanspaces.com'], +]);