fix(livewire): stop broadcast handlers from wiping in-progress form input

This commit is contained in:
Aditya Tripathi committed 2026-05-20 19:04:43 +00:00
1 parent 65c0c92c02
commit b9f773c1d9
18 files changed
+470 -247

No files matched your search

@@ -17,17 +17,10 @@ class Configuration extends Component
public $servers;
public function getListeners()
{
$teamId = auth()->user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},ServiceChecked" => '$refresh',
"echo-private:team.{$teamId},ServiceStatusChanged" => '$refresh',
'buildPackUpdated' => '$refresh',
'refresh' => '$refresh',
];
}
protected $listeners = [
'buildPackUpdated' => '$refresh',
'refresh' => '$refresh',
];
public function mount()
{
@@ -51,8 +44,6 @@ class Configuration extends Component
$this->environment = $environment;
$this->application = $application;
if ($this->application->build_pack === 'dockercompose' && $this->currentRoute === 'project.application.healthcheck') {
return redirect()->route('project.application.configuration', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]);
}
@@ -48,13 +48,26 @@ class General extends Component
public function getListeners()
{
$userId = Auth::id();
$teamId = Auth::user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},DatabaseProxyStopped" => 'databaseProxyStopped',
// Broadcasts go to refreshStatus, which only writes display-only properties.
// Never wire status broadcasts to a handler that touches text-input properties —
// it clobbers in-progress typing every 10s. See coolify#6062 / #6354 / #9695.
"echo-private:user.{$userId},DatabaseStatusChanged" => 'refreshStatus',
"echo-private:team.{$teamId},ServiceChecked" => 'refreshStatus',
];
}
public function refreshStatus(): void
{
$this->database->refresh();
$this->dbUrl = $this->database->internal_db_url;
$this->dbUrlPublic = $this->database->external_db_url;
}
public function mount()
{
try {
@@ -2,8 +2,9 @@
namespace App\Livewire\Project\Database;
use Auth;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\ItemNotFoundException;
use Livewire\Component;
class Configuration extends Component
@@ -18,15 +19,6 @@ class Configuration extends Component
public $environment;
public function getListeners()
{
$teamId = Auth::user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},ServiceChecked" => '$refresh',
];
}
public function mount()
{
try {
@@ -55,10 +47,10 @@ class Configuration extends Component
$this->dispatch('configurationChanged');
}
} catch (\Throwable $e) {
if ($e instanceof \Illuminate\Auth\Access\AuthorizationException) {
if ($e instanceof AuthorizationException) {
return redirect()->route('dashboard');
}
if ($e instanceof \Illuminate\Support\ItemNotFoundException) {
if ($e instanceof ItemNotFoundException) {
return redirect()->route('dashboard');
}
@@ -57,11 +57,22 @@ class General extends Component
return [
"echo-private:team.{$teamId},DatabaseProxyStopped" => 'databaseProxyStopped',
"echo-private:user.{$userId},DatabaseStatusChanged" => 'refresh',
"echo-private:team.{$teamId},ServiceChecked" => 'refresh',
// Broadcasts go to refreshStatus, which only writes display-only properties.
// Never wire status broadcasts to a handler that touches text-input properties —
// it clobbers in-progress typing every 10s. See coolify#6062 / #6354 / #9695.
"echo-private:user.{$userId},DatabaseStatusChanged" => 'refreshStatus',
"echo-private:team.{$teamId},ServiceChecked" => 'refreshStatus',
];
}
public function refreshStatus(): void
{
$this->database->refresh();
$this->dbUrl = $this->database->internal_db_url;
$this->dbUrlPublic = $this->database->external_db_url;
$this->certificateValidUntil = $this->database->sslCertificates()->first()?->valid_until;
}
public function mount()
{
try {
+28 -26
View File
@@ -5,9 +5,17 @@ namespace App\Livewire\Project\Database;
use App\Models\S3Storage;
use App\Models\Server;
use App\Models\Service;
use App\Models\ServiceDatabase;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use App\Support\ValidationPatterns;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Locked;
@@ -192,15 +200,9 @@ class Import extends Component
return Server::ownedByCurrentTeam()->find($this->serverId);
}
public function getListeners()
{
$userId = Auth::id();
return [
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
'slideOverClosed' => 'resetActivityId',
];
}
protected $listeners = [
'slideOverClosed' => 'resetActivityId',
];
public function resetActivityId()
{
@@ -219,7 +221,7 @@ class Import extends Component
$morphClass = $this->resource->getMorphClass();
// Handle ServiceDatabase by checking the database type
if ($morphClass === \App\Models\ServiceDatabase::class) {
if ($morphClass === ServiceDatabase::class) {
$dbType = $this->resource->databaseType();
if (str_contains($dbType, 'mysql')) {
$morphClass = 'mysql';
@@ -231,7 +233,7 @@ class Import extends Component
}
switch ($morphClass) {
case \App\Models\StandaloneMariadb::class:
case StandaloneMariadb::class:
case 'mariadb':
if ($value === true) {
$this->mariadbRestoreCommand = <<<'EOD'
@@ -247,7 +249,7 @@ EOD;
$this->mariadbRestoreCommand = 'mariadb -u $MARIADB_USER -p$MARIADB_PASSWORD $MARIADB_DATABASE';
}
break;
case \App\Models\StandaloneMysql::class:
case StandaloneMysql::class:
case 'mysql':
if ($value === true) {
$this->mysqlRestoreCommand = <<<'EOD'
@@ -263,7 +265,7 @@ EOD;
$this->mysqlRestoreCommand = 'mysql -u $MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE';
}
break;
case \App\Models\StandalonePostgresql::class:
case StandalonePostgresql::class:
case 'postgresql':
if ($value === true) {
$this->postgresqlRestoreCommand = <<<'EOD'
@@ -321,7 +323,7 @@ EOD;
$this->resourceStatus = $resource->status ?? '';
// Handle ServiceDatabase server access differently
if ($resource->getMorphClass() === \App\Models\ServiceDatabase::class) {
if ($resource->getMorphClass() === ServiceDatabase::class) {
$server = $resource->service?->server;
if (! $server) {
abort(404, 'Server not found for this service database.');
@@ -359,16 +361,16 @@ EOD;
}
if (
$resource->getMorphClass() === \App\Models\StandaloneRedis::class ||
$resource->getMorphClass() === \App\Models\StandaloneKeydb::class ||
$resource->getMorphClass() === \App\Models\StandaloneDragonfly::class ||
$resource->getMorphClass() === \App\Models\StandaloneClickhouse::class
$resource->getMorphClass() === StandaloneRedis::class ||
$resource->getMorphClass() === StandaloneKeydb::class ||
$resource->getMorphClass() === StandaloneDragonfly::class ||
$resource->getMorphClass() === StandaloneClickhouse::class
) {
$this->unsupported = true;
}
// Mark unsupported ServiceDatabase types (Redis, KeyDB, etc.)
if ($resource->getMorphClass() === \App\Models\ServiceDatabase::class) {
if ($resource->getMorphClass() === ServiceDatabase::class) {
$dbType = $resource->databaseType();
if (str_contains($dbType, 'redis') || str_contains($dbType, 'keydb') ||
str_contains($dbType, 'dragonfly') || str_contains($dbType, 'clickhouse')) {
@@ -664,7 +666,7 @@ EOD;
$fullImageName = "{$helperImage}:{$latestVersion}";
// Get the database destination network
if ($this->resource->getMorphClass() === \App\Models\ServiceDatabase::class) {
if ($this->resource->getMorphClass() === ServiceDatabase::class) {
$destinationNetwork = $this->resource->service->destination->network ?? 'coolify';
} else {
$destinationNetwork = $this->resource->destination->network ?? 'coolify';
@@ -756,7 +758,7 @@ EOD;
$morphClass = $this->resource->getMorphClass();
// Handle ServiceDatabase by checking the database type
if ($morphClass === \App\Models\ServiceDatabase::class) {
if ($morphClass === ServiceDatabase::class) {
$dbType = $this->resource->databaseType();
if (str_contains($dbType, 'mysql')) {
$morphClass = 'mysql';
@@ -770,7 +772,7 @@ EOD;
}
switch ($morphClass) {
case \App\Models\StandaloneMariadb::class:
case StandaloneMariadb::class:
case 'mariadb':
$restoreCommand = $this->mariadbRestoreCommand;
if ($this->dumpAll) {
@@ -779,7 +781,7 @@ EOD;
$restoreCommand .= " < {$tmpPath}";
}
break;
case \App\Models\StandaloneMysql::class:
case StandaloneMysql::class:
case 'mysql':
$restoreCommand = $this->mysqlRestoreCommand;
if ($this->dumpAll) {
@@ -788,7 +790,7 @@ EOD;
$restoreCommand .= " < {$tmpPath}";
}
break;
case \App\Models\StandalonePostgresql::class:
case StandalonePostgresql::class:
case 'postgresql':
$restoreCommand = $this->postgresqlRestoreCommand;
if ($this->dumpAll) {
@@ -797,7 +799,7 @@ EOD;
$restoreCommand .= " {$tmpPath}";
}
break;
case \App\Models\StandaloneMongodb::class:
case StandaloneMongodb::class:
case 'mongodb':
$restoreCommand = $this->mongodbRestoreCommand;
if ($this->dumpAll === false) {
@@ -59,11 +59,22 @@ class General extends Component
return [
"echo-private:team.{$teamId},DatabaseProxyStopped" => 'databaseProxyStopped',
"echo-private:user.{$userId},DatabaseStatusChanged" => 'refresh',
"echo-private:team.{$teamId},ServiceChecked" => 'refresh',
// Broadcasts go to refreshStatus, which only writes display-only properties.
// Never wire status broadcasts to a handler that touches text-input properties —
// it clobbers in-progress typing every 10s. See coolify#6062 / #6354 / #9695.
"echo-private:user.{$userId},DatabaseStatusChanged" => 'refreshStatus',
"echo-private:team.{$teamId},ServiceChecked" => 'refreshStatus',
];
}
public function refreshStatus(): void
{
$this->database->refresh();
$this->dbUrl = $this->database->internal_db_url;
$this->dbUrlPublic = $this->database->external_db_url;
$this->certificateValidUntil = $this->database->sslCertificates()->first()?->valid_until;
}
public function mount()
{
try {
@@ -64,11 +64,22 @@ class General extends Component
$teamId = Auth::user()->currentTeam()->id;
return [
"echo-private:user.{$userId},DatabaseStatusChanged" => 'refresh',
"echo-private:team.{$teamId},ServiceChecked" => 'refresh',
// Broadcasts go to refreshStatus, which only writes display-only properties.
// Never wire status broadcasts to a handler that touches text-input properties —
// it clobbers in-progress typing every 10s. See coolify#6062 / #6354 / #9695.
"echo-private:user.{$userId},DatabaseStatusChanged" => 'refreshStatus',
"echo-private:team.{$teamId},ServiceChecked" => 'refreshStatus',
];
}
public function refreshStatus(): void
{
$this->database->refresh();
$this->db_url = $this->database->internal_db_url;
$this->db_url_public = $this->database->external_db_url;
$this->certificateValidUntil = $this->database->sslCertificates()->first()?->valid_until;
}
protected function rules(): array
{
return [
@@ -64,11 +64,22 @@ class General extends Component
$teamId = Auth::user()->currentTeam()->id;
return [
"echo-private:user.{$userId},DatabaseStatusChanged" => 'refresh',
"echo-private:team.{$teamId},ServiceChecked" => 'refresh',
// Broadcasts go to refreshStatus, which only writes display-only properties.
// Never wire status broadcasts to a handler that touches text-input properties —
// it clobbers in-progress typing every 10s. See coolify#6062 / #6354 / #9695.
"echo-private:user.{$userId},DatabaseStatusChanged" => 'refreshStatus',
"echo-private:team.{$teamId},ServiceChecked" => 'refreshStatus',
];
}
public function refreshStatus(): void
{
$this->database->refresh();
$this->db_url = $this->database->internal_db_url;
$this->db_url_public = $this->database->external_db_url;
$this->certificateValidUntil = $this->database->sslCertificates()->first()?->valid_until;
}
protected function rules(): array
{
return [
@@ -66,11 +66,22 @@ class General extends Component
$teamId = Auth::user()->currentTeam()->id;
return [
"echo-private:user.{$userId},DatabaseStatusChanged" => 'refresh',
"echo-private:team.{$teamId},ServiceChecked" => 'refresh',
// Broadcasts go to refreshStatus, which only writes display-only properties.
// Never wire status broadcasts to a handler that touches text-input properties —
// it clobbers in-progress typing every 10s. See coolify#6062 / #6354 / #9695.
"echo-private:user.{$userId},DatabaseStatusChanged" => 'refreshStatus',
"echo-private:team.{$teamId},ServiceChecked" => 'refreshStatus',
];
}
public function refreshStatus(): void
{
$this->database->refresh();
$this->db_url = $this->database->internal_db_url;
$this->db_url_public = $this->database->external_db_url;
$this->certificateValidUntil = $this->database->sslCertificates()->first()?->valid_until;
}
protected function rules(): array
{
return [
@@ -74,13 +74,24 @@ class General extends Component
$teamId = Auth::user()->currentTeam()->id;
return [
"echo-private:user.{$userId},DatabaseStatusChanged" => 'refresh',
"echo-private:team.{$teamId},ServiceChecked" => 'refresh',
// Broadcasts go to refreshStatus, which only writes display-only properties.
// Never wire status broadcasts to a handler that touches text-input properties —
// it clobbers in-progress typing every 10s. See coolify#6062 / #6354 / #9695.
"echo-private:user.{$userId},DatabaseStatusChanged" => 'refreshStatus',
"echo-private:team.{$teamId},ServiceChecked" => 'refreshStatus',
'save_init_script',
'delete_init_script',
];
}
public function refreshStatus(): void
{
$this->database->refresh();
$this->db_url = $this->database->internal_db_url;
$this->db_url_public = $this->database->external_db_url;
$this->certificateValidUntil = $this->database->sslCertificates()->first()?->valid_until;
}
protected function rules(): array
{
return [
@@ -4,14 +4,11 @@ namespace App\Livewire\Project\Database\Redis;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
use App\Helpers\SslHelper;
use App\Models\Server;
use App\Models\StandaloneRedis;
use App\Support\ValidationPatterns;
use Carbon\Carbon;
use Exception;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
class General extends Component
@@ -48,25 +45,9 @@ class General extends Component
public string $redisVersion;
public ?string $dbUrl = null;
public ?string $dbUrlPublic = null;
public bool $enableSsl = false;
public ?Carbon $certificateValidUntil = null;
public function getListeners()
{
$userId = Auth::id();
$teamId = Auth::user()->currentTeam()->id;
return [
"echo-private:user.{$userId},DatabaseStatusChanged" => 'refresh',
"echo-private:team.{$teamId},ServiceChecked" => 'refresh',
'envsUpdated' => 'refresh',
];
}
protected $listeners = [
'envsUpdated' => 'refresh',
];
protected function rules(): array
{
@@ -87,7 +68,6 @@ class General extends Component
'redisPassword' => ValidationPatterns::databasePasswordRules(
enforcePattern: $this->redisPassword !== $this->database->redis_password,
),
'enableSsl' => 'boolean',
];
}
@@ -122,7 +102,6 @@ class General extends Component
'customDockerRunOptions' => 'Custom Docker Options',
'redisUsername' => 'Redis Username',
'redisPassword' => 'Redis Password',
'enableSsl' => 'Enable SSL',
];
public function mount()
@@ -136,12 +115,6 @@ class General extends Component
return;
}
$existingCert = $this->database->sslCertificates()->first();
if ($existingCert) {
$this->certificateValidUntil = $existingCert->valid_until;
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -161,11 +134,7 @@ class General extends Component
$this->database->public_port_timeout = $this->publicPortTimeout ?: null;
$this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
$this->database->custom_docker_run_options = $this->customDockerRunOptions;
$this->database->enable_ssl = $this->enableSsl;
$this->database->save();
$this->dbUrl = $this->database->internal_db_url;
$this->dbUrlPublic = $this->database->external_db_url;
} else {
$this->name = $this->database->name;
$this->description = $this->database->description;
@@ -177,9 +146,6 @@ class General extends Component
$this->publicPortTimeout = $this->database->public_port_timeout;
$this->isLogDrainEnabled = $this->database->is_log_drain_enabled;
$this->customDockerRunOptions = $this->database->custom_docker_run_options;
$this->enableSsl = $this->database->enable_ssl;
$this->dbUrl = $this->database->internal_db_url;
$this->dbUrlPublic = $this->database->external_db_url;
$this->redisVersion = $this->database->getRedisVersion();
$this->redisUsername = $this->database->redis_username;
$this->redisPassword = $this->database->redis_password;
@@ -227,6 +193,7 @@ class General extends Component
);
$this->dispatch('success', 'Database updated.');
$this->dispatch('databaseUpdated');
} catch (Exception $e) {
return handleError($e, $this);
} finally {
@@ -259,6 +226,7 @@ class General extends Component
StopDatabaseProxy::run($this->database);
$this->dispatch('success', 'Database is no longer publicly accessible.');
}
$this->dispatch('databaseUpdated');
} catch (\Throwable $e) {
$this->isPublic = ! $this->isPublic;
$this->syncData(true);
@@ -267,63 +235,6 @@ class General extends Component
}
}
public function instantSaveSSL()
{
try {
$this->authorize('update', $this->database);
$this->syncData(true);
$this->dispatch('success', 'SSL configuration updated.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function regenerateSslCertificate()
{
try {
$this->authorize('update', $this->database);
$existingCert = $this->database->sslCertificates()->first();
if (! $existingCert) {
$this->dispatch('error', 'No existing SSL certificate found for this database.');
return;
}
$caCert = $this->server->sslCertificates()->where('is_ca_certificate', true)->first();
if (! $caCert) {
$this->server->generateCaCertificate();
$caCert = $this->server->sslCertificates()->where('is_ca_certificate', true)->first();
}
if (! $caCert) {
$this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
return;
}
SslHelper::generateSslCertificate(
commonName: $existingCert->common_name,
subjectAlternativeNames: $existingCert->subject_alternative_names ?? [],
resourceType: $existingCert->resource_type,
resourceId: $existingCert->resource_id,
serverId: $existingCert->server_id,
caCert: $caCert->ssl_certificate,
caKey: $caCert->ssl_private_key,
configurationDir: $existingCert->configuration_dir,
mountPath: $existingCert->mount_path,
isPemKeyFileRequired: true,
);
$this->dispatch('success', 'SSL certificates regenerated. Restart database to apply changes.');
} catch (Exception $e) {
handleError($e, $this);
}
}
public function refresh(): void
{
$this->database->refresh();
@@ -0,0 +1,116 @@
<?php
namespace App\Livewire\Project\Database\Redis;
use App\Helpers\SslHelper;
use App\Models\StandaloneRedis;
use Carbon\Carbon;
use Exception;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
class StatusInfo extends Component
{
use AuthorizesRequests;
public StandaloneRedis $database;
public bool $enableSsl = false;
public ?Carbon $certificateValidUntil = null;
public ?string $dbUrl = null;
public ?string $dbUrlPublic = null;
public function getListeners()
{
$userId = Auth::id();
$teamId = Auth::user()->currentTeam()->id;
return [
"echo-private:user.{$userId},DatabaseStatusChanged" => 'refresh',
"echo-private:team.{$teamId},ServiceChecked" => 'refresh',
'databaseUpdated' => 'refresh',
];
}
public function mount(): void
{
$this->refresh();
}
public function refresh(): void
{
$this->database->refresh();
$this->enableSsl = (bool) $this->database->enable_ssl;
$this->dbUrl = $this->database->internal_db_url;
$this->dbUrlPublic = $this->database->external_db_url;
$this->certificateValidUntil = $this->database->sslCertificates()->first()?->valid_until;
}
public function instantSaveSSL(): void
{
try {
$this->authorize('update', $this->database);
$this->database->enable_ssl = $this->enableSsl;
$this->database->save();
$this->dispatch('success', 'SSL configuration updated.');
} catch (Exception $e) {
handleError($e, $this);
}
}
public function regenerateSslCertificate(): void
{
try {
$this->authorize('update', $this->database);
$existingCert = $this->database->sslCertificates()->first();
if (! $existingCert) {
$this->dispatch('error', 'No existing SSL certificate found for this database.');
return;
}
$server = $this->database->destination->server;
$caCert = $server->sslCertificates()->where('is_ca_certificate', true)->first();
if (! $caCert) {
$server->generateCaCertificate();
$caCert = $server->sslCertificates()->where('is_ca_certificate', true)->first();
}
if (! $caCert) {
$this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
return;
}
SslHelper::generateSslCertificate(
commonName: $existingCert->common_name,
subjectAlternativeNames: $existingCert->subject_alternative_names ?? [],
resourceType: $existingCert->resource_type,
resourceId: $existingCert->resource_id,
serverId: $existingCert->server_id,
caCert: $caCert->ssl_certificate,
caKey: $caCert->ssl_private_key,
configurationDir: $existingCert->configuration_dir,
mountPath: $existingCert->mount_path,
isPemKeyFileRequired: true,
);
$this->refresh();
$this->dispatch('success', 'SSL certificates regenerated. Restart database to apply changes.');
} catch (Exception $e) {
handleError($e, $this);
}
}
public function render()
{
return view('livewire.project.database.redis.status-info');
}
}
+4 -25
View File
@@ -4,7 +4,6 @@ namespace App\Livewire\Project\Service;
use App\Models\Service;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
class Configuration extends Component
@@ -27,16 +26,10 @@ class Configuration extends Component
public array $parameters;
public function getListeners()
{
$teamId = Auth::user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},ServiceChecked" => 'serviceChecked',
'refreshServices' => 'refreshServices',
'refresh' => 'refreshServices',
];
}
protected $listeners = [
'refreshServices' => 'refreshServices',
'refresh' => 'refreshServices',
];
public function render()
{
@@ -105,18 +98,4 @@ class Configuration extends Component
return handleError($e, $this);
}
}
public function serviceChecked()
{
try {
$this->service->applications->each(function ($application) {
$application->refresh();
});
$this->service->databases->each(function ($database) {
$database->refresh();
});
} catch (\Exception $e) {
return handleError($e, $this);
}
}
}