fix(ui): replace native browser dialogs with confirmation modals

Use the Coolify confirmation modal instead of wire:confirm for server
management, server transfer and import, resource migration, and S3
backup disabling. Show a toast instead of alert() for a missing GitHub
App webhook endpoint. Add a test that blocks native dialogs in views.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai
2026-09-28 15:37:20 +02:00
co-authored by Claude Opus 5.5
parent 15909fa641
commit 1f0e2f74d0
8 changed files with 122 additions and 47 deletions
@@ -1,4 +1,4 @@
<div x-data="{
<div x-on:migrate-resource-confirmed.window="$wire.migrateTo(selectedMigrateDestination)" x-data="{
selectedCloneServer: null,
selectedCloneDestination: null,
selectedCloneProject: null,
@@ -217,11 +217,17 @@
The resource keeps the same UUID and configuration. Only the hosting server and network
destination change.
</p>
<x-forms.button
wire:confirm="Migrate this resource? It will be stopped on the source server. Redeploy after migration completes."
@click="$wire.migrateTo(selectedMigrateDestination)">
Migrate resource
</x-forms.button>
<x-modal-confirmation title="Migrate this resource?" submitAction="migrate-resource-confirmed"
dispatchAction :confirmWithText="false" :confirmWithPassword="false"
step2ButtonText="Migrate resource"
:actions="[
'The resource will be stopped on the source server.',
'Redeploy the resource after the migration completes.',
]">
<x-slot:trigger>
<x-forms.button>Migrate resource</x-forms.button>
</x-slot:trigger>
</x-modal-confirmation>
</div>
</x-application.settings-section>
@endif
+31 -11
View File
@@ -124,17 +124,37 @@
</x-forms.button>
</x-process-dialog>
@if (isDev())
<x-forms.button type="button" wire:click="toggleManagement"
wire:confirm="{{ $server->isTransferredAway()
? 'Enable management? Confirm that no other Coolify instance manages this server. Two active instances can cause conflicting deployments, proxy changes, backups, and restarts.'
: 'Disable management? Deployments, webhooks, scheduled tasks, backups, proxy updates, and monitoring will stop on this instance. Existing workloads continue to run.' }}"
title="{{ $server->isTransferredAway()
? 'Let this instance run deployments, webhooks, scheduled tasks, backups, proxy updates, and monitoring for this server'
: 'Stop deployments, webhooks, scheduled tasks, backups, proxy updates, and monitoring for this server on this instance' }}"
canGate="update" :canResource="$server">
<x-reicon name="{{ $server->isTransferredAway() ? 'play-circle' : 'stop-circle' }}" class="size-3.5" />
{{ $server->isTransferredAway() ? 'Enable management' : 'Disable management' }}
</x-forms.button>
<div wire:key="server-management-{{ $server->isTransferredAway() ? 'enable' : 'disable' }}">
@if ($server->isTransferredAway())
<x-modal-confirmation title="Enable management on this instance?"
submitAction="toggleManagement" :confirmWithText="false"
:confirmWithPassword="false" step2ButtonText="Enable management"
warningMessage="Before you continue, disable this server on every other Coolify instance. Two active instances can cause conflicting deployments, proxy changes, backups, and restarts."
:actions="['This instance will run deployments, webhooks, scheduled tasks, backups, proxy updates, and monitoring for this server.']">
<x-slot:trigger>
<x-forms.button type="button" canGate="update" :canResource="$server">
<x-reicon name="play-circle" class="size-3.5" />
Enable management
</x-forms.button>
</x-slot:trigger>
</x-modal-confirmation>
@else
<x-modal-confirmation title="Disable management on this instance?"
submitAction="toggleManagement" :confirmWithText="false"
:confirmWithPassword="false" step2ButtonText="Disable management"
:actions="[
'Deployments, webhooks, scheduled tasks, backups, proxy updates, and monitoring will stop on this instance for this server.',
'Existing workloads continue to run on the server.',
]">
<x-slot:trigger>
<x-forms.button type="button" canGate="update" :canResource="$server">
<x-reicon name="stop-circle" class="size-3.5" />
Disable management
</x-forms.button>
</x-slot:trigger>
</x-modal-confirmation>
@endif
</div>
@endif
@if ($server->isTransferredAway())
<x-status-badge label="Transferred away" type="warning" />
@@ -29,12 +29,16 @@
<span wire:loading.remove wire:target="dryRun">Dry run</span>
<span wire:loading wire:target="dryRun">Checking…</span>
</x-forms.button>
<x-forms.button wire:click="importBundle" wire:loading.attr="disabled"
wire:target="dryRun,importBundle"
wire:confirm="Import this server into the current team?">
<span wire:loading.remove wire:target="importBundle">Import server</span>
<span wire:loading wire:target="importBundle">Importing…</span>
</x-forms.button>
<x-modal-confirmation title="Import this server?" submitAction="importBundle"
:confirmWithText="false" :confirmWithPassword="false" step2ButtonText="Import server"
:actions="['The server from the bundle will be added to the current team.']">
<x-slot:trigger>
<x-forms.button wire:loading.attr="disabled" wire:target="dryRun,importBundle">
<span wire:loading.remove wire:target="importBundle">Import server</span>
<span wire:loading wire:target="importBundle">Importing…</span>
</x-forms.button>
</x-slot:trigger>
</x-modal-confirmation>
</div>
</div>
@@ -50,12 +50,20 @@
label="Write ownership file on the host via SSH (optional)" />
</div>
<div>
<x-forms.button canGate="update" :canResource="$server" wire:click="migrateServer"
wire:loading.attr="disabled"
wire:confirm="Transfer this server to the target instance? Automations will be disabled here.">
<span wire:loading.remove wire:target="migrateServer">Transfer server</span>
<span wire:loading wire:target="migrateServer">Transferring…</span>
</x-forms.button>
<x-modal-confirmation title="Transfer this server?" submitAction="migrateServer"
:confirmWithText="false" :confirmWithPassword="false" step2ButtonText="Transfer server"
:actions="[
'The server will be transferred to the target instance.',
'Automations for this server will be disabled on this instance.',
]">
<x-slot:trigger>
<x-forms.button canGate="update" :canResource="$server" wire:loading.attr="disabled"
wire:target="migrateServer">
<span wire:loading.remove wire:target="migrateServer">Transfer server</span>
<span wire:loading wire:target="migrateServer">Transferring…</span>
</x-forms.button>
</x-slot:trigger>
</x-modal-confirmation>
</div>
@if (count($lastWarnings) > 0)
<x-callout type="warning" title="Warnings">
@@ -116,11 +124,16 @@
<p class="mb-3 text-xs leading-5 text-neutral-500 dark:text-fg-dim">
Disable automations after a manual import on the target.
</p>
<x-forms.button canGate="update" :canResource="$server" wire:click="completeTransfer"
wire:loading.attr="disabled"
wire:confirm="Disable automations on this server?">
Mark transferred & disable automations
</x-forms.button>
<x-modal-confirmation title="Disable automations on this server?"
submitAction="completeTransfer" :confirmWithText="false"
:confirmWithPassword="false" step2ButtonText="Disable automations"
:actions="['The server will be marked as transferred and automations will be disabled on this instance.']">
<x-slot:trigger>
<x-forms.button canGate="update" :canResource="$server">
Mark transferred & disable automations
</x-forms.button>
</x-slot:trigger>
</x-modal-confirmation>
</div>
<div>
@@ -360,11 +360,11 @@
const selectedEndpoint = webhook_endpoint ? webhook_endpoint.trim() : '';
const customEndpoint = custom_webhook_endpoint ? custom_webhook_endpoint.trim() : '';
if (use_custom_webhook_endpoint && !customEndpoint) {
alert('Please enter a custom webhook endpoint.');
window.toast('Error', { type: 'danger', description: 'Please enter a custom webhook endpoint.' });
return;
}
if (!use_custom_webhook_endpoint && !selectedEndpoint) {
alert('Please enter a webhook endpoint.');
window.toast('Error', { type: 'danger', description: 'Please enter a webhook endpoint.' });
return;
}
let baseUrl = (use_custom_webhook_endpoint ? customEndpoint : selectedEndpoint).replace(/\/+$/, '');
@@ -102,11 +102,16 @@
portal />
<button type="button" class="button shrink-0"
wire:click="moveBackup({{ $backup->id }})">Move</button>
<button type="button" class="button shrink-0 text-error"
wire:click="disableS3({{ $backup->id }})"
wire:confirm="Are you sure you want to disable S3 for this backup schedule?">
Disable
</button>
<div wire:key="disableS3-{{ $backup->id }}">
<x-modal-confirmation title="Disable S3 for this backup schedule?"
submitAction="disableS3({{ $backup->id }})" :confirmWithText="false"
:confirmWithPassword="false" step2ButtonText="Disable S3"
:actions="['Backups from this schedule will no longer be uploaded to S3.']">
<x-slot:trigger>
<button type="button" class="button shrink-0 text-error">Disable</button>
</x-slot:trigger>
</x-modal-confirmation>
</div>
</div>
</div>
@endforeach
@@ -142,11 +147,16 @@
portal />
<button type="button" class="button shrink-0"
wire:click="moveVolumeBackup({{ $backup->id }})">Move</button>
<button type="button" class="button shrink-0 text-error"
wire:click="disableVolumeS3({{ $backup->id }})"
wire:confirm="Are you sure you want to disable S3 for this backup schedule?">
Disable
</button>
<div wire:key="disableVolumeS3-{{ $backup->id }}">
<x-modal-confirmation title="Disable S3 for this backup schedule?"
submitAction="disableVolumeS3({{ $backup->id }})" :confirmWithText="false"
:confirmWithPassword="false" step2ButtonText="Disable S3"
:actions="['Backups from this schedule will no longer be uploaded to S3.']">
<x-slot:trigger>
<button type="button" class="button shrink-0 text-error">Disable</button>
</x-slot:trigger>
</x-modal-confirmation>
</div>
</div>
</div>
@endforeach
@@ -148,7 +148,8 @@ test('server overview shows a management button that matches the ownership state
Livewire::test(Show::class, ['server_uuid' => $this->server->uuid])
->assertSee('Disable management')
->assertDontSee('Enable management')
->call('toggleManagement')
->assertDontSeeHtml('wire:confirm')
->call('toggleManagement', '')
->assertSee('Enable management')
->assertDontSee('Disable management');
});
+21
View File
@@ -0,0 +1,21 @@
<?php
use Symfony\Component\Finder\Finder;
it('does not use native browser dialogs in views or scripts', function () {
$files = Finder::create()
->files()
->in([__DIR__.'/../../resources/views', __DIR__.'/../../resources/js'])
->name(['*.blade.php', '*.js']);
$offenders = [];
foreach ($files as $file) {
foreach (explode("\n", $file->getContents()) as $number => $line) {
if (preg_match('/wire:confirm|(?<![\w.$])(?:window\.)?(?:alert|confirm|prompt)\s*\(/', $line)) {
$offenders[] = $file->getRelativePathname().':'.($number + 1);
}
}
}
expect($offenders)->toBeEmpty();
});