mirror of
https://github.com/tiennm99/coolify.git
synced 2026-09-06 06:17:25 +00:00
fix(ui): normalize S3 endpoints and remove duplicate config warnings
Normalize endpoints on blur and submit, link internal-target errors to settings, and target the S3 submit loading state.
This commit is contained in:
@@ -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 = '<a class="font-medium underline" href="'.e($settingsUrl).'">Set them here.</a>';
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,6 @@
|
||||
<x-slot:title>
|
||||
{{ data_get_str($service, 'name')->limit(10) }} > Backups | Coolify
|
||||
</x-slot>
|
||||
<livewire:project.shared.configuration-checker :resource="$service" />
|
||||
<livewire:project.service.heading :service="$service" :parameters="$parameters" :query="request()->query()"
|
||||
wire:key="service-heading-volume-backup-index" />
|
||||
|
||||
|
||||
@@ -7,10 +7,8 @@
|
||||
<livewire:project.shared.configuration-checker :resource="$resource" />
|
||||
<livewire:project.application.heading :application="$resource" wire:key="application-heading-command" />
|
||||
@elseif ($type === 'database')
|
||||
<livewire:project.shared.configuration-checker :resource="$resource" />
|
||||
<livewire:project.database.heading :database="$resource" />
|
||||
@elseif ($type === 'service')
|
||||
<livewire:project.shared.configuration-checker :resource="$resource" />
|
||||
<livewire:project.service.heading :service="$resource" :parameters="$parameters" title="Terminal" />
|
||||
@else
|
||||
<livewire:server.navbar :server="$servers->first()" />
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
{{ data_get_str($resource, 'name')->limit(10) }} > Logs | Coolify
|
||||
</x-slot>
|
||||
|
||||
<livewire:project.shared.configuration-checker :resource="$resource" />
|
||||
|
||||
@if ($type === 'application')
|
||||
<livewire:project.shared.configuration-checker :resource="$resource" />
|
||||
<livewire:project.application.heading :application="$resource" wire:key="application-heading-logs" />
|
||||
@elseif ($type === 'database')
|
||||
<livewire:project.database.heading :database="$resource" />
|
||||
|
||||
@@ -7,7 +7,18 @@
|
||||
<x-forms.input required label="Name" id="name" />
|
||||
<x-forms.input label="Description" id="description" />
|
||||
</div>
|
||||
<x-forms.input required type="url" label="Endpoint" wire:model.blur="endpoint" />
|
||||
<x-forms.input required type="url" label="Endpoint" id="endpoint"
|
||||
x-on:blur="
|
||||
let value = $el.value.trim();
|
||||
const hasScheme = /^https?:/i.test(value) || /^[a-z][a-z0-9+.-]*:\/\//i.test(value);
|
||||
if (value && !hasScheme) {
|
||||
value = `https://${value}`;
|
||||
}
|
||||
if ($el.value !== value) {
|
||||
$el.value = value;
|
||||
$el.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
" />
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input required label="Bucket" id="bucket" />
|
||||
<x-forms.input required helper="Region only required for AWS. Leave it as-is for other providers."
|
||||
@@ -18,7 +29,7 @@
|
||||
<x-forms.input required type="password" label="Secret Key" id="secret" />
|
||||
</div>
|
||||
|
||||
<x-forms.button class="mt-4" type="submit">
|
||||
<x-forms.button class="mt-4" type="submit" wire:target="submit">
|
||||
Validate Connection & Continue
|
||||
</x-forms.button>
|
||||
</form>
|
||||
|
||||
@@ -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('<x-forms.button class="mt-4" type="submit" wire:target="submit">');
|
||||
});
|
||||
|
||||
it('keeps method arguments when deriving the loading target', function () {
|
||||
$html = Blade::render(
|
||||
'<x-forms.button wire:click="setPrivateKey(42)">Use this key</x-forms.button>'
|
||||
|
||||
@@ -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(" <livewire:project.shared.configuration-checker :resource=\"\$resource\" />\n\n @if (\$type === 'application')")
|
||||
->and($databaseHeading)
|
||||
->toContain('<livewire:project.shared.configuration-checker :resource="$database" />');
|
||||
});
|
||||
|
||||
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 <livewire:project.shared.configuration-checker :resource=\"\$resource\" />")
|
||||
->not->toContain("@elseif (\$type === 'service')\n <livewire:project.shared.configuration-checker :resource=\"\$resource\" />");
|
||||
});
|
||||
|
||||
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('<livewire:project.shared.configuration-checker :resource="$service" />')
|
||||
->toContain('<livewire:project.service.heading :service="$service"');
|
||||
});
|
||||
|
||||
it('moves application backups from the top tabs into the settings sidebar', function () {
|
||||
$heading = file_get_contents(resource_path('views/livewire/project/application/heading.blade.php'));
|
||||
$configuration = file_get_contents(resource_path('views/livewire/project/application/configuration.blade.php'));
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Storage\Create;
|
||||
|
||||
it('formats the internal target settings route as a clickable link', function () {
|
||||
$settingsUrl = route('settings.advanced').'#endpoint-section';
|
||||
$exception = new RuntimeException("Private target. Configure allowed internal targets: {$settingsUrl}");
|
||||
$method = new ReflectionMethod(Create::class, 'connectionErrorDescription');
|
||||
|
||||
$description = $method->invoke(new Create, $exception);
|
||||
|
||||
expect($description)
|
||||
->toContain('href="'.$settingsUrl.'"')
|
||||
->toContain('Set them here.')
|
||||
->not->toContain('targets: '.$settingsUrl);
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Storage\Create;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class);
|
||||
|
||||
it('normalizes S3 endpoints without a Livewire blur request', function () {
|
||||
$view = file_get_contents(resource_path('views/livewire/storage/create.blade.php'));
|
||||
|
||||
expect($view)
|
||||
->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'],
|
||||
]);
|
||||
Reference in New Issue
Block a user