From 2677e471387e4f7639e442bf74d7f8bfc9aacd57 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 15 Aug 2026 11:46:49 +0200 Subject: [PATCH] fix(storage): split S3 endpoint input and persist stopped status Persist exited status across application, database, service, and preview stop paths, and reuse the split host/port/path input for S3 endpoints with linked validation feedback. --- app/Actions/Application/StopApplication.php | 10 ++--- app/Actions/Database/StopDatabase.php | 1 + app/Actions/Service/StopService.php | 3 ++ .../Service/StopServiceApplication.php | 4 ++ app/Livewire/Project/Application/Previews.php | 6 +++ app/Livewire/Storage/Create.php | 2 +- .../components/forms/domain-input.blade.php | 19 +++++++-- .../views/livewire/storage/create.blade.php | 14 +------ .../views/livewire/storage/form.blade.php | 8 +++- tests/Feature/SplitUrlInputTest.php | 39 ++++++++++++++++++ .../S3StorageEndpointNormalizationTest.php | 16 +++++--- tests/Unit/StopActionsPersistStatusTest.php | 40 +++++++++++++++++++ 12 files changed, 132 insertions(+), 30 deletions(-) create mode 100644 tests/Feature/SplitUrlInputTest.php create mode 100644 tests/Unit/StopActionsPersistStatusTest.php diff --git a/app/Actions/Application/StopApplication.php b/app/Actions/Application/StopApplication.php index 81148e3d8..66ceb95f6 100644 --- a/app/Actions/Application/StopApplication.php +++ b/app/Actions/Application/StopApplication.php @@ -28,7 +28,7 @@ class StopApplication if ($server->isSwarm()) { instant_remote_process(["docker stack rm {$application->uuid}"], $server); - return; + continue; } $containers = $previewDeployments @@ -57,17 +57,15 @@ class StopApplication } } + $status = ['status' => 'exited']; if ($resetRestartCount) { - $application->update([ + $status = array_merge($status, [ 'restart_count' => 0, 'last_restart_at' => null, 'last_restart_type' => null, ]); - } else { - $application->update([ - 'status' => 'exited', - ]); } + $application->update($status); ServiceStatusChanged::dispatch($application->environment->project->team->id); } diff --git a/app/Actions/Database/StopDatabase.php b/app/Actions/Database/StopDatabase.php index 5bbef7d7d..a3a7f16ef 100644 --- a/app/Actions/Database/StopDatabase.php +++ b/app/Actions/Database/StopDatabase.php @@ -30,6 +30,7 @@ class StopDatabase // Reset restart tracking when database is manually stopped $database->update([ + 'status' => 'exited', 'restart_count' => 0, 'last_restart_at' => null, 'last_restart_type' => null, diff --git a/app/Actions/Service/StopService.php b/app/Actions/Service/StopService.php index 9817bf32d..5e34c8e6a 100644 --- a/app/Actions/Service/StopService.php +++ b/app/Actions/Service/StopService.php @@ -49,6 +49,9 @@ class StopService $this->stopContainersInParallel($containersToStop, $server); } + $applications->each->update(['status' => 'exited']); + $dbs->each->update(['status' => 'exited']); + if ($deleteConnectedNetworks) { $service->deleteConnectedNetworks(); } diff --git a/app/Actions/Service/StopServiceApplication.php b/app/Actions/Service/StopServiceApplication.php index 724e1a254..184dcb491 100644 --- a/app/Actions/Service/StopServiceApplication.php +++ b/app/Actions/Service/StopServiceApplication.php @@ -2,6 +2,7 @@ namespace App\Actions\Service; +use App\Events\ServiceStatusChanged; use App\Models\ServiceApplication; use App\Models\ServiceDatabase; use Lorisleiva\Actions\Concerns\AsAction; @@ -21,5 +22,8 @@ class StopServiceApplication instant_remote_process([ "docker stop {$containerName}", ], $server); + + $serviceApplication->update(['status' => 'exited']); + ServiceStatusChanged::dispatch($service->environment->project->team->id); } } diff --git a/app/Livewire/Project/Application/Previews.php b/app/Livewire/Project/Application/Previews.php index e70636628..e07a985b4 100644 --- a/app/Livewire/Project/Application/Previews.php +++ b/app/Livewire/Project/Application/Previews.php @@ -3,6 +3,7 @@ namespace App\Livewire\Project\Application; use App\Actions\Docker\GetContainersStatus; +use App\Events\ServiceStatusChanged; use App\Jobs\DeleteResourceJob; use App\Models\Application; use App\Models\ApplicationPreview; @@ -373,6 +374,11 @@ class Previews extends Component $this->stopContainers($containers, $server); } + ApplicationPreview::where('application_id', $this->application->id) + ->where('pull_request_id', $pull_request_id) + ->update(['status' => 'exited']); + ServiceStatusChanged::dispatch($this->application->environment->project->team->id); + GetContainersStatus::run($server); $this->application->refresh(); $this->dispatch('containerStatusUpdated'); diff --git a/app/Livewire/Storage/Create.php b/app/Livewire/Storage/Create.php index 49da0ef68..d741e6918 100644 --- a/app/Livewire/Storage/Create.php +++ b/app/Livewire/Storage/Create.php @@ -26,7 +26,7 @@ class Create extends Component public string $bucket; - public string $endpoint; + public string $endpoint = ''; public S3Storage $storage; diff --git a/resources/views/components/forms/domain-input.blade.php b/resources/views/components/forms/domain-input.blade.php index 030fc0f5f..e7fc1c063 100644 --- a/resources/views/components/forms/domain-input.blade.php +++ b/resources/views/components/forms/domain-input.blade.php @@ -3,6 +3,8 @@ 'wire' => true, 'value' => '', 'errorId' => null, + 'hostLabel' => 'Domain', + 'hostPlaceholder' => 'app.example.com', ])
- @error($errorId ?? $id) -

{{ $message }}

+ @php + preg_match('/(https?:\/\/\S+)$/', $message, $validationLinkMatches); + $validationLink = $validationLinkMatches[1] ?? null; + @endphp +

+ @if ($validationLink) + {{ str($message)->beforeLast($validationLink)->trim() }} + Set them here. + @else + {{ $message }} + @endif +

@enderror
diff --git a/resources/views/livewire/storage/create.blade.php b/resources/views/livewire/storage/create.blade.php index dd6b1d687..1154a7e90 100644 --- a/resources/views/livewire/storage/create.blade.php +++ b/resources/views/livewire/storage/create.blade.php @@ -7,18 +7,8 @@ - +
- + @can('update', $storage) + + @else + + @endcan
diff --git a/tests/Feature/SplitUrlInputTest.php b/tests/Feature/SplitUrlInputTest.php new file mode 100644 index 000000000..5bfa0feb6 --- /dev/null +++ b/tests/Feature/SplitUrlInputTest.php @@ -0,0 +1,39 @@ + + BLADE); + + expect($html) + ->toContain('Host') + ->toContain('minio.internal or 192.168.1.50') + ->toContain("scheme: 'https'") + ->toContain("host: ''") + ->toContain("port: ''") + ->toContain("path: ''"); +}); + +it('keeps validation errors attached to the composed endpoint', function () { + $settingsUrl = route('settings.advanced').'#endpoint-section'; + $errors = new ViewErrorBag; + $errors->put('default', new MessageBag([ + 'endpoint' => "The endpoint is invalid. Configure allowed internal targets: {$settingsUrl}", + ])); + View::share('errors', $errors); + + $html = Blade::render(''); + + expect($html) + ->toContain('The endpoint is invalid.') + ->toContain('href="'.$settingsUrl.'"') + ->toContain('Set them here.'); +}); diff --git a/tests/Unit/S3StorageEndpointNormalizationTest.php b/tests/Unit/S3StorageEndpointNormalizationTest.php index 860fbe731..ee2ecc04a 100644 --- a/tests/Unit/S3StorageEndpointNormalizationTest.php +++ b/tests/Unit/S3StorageEndpointNormalizationTest.php @@ -5,14 +5,18 @@ 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')); +it('uses the shared split URL input without a Livewire blur request', function () { + $createView = file_get_contents(resource_path('views/livewire/storage/create.blade.php')); + $editView = file_get_contents(resource_path('views/livewire/storage/form.blade.php')); - expect($view) + expect($createView) ->not->toContain('wire:model.blur="endpoint"') - ->toContain('x-on:blur') - ->toContain('const hasScheme') - ->toContain('dispatchEvent'); + ->toContain('toContain('host-label="Host"') + ->toContain('host-placeholder="minio.internal or 192.168.1.50"') + ->and($editView) + ->toContain('toContain('@can(\'update\', $storage)'); }); it('normalizes endpoints again on the backend', function (string $endpoint, string $expected) { diff --git a/tests/Unit/StopActionsPersistStatusTest.php b/tests/Unit/StopActionsPersistStatusTest.php new file mode 100644 index 000000000..a924381bb --- /dev/null +++ b/tests/Unit/StopActionsPersistStatusTest.php @@ -0,0 +1,40 @@ +toContain("'status' => 'exited'"); +}); + +it('persists exited status for every full application stop path', function () { + $action = file_get_contents(__DIR__.'/../../app/Actions/Application/StopApplication.php'); + + expect($action) + ->toContain("\$status = ['status' => 'exited'];") + ->toContain('$application->update($status);') + ->not->toMatch('/docker stack rm .*?return;/s'); +}); + +it('persists exited status for all children when stopping a service', function () { + $action = file_get_contents(__DIR__.'/../../app/Actions/Service/StopService.php'); + + expect($action) + ->toContain("\$applications->each->update(['status' => 'exited']);") + ->toContain("\$dbs->each->update(['status' => 'exited']);"); +}); + +it('persists exited status when stopping an individual service resource', function () { + $action = file_get_contents(__DIR__.'/../../app/Actions/Service/StopServiceApplication.php'); + + expect($action) + ->toContain("\$serviceApplication->update(['status' => 'exited']);") + ->toContain('ServiceStatusChanged::dispatch($service->environment->project->team->id);'); +}); + +it('persists exited status when stopping a preview deployment', function () { + $component = file_get_contents(__DIR__.'/../../app/Livewire/Project/Application/Previews.php'); + + expect($component) + ->toContain("->update(['status' => 'exited']);") + ->toContain('ServiceStatusChanged::dispatch($this->application->environment->project->team->id);'); +});