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'],
+]);