fix(livewire): stop broadcast handlers from wiping in-progress form input
This commit is contained in:
parent
65c0c92c02
commit
b9f773c1d9
18 changed files with 470 additions and 247 deletions
|
|
@ -17,17 +17,10 @@ class Configuration extends Component
|
||||||
|
|
||||||
public $servers;
|
public $servers;
|
||||||
|
|
||||||
public function getListeners()
|
protected $listeners = [
|
||||||
{
|
'buildPackUpdated' => '$refresh',
|
||||||
$teamId = auth()->user()->currentTeam()->id;
|
'refresh' => '$refresh',
|
||||||
|
];
|
||||||
return [
|
|
||||||
"echo-private:team.{$teamId},ServiceChecked" => '$refresh',
|
|
||||||
"echo-private:team.{$teamId},ServiceStatusChanged" => '$refresh',
|
|
||||||
'buildPackUpdated' => '$refresh',
|
|
||||||
'refresh' => '$refresh',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function mount()
|
public function mount()
|
||||||
{
|
{
|
||||||
|
|
@ -51,8 +44,6 @@ public function mount()
|
||||||
$this->environment = $environment;
|
$this->environment = $environment;
|
||||||
$this->application = $application;
|
$this->application = $application;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if ($this->application->build_pack === 'dockercompose' && $this->currentRoute === 'project.application.healthcheck') {
|
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]);
|
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()
|
public function getListeners()
|
||||||
{
|
{
|
||||||
|
$userId = Auth::id();
|
||||||
$teamId = Auth::user()->currentTeam()->id;
|
$teamId = Auth::user()->currentTeam()->id;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
"echo-private:team.{$teamId},DatabaseProxyStopped" => 'databaseProxyStopped',
|
"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()
|
public function mount()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,9 @@
|
||||||
|
|
||||||
namespace App\Livewire\Project\Database;
|
namespace App\Livewire\Project\Database;
|
||||||
|
|
||||||
use Auth;
|
use Illuminate\Auth\Access\AuthorizationException;
|
||||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||||
|
use Illuminate\Support\ItemNotFoundException;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
class Configuration extends Component
|
class Configuration extends Component
|
||||||
|
|
@ -18,15 +19,6 @@ class Configuration extends Component
|
||||||
|
|
||||||
public $environment;
|
public $environment;
|
||||||
|
|
||||||
public function getListeners()
|
|
||||||
{
|
|
||||||
$teamId = Auth::user()->currentTeam()->id;
|
|
||||||
|
|
||||||
return [
|
|
||||||
"echo-private:team.{$teamId},ServiceChecked" => '$refresh',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function mount()
|
public function mount()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|
@ -55,10 +47,10 @@ public function mount()
|
||||||
$this->dispatch('configurationChanged');
|
$this->dispatch('configurationChanged');
|
||||||
}
|
}
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
if ($e instanceof \Illuminate\Auth\Access\AuthorizationException) {
|
if ($e instanceof AuthorizationException) {
|
||||||
return redirect()->route('dashboard');
|
return redirect()->route('dashboard');
|
||||||
}
|
}
|
||||||
if ($e instanceof \Illuminate\Support\ItemNotFoundException) {
|
if ($e instanceof ItemNotFoundException) {
|
||||||
return redirect()->route('dashboard');
|
return redirect()->route('dashboard');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,11 +57,22 @@ public function getListeners()
|
||||||
|
|
||||||
return [
|
return [
|
||||||
"echo-private:team.{$teamId},DatabaseProxyStopped" => 'databaseProxyStopped',
|
"echo-private:team.{$teamId},DatabaseProxyStopped" => 'databaseProxyStopped',
|
||||||
"echo-private:user.{$userId},DatabaseStatusChanged" => 'refresh',
|
// Broadcasts go to refreshStatus, which only writes display-only properties.
|
||||||
"echo-private:team.{$teamId},ServiceChecked" => 'refresh',
|
// 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()
|
public function mount()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,17 @@
|
||||||
use App\Models\S3Storage;
|
use App\Models\S3Storage;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
use App\Models\Service;
|
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 App\Support\ValidationPatterns;
|
||||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Livewire\Attributes\Computed;
|
use Livewire\Attributes\Computed;
|
||||||
use Livewire\Attributes\Locked;
|
use Livewire\Attributes\Locked;
|
||||||
|
|
@ -192,15 +200,9 @@ public function server()
|
||||||
return Server::ownedByCurrentTeam()->find($this->serverId);
|
return Server::ownedByCurrentTeam()->find($this->serverId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getListeners()
|
protected $listeners = [
|
||||||
{
|
'slideOverClosed' => 'resetActivityId',
|
||||||
$userId = Auth::id();
|
];
|
||||||
|
|
||||||
return [
|
|
||||||
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
|
|
||||||
'slideOverClosed' => 'resetActivityId',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function resetActivityId()
|
public function resetActivityId()
|
||||||
{
|
{
|
||||||
|
|
@ -219,7 +221,7 @@ public function updatedDumpAll($value)
|
||||||
$morphClass = $this->resource->getMorphClass();
|
$morphClass = $this->resource->getMorphClass();
|
||||||
|
|
||||||
// Handle ServiceDatabase by checking the database type
|
// Handle ServiceDatabase by checking the database type
|
||||||
if ($morphClass === \App\Models\ServiceDatabase::class) {
|
if ($morphClass === ServiceDatabase::class) {
|
||||||
$dbType = $this->resource->databaseType();
|
$dbType = $this->resource->databaseType();
|
||||||
if (str_contains($dbType, 'mysql')) {
|
if (str_contains($dbType, 'mysql')) {
|
||||||
$morphClass = 'mysql';
|
$morphClass = 'mysql';
|
||||||
|
|
@ -231,7 +233,7 @@ public function updatedDumpAll($value)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch ($morphClass) {
|
switch ($morphClass) {
|
||||||
case \App\Models\StandaloneMariadb::class:
|
case StandaloneMariadb::class:
|
||||||
case 'mariadb':
|
case 'mariadb':
|
||||||
if ($value === true) {
|
if ($value === true) {
|
||||||
$this->mariadbRestoreCommand = <<<'EOD'
|
$this->mariadbRestoreCommand = <<<'EOD'
|
||||||
|
|
@ -247,7 +249,7 @@ public function updatedDumpAll($value)
|
||||||
$this->mariadbRestoreCommand = 'mariadb -u $MARIADB_USER -p$MARIADB_PASSWORD $MARIADB_DATABASE';
|
$this->mariadbRestoreCommand = 'mariadb -u $MARIADB_USER -p$MARIADB_PASSWORD $MARIADB_DATABASE';
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case \App\Models\StandaloneMysql::class:
|
case StandaloneMysql::class:
|
||||||
case 'mysql':
|
case 'mysql':
|
||||||
if ($value === true) {
|
if ($value === true) {
|
||||||
$this->mysqlRestoreCommand = <<<'EOD'
|
$this->mysqlRestoreCommand = <<<'EOD'
|
||||||
|
|
@ -263,7 +265,7 @@ public function updatedDumpAll($value)
|
||||||
$this->mysqlRestoreCommand = 'mysql -u $MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE';
|
$this->mysqlRestoreCommand = 'mysql -u $MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE';
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case \App\Models\StandalonePostgresql::class:
|
case StandalonePostgresql::class:
|
||||||
case 'postgresql':
|
case 'postgresql':
|
||||||
if ($value === true) {
|
if ($value === true) {
|
||||||
$this->postgresqlRestoreCommand = <<<'EOD'
|
$this->postgresqlRestoreCommand = <<<'EOD'
|
||||||
|
|
@ -321,7 +323,7 @@ public function getContainers()
|
||||||
$this->resourceStatus = $resource->status ?? '';
|
$this->resourceStatus = $resource->status ?? '';
|
||||||
|
|
||||||
// Handle ServiceDatabase server access differently
|
// Handle ServiceDatabase server access differently
|
||||||
if ($resource->getMorphClass() === \App\Models\ServiceDatabase::class) {
|
if ($resource->getMorphClass() === ServiceDatabase::class) {
|
||||||
$server = $resource->service?->server;
|
$server = $resource->service?->server;
|
||||||
if (! $server) {
|
if (! $server) {
|
||||||
abort(404, 'Server not found for this service database.');
|
abort(404, 'Server not found for this service database.');
|
||||||
|
|
@ -359,16 +361,16 @@ public function getContainers()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
$resource->getMorphClass() === \App\Models\StandaloneRedis::class ||
|
$resource->getMorphClass() === StandaloneRedis::class ||
|
||||||
$resource->getMorphClass() === \App\Models\StandaloneKeydb::class ||
|
$resource->getMorphClass() === StandaloneKeydb::class ||
|
||||||
$resource->getMorphClass() === \App\Models\StandaloneDragonfly::class ||
|
$resource->getMorphClass() === StandaloneDragonfly::class ||
|
||||||
$resource->getMorphClass() === \App\Models\StandaloneClickhouse::class
|
$resource->getMorphClass() === StandaloneClickhouse::class
|
||||||
) {
|
) {
|
||||||
$this->unsupported = true;
|
$this->unsupported = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark unsupported ServiceDatabase types (Redis, KeyDB, etc.)
|
// Mark unsupported ServiceDatabase types (Redis, KeyDB, etc.)
|
||||||
if ($resource->getMorphClass() === \App\Models\ServiceDatabase::class) {
|
if ($resource->getMorphClass() === ServiceDatabase::class) {
|
||||||
$dbType = $resource->databaseType();
|
$dbType = $resource->databaseType();
|
||||||
if (str_contains($dbType, 'redis') || str_contains($dbType, 'keydb') ||
|
if (str_contains($dbType, 'redis') || str_contains($dbType, 'keydb') ||
|
||||||
str_contains($dbType, 'dragonfly') || str_contains($dbType, 'clickhouse')) {
|
str_contains($dbType, 'dragonfly') || str_contains($dbType, 'clickhouse')) {
|
||||||
|
|
@ -664,7 +666,7 @@ public function restoreFromS3(string $password = ''): bool|string
|
||||||
$fullImageName = "{$helperImage}:{$latestVersion}";
|
$fullImageName = "{$helperImage}:{$latestVersion}";
|
||||||
|
|
||||||
// Get the database destination network
|
// 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';
|
$destinationNetwork = $this->resource->service->destination->network ?? 'coolify';
|
||||||
} else {
|
} else {
|
||||||
$destinationNetwork = $this->resource->destination->network ?? 'coolify';
|
$destinationNetwork = $this->resource->destination->network ?? 'coolify';
|
||||||
|
|
@ -756,7 +758,7 @@ public function buildRestoreCommand(string $tmpPath): string
|
||||||
$morphClass = $this->resource->getMorphClass();
|
$morphClass = $this->resource->getMorphClass();
|
||||||
|
|
||||||
// Handle ServiceDatabase by checking the database type
|
// Handle ServiceDatabase by checking the database type
|
||||||
if ($morphClass === \App\Models\ServiceDatabase::class) {
|
if ($morphClass === ServiceDatabase::class) {
|
||||||
$dbType = $this->resource->databaseType();
|
$dbType = $this->resource->databaseType();
|
||||||
if (str_contains($dbType, 'mysql')) {
|
if (str_contains($dbType, 'mysql')) {
|
||||||
$morphClass = 'mysql';
|
$morphClass = 'mysql';
|
||||||
|
|
@ -770,7 +772,7 @@ public function buildRestoreCommand(string $tmpPath): string
|
||||||
}
|
}
|
||||||
|
|
||||||
switch ($morphClass) {
|
switch ($morphClass) {
|
||||||
case \App\Models\StandaloneMariadb::class:
|
case StandaloneMariadb::class:
|
||||||
case 'mariadb':
|
case 'mariadb':
|
||||||
$restoreCommand = $this->mariadbRestoreCommand;
|
$restoreCommand = $this->mariadbRestoreCommand;
|
||||||
if ($this->dumpAll) {
|
if ($this->dumpAll) {
|
||||||
|
|
@ -779,7 +781,7 @@ public function buildRestoreCommand(string $tmpPath): string
|
||||||
$restoreCommand .= " < {$tmpPath}";
|
$restoreCommand .= " < {$tmpPath}";
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case \App\Models\StandaloneMysql::class:
|
case StandaloneMysql::class:
|
||||||
case 'mysql':
|
case 'mysql':
|
||||||
$restoreCommand = $this->mysqlRestoreCommand;
|
$restoreCommand = $this->mysqlRestoreCommand;
|
||||||
if ($this->dumpAll) {
|
if ($this->dumpAll) {
|
||||||
|
|
@ -788,7 +790,7 @@ public function buildRestoreCommand(string $tmpPath): string
|
||||||
$restoreCommand .= " < {$tmpPath}";
|
$restoreCommand .= " < {$tmpPath}";
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case \App\Models\StandalonePostgresql::class:
|
case StandalonePostgresql::class:
|
||||||
case 'postgresql':
|
case 'postgresql':
|
||||||
$restoreCommand = $this->postgresqlRestoreCommand;
|
$restoreCommand = $this->postgresqlRestoreCommand;
|
||||||
if ($this->dumpAll) {
|
if ($this->dumpAll) {
|
||||||
|
|
@ -797,7 +799,7 @@ public function buildRestoreCommand(string $tmpPath): string
|
||||||
$restoreCommand .= " {$tmpPath}";
|
$restoreCommand .= " {$tmpPath}";
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case \App\Models\StandaloneMongodb::class:
|
case StandaloneMongodb::class:
|
||||||
case 'mongodb':
|
case 'mongodb':
|
||||||
$restoreCommand = $this->mongodbRestoreCommand;
|
$restoreCommand = $this->mongodbRestoreCommand;
|
||||||
if ($this->dumpAll === false) {
|
if ($this->dumpAll === false) {
|
||||||
|
|
|
||||||
|
|
@ -59,11 +59,22 @@ public function getListeners()
|
||||||
|
|
||||||
return [
|
return [
|
||||||
"echo-private:team.{$teamId},DatabaseProxyStopped" => 'databaseProxyStopped',
|
"echo-private:team.{$teamId},DatabaseProxyStopped" => 'databaseProxyStopped',
|
||||||
"echo-private:user.{$userId},DatabaseStatusChanged" => 'refresh',
|
// Broadcasts go to refreshStatus, which only writes display-only properties.
|
||||||
"echo-private:team.{$teamId},ServiceChecked" => 'refresh',
|
// 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()
|
public function mount()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -64,11 +64,22 @@ public function getListeners()
|
||||||
$teamId = Auth::user()->currentTeam()->id;
|
$teamId = Auth::user()->currentTeam()->id;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
"echo-private:user.{$userId},DatabaseStatusChanged" => 'refresh',
|
// Broadcasts go to refreshStatus, which only writes display-only properties.
|
||||||
"echo-private:team.{$teamId},ServiceChecked" => 'refresh',
|
// 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
|
protected function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
|
|
||||||
|
|
@ -64,11 +64,22 @@ public function getListeners()
|
||||||
$teamId = Auth::user()->currentTeam()->id;
|
$teamId = Auth::user()->currentTeam()->id;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
"echo-private:user.{$userId},DatabaseStatusChanged" => 'refresh',
|
// Broadcasts go to refreshStatus, which only writes display-only properties.
|
||||||
"echo-private:team.{$teamId},ServiceChecked" => 'refresh',
|
// 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
|
protected function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
|
|
||||||
|
|
@ -66,11 +66,22 @@ public function getListeners()
|
||||||
$teamId = Auth::user()->currentTeam()->id;
|
$teamId = Auth::user()->currentTeam()->id;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
"echo-private:user.{$userId},DatabaseStatusChanged" => 'refresh',
|
// Broadcasts go to refreshStatus, which only writes display-only properties.
|
||||||
"echo-private:team.{$teamId},ServiceChecked" => 'refresh',
|
// 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
|
protected function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
|
|
||||||
|
|
@ -74,13 +74,24 @@ public function getListeners()
|
||||||
$teamId = Auth::user()->currentTeam()->id;
|
$teamId = Auth::user()->currentTeam()->id;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
"echo-private:user.{$userId},DatabaseStatusChanged" => 'refresh',
|
// Broadcasts go to refreshStatus, which only writes display-only properties.
|
||||||
"echo-private:team.{$teamId},ServiceChecked" => 'refresh',
|
// 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',
|
'save_init_script',
|
||||||
'delete_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
|
protected function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,11 @@
|
||||||
|
|
||||||
use App\Actions\Database\StartDatabaseProxy;
|
use App\Actions\Database\StartDatabaseProxy;
|
||||||
use App\Actions\Database\StopDatabaseProxy;
|
use App\Actions\Database\StopDatabaseProxy;
|
||||||
use App\Helpers\SslHelper;
|
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
use App\Models\StandaloneRedis;
|
use App\Models\StandaloneRedis;
|
||||||
use App\Support\ValidationPatterns;
|
use App\Support\ValidationPatterns;
|
||||||
use Carbon\Carbon;
|
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
class General extends Component
|
class General extends Component
|
||||||
|
|
@ -48,25 +45,9 @@ class General extends Component
|
||||||
|
|
||||||
public string $redisVersion;
|
public string $redisVersion;
|
||||||
|
|
||||||
public ?string $dbUrl = null;
|
protected $listeners = [
|
||||||
|
'envsUpdated' => 'refresh',
|
||||||
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 function rules(): array
|
protected function rules(): array
|
||||||
{
|
{
|
||||||
|
|
@ -87,7 +68,6 @@ protected function rules(): array
|
||||||
'redisPassword' => ValidationPatterns::databasePasswordRules(
|
'redisPassword' => ValidationPatterns::databasePasswordRules(
|
||||||
enforcePattern: $this->redisPassword !== $this->database->redis_password,
|
enforcePattern: $this->redisPassword !== $this->database->redis_password,
|
||||||
),
|
),
|
||||||
'enableSsl' => 'boolean',
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -122,7 +102,6 @@ protected function messages(): array
|
||||||
'customDockerRunOptions' => 'Custom Docker Options',
|
'customDockerRunOptions' => 'Custom Docker Options',
|
||||||
'redisUsername' => 'Redis Username',
|
'redisUsername' => 'Redis Username',
|
||||||
'redisPassword' => 'Redis Password',
|
'redisPassword' => 'Redis Password',
|
||||||
'enableSsl' => 'Enable SSL',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
public function mount()
|
public function mount()
|
||||||
|
|
@ -136,12 +115,6 @@ public function mount()
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$existingCert = $this->database->sslCertificates()->first();
|
|
||||||
|
|
||||||
if ($existingCert) {
|
|
||||||
$this->certificateValidUntil = $existingCert->valid_until;
|
|
||||||
}
|
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return handleError($e, $this);
|
return handleError($e, $this);
|
||||||
}
|
}
|
||||||
|
|
@ -161,11 +134,7 @@ public function syncData(bool $toModel = false)
|
||||||
$this->database->public_port_timeout = $this->publicPortTimeout ?: null;
|
$this->database->public_port_timeout = $this->publicPortTimeout ?: null;
|
||||||
$this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
|
$this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
|
||||||
$this->database->custom_docker_run_options = $this->customDockerRunOptions;
|
$this->database->custom_docker_run_options = $this->customDockerRunOptions;
|
||||||
$this->database->enable_ssl = $this->enableSsl;
|
|
||||||
$this->database->save();
|
$this->database->save();
|
||||||
|
|
||||||
$this->dbUrl = $this->database->internal_db_url;
|
|
||||||
$this->dbUrlPublic = $this->database->external_db_url;
|
|
||||||
} else {
|
} else {
|
||||||
$this->name = $this->database->name;
|
$this->name = $this->database->name;
|
||||||
$this->description = $this->database->description;
|
$this->description = $this->database->description;
|
||||||
|
|
@ -177,9 +146,6 @@ public function syncData(bool $toModel = false)
|
||||||
$this->publicPortTimeout = $this->database->public_port_timeout;
|
$this->publicPortTimeout = $this->database->public_port_timeout;
|
||||||
$this->isLogDrainEnabled = $this->database->is_log_drain_enabled;
|
$this->isLogDrainEnabled = $this->database->is_log_drain_enabled;
|
||||||
$this->customDockerRunOptions = $this->database->custom_docker_run_options;
|
$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->redisVersion = $this->database->getRedisVersion();
|
||||||
$this->redisUsername = $this->database->redis_username;
|
$this->redisUsername = $this->database->redis_username;
|
||||||
$this->redisPassword = $this->database->redis_password;
|
$this->redisPassword = $this->database->redis_password;
|
||||||
|
|
@ -227,6 +193,7 @@ public function submit()
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->dispatch('success', 'Database updated.');
|
$this->dispatch('success', 'Database updated.');
|
||||||
|
$this->dispatch('databaseUpdated');
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
return handleError($e, $this);
|
return handleError($e, $this);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -259,6 +226,7 @@ public function instantSave()
|
||||||
StopDatabaseProxy::run($this->database);
|
StopDatabaseProxy::run($this->database);
|
||||||
$this->dispatch('success', 'Database is no longer publicly accessible.');
|
$this->dispatch('success', 'Database is no longer publicly accessible.');
|
||||||
}
|
}
|
||||||
|
$this->dispatch('databaseUpdated');
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$this->isPublic = ! $this->isPublic;
|
$this->isPublic = ! $this->isPublic;
|
||||||
$this->syncData(true);
|
$this->syncData(true);
|
||||||
|
|
@ -267,63 +235,6 @@ public function instantSave()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
public function refresh(): void
|
||||||
{
|
{
|
||||||
$this->database->refresh();
|
$this->database->refresh();
|
||||||
|
|
|
||||||
116
app/Livewire/Project/Database/Redis/StatusInfo.php
Normal file
116
app/Livewire/Project/Database/Redis/StatusInfo.php
Normal file
|
|
@ -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,7 +4,6 @@
|
||||||
|
|
||||||
use App\Models\Service;
|
use App\Models\Service;
|
||||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
class Configuration extends Component
|
class Configuration extends Component
|
||||||
|
|
@ -27,16 +26,10 @@ class Configuration extends Component
|
||||||
|
|
||||||
public array $parameters;
|
public array $parameters;
|
||||||
|
|
||||||
public function getListeners()
|
protected $listeners = [
|
||||||
{
|
'refreshServices' => 'refreshServices',
|
||||||
$teamId = Auth::user()->currentTeam()->id;
|
'refresh' => 'refreshServices',
|
||||||
|
];
|
||||||
return [
|
|
||||||
"echo-private:team.{$teamId},ServiceChecked" => 'serviceChecked',
|
|
||||||
'refreshServices' => 'refreshServices',
|
|
||||||
'refresh' => 'refreshServices',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function render()
|
public function render()
|
||||||
{
|
{
|
||||||
|
|
@ -105,18 +98,4 @@ public function restartDatabase($id)
|
||||||
return handleError($e, $this);
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,9 @@ public function handleSentinelRestarted($event)
|
||||||
{
|
{
|
||||||
if ($event['serverUuid'] === $this->server->uuid) {
|
if ($event['serverUuid'] === $this->server->uuid) {
|
||||||
$this->server->refresh();
|
$this->server->refresh();
|
||||||
$this->syncData();
|
// Only refresh display-only state; never re-sync text-input properties
|
||||||
|
// (would clobber any unsaved typing — see coolify#6062 / #6354 / #9695).
|
||||||
|
$this->sentinelUpdatedAt = $this->server->sentinel_updated_at;
|
||||||
$this->dispatch('success', 'Sentinel has been restarted successfully.');
|
$this->dispatch('success', 'Sentinel has been restarted successfully.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -277,7 +277,9 @@ public function handleSentinelRestarted($event)
|
||||||
// Only refresh if the event is for this server
|
// Only refresh if the event is for this server
|
||||||
if (isset($event['serverUuid']) && $event['serverUuid'] === $this->server->uuid) {
|
if (isset($event['serverUuid']) && $event['serverUuid'] === $this->server->uuid) {
|
||||||
$this->server->refresh();
|
$this->server->refresh();
|
||||||
$this->syncData();
|
// Only refresh display-only state; never re-sync text-input properties
|
||||||
|
// (would clobber any unsaved typing — see coolify#6062 / #6354 / #9695).
|
||||||
|
$this->sentinelUpdatedAt = $this->server->sentinel_updated_at;
|
||||||
$this->dispatch('success', 'Sentinel has been restarted successfully.');
|
$this->dispatch('success', 'Sentinel has been restarted successfully.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -457,12 +459,15 @@ public function handleServerValidated($event = null)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh server data
|
// Refresh server data and only the display-only state that validation produces.
|
||||||
|
// Never re-sync text-input properties via syncData() — would clobber any
|
||||||
|
// unsaved typing (see coolify#6062 / #6354 / #9695).
|
||||||
$this->server->refresh();
|
$this->server->refresh();
|
||||||
$this->syncData();
|
$this->server->settings->refresh();
|
||||||
|
|
||||||
// Update validation state
|
|
||||||
$this->isValidating = $this->server->is_validating ?? false;
|
$this->isValidating = $this->server->is_validating ?? false;
|
||||||
|
$this->validationLogs = $this->server->validation_logs;
|
||||||
|
$this->isReachable = $this->server->settings->is_reachable;
|
||||||
|
$this->isUsable = $this->server->settings->is_usable;
|
||||||
|
|
||||||
// Reload Hetzner tokens in case the linking section should now be shown
|
// Reload Hetzner tokens in case the linking section should now be shown
|
||||||
$this->loadHetznerTokens();
|
$this->loadHetznerTokens();
|
||||||
|
|
|
||||||
|
|
@ -60,56 +60,8 @@
|
||||||
helper="A comma separated list of ports you would like to map to the host system.<br><span class='inline-block font-bold dark:text-warning'>Example</span>3000:5432,3002:5433"
|
helper="A comma separated list of ports you would like to map to the host system.<br><span class='inline-block font-bold dark:text-warning'>Example</span>3000:5432,3002:5433"
|
||||||
canGate="update" :canResource="$database" />
|
canGate="update" :canResource="$database" />
|
||||||
</div>
|
</div>
|
||||||
<x-forms.input label="Redis URL (internal)"
|
|
||||||
helper="If you change the user/password/port, this could be different. This is with the default values."
|
|
||||||
type="password" readonly wire:model="dbUrl" canGate="update" :canResource="$database" />
|
|
||||||
@if ($dbUrlPublic)
|
|
||||||
<x-forms.input label="Redis URL (public)"
|
|
||||||
helper="If you change the user/password/port, this could be different. This is with the default values."
|
|
||||||
type="password" readonly wire:model="dbUrlPublic" canGate="update" :canResource="$database" />
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<div class="flex items-center justify-between py-2">
|
|
||||||
<div class="flex items-center justify-between w-full">
|
|
||||||
<h3>SSL Configuration</h3>
|
|
||||||
@if ($enableSsl && $certificateValidUntil)
|
|
||||||
<x-modal-confirmation title="Regenerate SSL Certificates"
|
|
||||||
buttonTitle="Regenerate SSL Certificates" :actions="[
|
|
||||||
'The SSL certificate of this database will be regenerated.',
|
|
||||||
'You must restart the database after regenerating the certificate to start using the new certificate.',
|
|
||||||
]"
|
|
||||||
submitAction="regenerateSslCertificate" :confirmWithText="false" :confirmWithPassword="false" />
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@if ($enableSsl && $certificateValidUntil)
|
|
||||||
<span class="text-sm">Valid until:
|
|
||||||
@if (now()->gt($certificateValidUntil))
|
|
||||||
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expired</span>
|
|
||||||
@elseif(now()->addDays(30)->gt($certificateValidUntil))
|
|
||||||
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expiring
|
|
||||||
soon</span>
|
|
||||||
@else
|
|
||||||
<span>{{ $certificateValidUntil->format('d.m.Y H:i:s') }}</span>
|
|
||||||
@endif
|
|
||||||
</span>
|
|
||||||
@endif
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<div class="w-64" wire:key='enable_ssl'>
|
|
||||||
@if (str($database->status)->contains('exited'))
|
|
||||||
<x-forms.checkbox id="enableSsl" label="Enable SSL"
|
|
||||||
wire:model.live="enableSsl" instantSave="instantSaveSSL" canGate="update"
|
|
||||||
:canResource="$database" />
|
|
||||||
@else
|
|
||||||
<x-forms.checkbox id="enableSsl" label="Enable SSL"
|
|
||||||
wire:model.live="enableSsl" instantSave="instantSaveSSL" disabled
|
|
||||||
helper="Database should be stopped to change this settings." canGate="update"
|
|
||||||
:canResource="$database" />
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<livewire:project.database.redis.status-info :database="$database" />
|
||||||
<div>
|
<div>
|
||||||
<div class="flex flex-col py-2 w-64">
|
<div class="flex flex-col py-2 w-64">
|
||||||
<div class="flex items-center gap-2 pb-2">
|
<div class="flex items-center gap-2 pb-2">
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<x-forms.input label="Redis URL (internal)"
|
||||||
|
helper="If you change the user/password/port, this could be different. This is with the default values."
|
||||||
|
type="password" readonly wire:model="dbUrl" canGate="update" :canResource="$database" />
|
||||||
|
@if ($dbUrlPublic)
|
||||||
|
<x-forms.input label="Redis URL (public)"
|
||||||
|
helper="If you change the user/password/port, this could be different. This is with the default values."
|
||||||
|
type="password" readonly wire:model="dbUrlPublic" canGate="update" :canResource="$database" />
|
||||||
|
@endif
|
||||||
|
<div class="flex flex-col gap-2 pt-4">
|
||||||
|
<div class="flex items-center justify-between py-2">
|
||||||
|
<div class="flex items-center justify-between w-full">
|
||||||
|
<h3>SSL Configuration</h3>
|
||||||
|
@if ($enableSsl && $certificateValidUntil)
|
||||||
|
<x-modal-confirmation title="Regenerate SSL Certificates"
|
||||||
|
buttonTitle="Regenerate SSL Certificates" :actions="[
|
||||||
|
'The SSL certificate of this database will be regenerated.',
|
||||||
|
'You must restart the database after regenerating the certificate to start using the new certificate.',
|
||||||
|
]"
|
||||||
|
submitAction="regenerateSslCertificate" :confirmWithText="false" :confirmWithPassword="false" />
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@if ($enableSsl && $certificateValidUntil)
|
||||||
|
<span class="text-sm">Valid until:
|
||||||
|
@if (now()->gt($certificateValidUntil))
|
||||||
|
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expired</span>
|
||||||
|
@elseif(now()->addDays(30)->gt($certificateValidUntil))
|
||||||
|
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expiring
|
||||||
|
soon</span>
|
||||||
|
@else
|
||||||
|
<span>{{ $certificateValidUntil->format('d.m.Y H:i:s') }}</span>
|
||||||
|
@endif
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="w-64" wire:key='enable_ssl'>
|
||||||
|
@if (str($database->status)->contains('exited'))
|
||||||
|
<x-forms.checkbox id="enableSsl" label="Enable SSL"
|
||||||
|
wire:model.live="enableSsl" instantSave="instantSaveSSL" canGate="update"
|
||||||
|
:canResource="$database" />
|
||||||
|
@else
|
||||||
|
<x-forms.checkbox id="enableSsl" label="Enable SSL"
|
||||||
|
wire:model.live="enableSsl" instantSave="instantSaveSSL" disabled
|
||||||
|
helper="Database should be stopped to change this settings." canGate="update"
|
||||||
|
:canResource="$database" />
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -7,11 +7,16 @@
|
||||||
use App\Livewire\Project\Database\Mysql\General as MysqlGeneral;
|
use App\Livewire\Project\Database\Mysql\General as MysqlGeneral;
|
||||||
use App\Livewire\Project\Database\Postgresql\General as PostgresqlGeneral;
|
use App\Livewire\Project\Database\Postgresql\General as PostgresqlGeneral;
|
||||||
use App\Livewire\Project\Database\Redis\General as RedisGeneral;
|
use App\Livewire\Project\Database\Redis\General as RedisGeneral;
|
||||||
|
use App\Livewire\Project\Database\Redis\StatusInfo as RedisStatusInfo;
|
||||||
|
use App\Livewire\Server\Sentinel;
|
||||||
|
use App\Livewire\Server\Show;
|
||||||
use App\Models\Environment;
|
use App\Models\Environment;
|
||||||
use App\Models\Project;
|
use App\Models\Project;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
use App\Models\StandaloneDocker;
|
use App\Models\StandaloneDocker;
|
||||||
use App\Models\StandaloneMysql;
|
use App\Models\StandaloneMysql;
|
||||||
|
use App\Models\StandalonePostgresql;
|
||||||
|
use App\Models\StandaloneRedis;
|
||||||
use App\Models\Team;
|
use App\Models\Team;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
@ -28,25 +33,75 @@
|
||||||
session(['currentTeam' => $this->team]);
|
session(['currentTeam' => $this->team]);
|
||||||
});
|
});
|
||||||
|
|
||||||
dataset('ssl-aware-database-general-components', [
|
dataset('database-general-forms-without-broadcasts', [
|
||||||
|
// Redis splits status-derived display into a sibling component; the form itself
|
||||||
|
// takes no broadcast listeners. Other DBs use the narrower refreshStatus pattern below.
|
||||||
|
RedisGeneral::class,
|
||||||
|
]);
|
||||||
|
|
||||||
|
dataset('database-general-forms-with-narrow-refresh', [
|
||||||
|
// Form listens to status broadcasts but routes them to refreshStatus, which only
|
||||||
|
// writes display-only properties (URLs, cert expiry) — never input-bound text fields.
|
||||||
|
PostgresqlGeneral::class,
|
||||||
MysqlGeneral::class,
|
MysqlGeneral::class,
|
||||||
MariadbGeneral::class,
|
MariadbGeneral::class,
|
||||||
MongodbGeneral::class,
|
MongodbGeneral::class,
|
||||||
RedisGeneral::class,
|
|
||||||
PostgresqlGeneral::class,
|
|
||||||
KeydbGeneral::class,
|
KeydbGeneral::class,
|
||||||
DragonflyGeneral::class,
|
DragonflyGeneral::class,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
it('maps database status broadcasts to refresh for ssl-aware database general components', function (string $componentClass) {
|
dataset('database-status-info-components', [
|
||||||
$component = app($componentClass);
|
RedisStatusInfo::class,
|
||||||
$listeners = $component->getListeners();
|
]);
|
||||||
|
|
||||||
expect($listeners["echo-private:user.{$this->user->id},DatabaseStatusChanged"])->toBe('refresh')
|
it('does not subscribe the form to status broadcasts when display lives in a sibling', function (string $componentClass) {
|
||||||
->and($listeners["echo-private:team.{$this->team->id},ServiceChecked"])->toBe('refresh');
|
// Regression guard for coolify#6062 / #6354 / #9695:
|
||||||
})->with('ssl-aware-database-general-components');
|
// For DBs whose status-derived display moved into a sibling component, the form
|
||||||
|
// itself must not subscribe to status broadcasts at all.
|
||||||
|
$listeners = resolveLivewireListeners(app($componentClass));
|
||||||
|
|
||||||
it('reloads the mysql database model when refreshing so ssl controls follow the latest status', function () {
|
expect($listeners)
|
||||||
|
->not->toHaveKey("echo-private:user.{$this->user->id},DatabaseStatusChanged")
|
||||||
|
->not->toHaveKey("echo-private:team.{$this->team->id},ServiceChecked")
|
||||||
|
->not->toHaveKey("echo-private:team.{$this->team->id},ServiceStatusChanged");
|
||||||
|
})->with('database-general-forms-without-broadcasts');
|
||||||
|
|
||||||
|
it('routes status broadcasts to refreshStatus, never to a handler that re-syncs inputs', function (string $componentClass) {
|
||||||
|
// Regression guard for coolify#6062 / #6354 / #9695:
|
||||||
|
// The form may listen to broadcasts, but only to a narrow handler (refreshStatus)
|
||||||
|
// that touches display-only properties. Routing to `refresh` or `$refresh` would
|
||||||
|
// re-sync every input property from the DB and wipe in-progress typing.
|
||||||
|
$listeners = resolveLivewireListeners(app($componentClass));
|
||||||
|
|
||||||
|
$databaseStatusKey = "echo-private:user.{$this->user->id},DatabaseStatusChanged";
|
||||||
|
$serviceCheckedKey = "echo-private:team.{$this->team->id},ServiceChecked";
|
||||||
|
|
||||||
|
expect($listeners[$databaseStatusKey] ?? null)->toBe('refreshStatus')
|
||||||
|
->and($listeners[$serviceCheckedKey] ?? null)->toBe('refreshStatus');
|
||||||
|
})->with('database-general-forms-with-narrow-refresh');
|
||||||
|
|
||||||
|
function resolveLivewireListeners(object $component): array
|
||||||
|
{
|
||||||
|
// Livewire's HandlesEvents trait declares getListeners() as protected,
|
||||||
|
// so subclasses that override it as public are callable directly, but
|
||||||
|
// subclasses that rely on $listeners are not. Reflection handles both.
|
||||||
|
$method = new ReflectionMethod($component, 'getListeners');
|
||||||
|
$method->setAccessible(true);
|
||||||
|
|
||||||
|
return (array) $method->invoke($component);
|
||||||
|
}
|
||||||
|
|
||||||
|
it('auto-refreshes status-info sibling on database status broadcasts', function (string $componentClass) {
|
||||||
|
// Status-derived display (connection URLs, SSL gate hint, cert expiry) lives in a sibling
|
||||||
|
// Livewire component so it can re-render on broadcasts without touching the form's DOM.
|
||||||
|
$listeners = resolveLivewireListeners(app($componentClass));
|
||||||
|
|
||||||
|
expect($listeners)
|
||||||
|
->toHaveKey("echo-private:user.{$this->user->id},DatabaseStatusChanged")
|
||||||
|
->toHaveKey("echo-private:team.{$this->team->id},ServiceChecked");
|
||||||
|
})->with('database-status-info-components');
|
||||||
|
|
||||||
|
it('reloads the mysql database model when refresh is called directly so ssl controls follow the latest status', function () {
|
||||||
$server = Server::factory()->create(['team_id' => $this->team->id]);
|
$server = Server::factory()->create(['team_id' => $this->team->id]);
|
||||||
$destination = StandaloneDocker::where('server_id', $server->id)->first();
|
$destination = StandaloneDocker::where('server_id', $server->id)->first();
|
||||||
$project = Project::factory()->create(['team_id' => $this->team->id]);
|
$project = Project::factory()->create(['team_id' => $this->team->id]);
|
||||||
|
|
@ -75,3 +130,91 @@
|
||||||
$component->call('refresh')
|
$component->call('refresh')
|
||||||
->assertSee('Database should be stopped to change this settings.');
|
->assertSee('Database should be stopped to change this settings.');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not clobber server form text inputs when sentinel restarts', function () {
|
||||||
|
$server = Server::factory()->create([
|
||||||
|
'team_id' => $this->team->id,
|
||||||
|
'name' => 'persisted-server-name',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$component = Livewire::test(Sentinel::class, ['server_uuid' => $server->uuid])
|
||||||
|
->set('sentinelToken', 'user-was-typing-this-token');
|
||||||
|
|
||||||
|
$component->call('handleSentinelRestarted', ['serverUuid' => $server->uuid]);
|
||||||
|
|
||||||
|
expect($component->get('sentinelToken'))->toBe('user-was-typing-this-token');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not clobber server form text inputs when server validation completes', function () {
|
||||||
|
$server = Server::factory()->create([
|
||||||
|
'team_id' => $this->team->id,
|
||||||
|
'name' => 'persisted-server-name',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$component = Livewire::test(Show::class, ['server_uuid' => $server->uuid])
|
||||||
|
->set('name', 'user-was-typing-here')
|
||||||
|
->set('ip', '203.0.113.42');
|
||||||
|
|
||||||
|
$component->call('handleServerValidated', ['serverUuid' => $server->uuid]);
|
||||||
|
|
||||||
|
expect($component->get('name'))->toBe('user-was-typing-here')
|
||||||
|
->and($component->get('ip'))->toBe('203.0.113.42');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves typed input on the postgres form when refreshStatus runs', function () {
|
||||||
|
$server = Server::factory()->create(['team_id' => $this->team->id]);
|
||||||
|
$destination = StandaloneDocker::where('server_id', $server->id)->first();
|
||||||
|
$project = Project::factory()->create(['team_id' => $this->team->id]);
|
||||||
|
$environment = Environment::factory()->create(['project_id' => $project->id]);
|
||||||
|
|
||||||
|
$database = StandalonePostgresql::create([
|
||||||
|
'name' => 'persisted-name',
|
||||||
|
'image' => 'postgres:16',
|
||||||
|
'postgres_user' => 'postgres',
|
||||||
|
'postgres_password' => 'password',
|
||||||
|
'postgres_db' => 'postgres',
|
||||||
|
'status' => 'exited:unhealthy',
|
||||||
|
'enable_ssl' => false,
|
||||||
|
'is_log_drain_enabled' => false,
|
||||||
|
'environment_id' => $environment->id,
|
||||||
|
'destination_id' => $destination->id,
|
||||||
|
'destination_type' => $destination->getMorphClass(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$component = Livewire::test(PostgresqlGeneral::class, ['database' => $database])
|
||||||
|
->set('name', 'user-was-typing-here')
|
||||||
|
->set('portsMappings', '5433:5432');
|
||||||
|
|
||||||
|
$component->call('refreshStatus');
|
||||||
|
|
||||||
|
expect($component->get('name'))->toBe('user-was-typing-here')
|
||||||
|
->and($component->get('portsMappings'))->toBe('5433:5432');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows the redis ssl gate hint after the sibling is refreshed', function () {
|
||||||
|
$server = Server::factory()->create(['team_id' => $this->team->id]);
|
||||||
|
$destination = StandaloneDocker::where('server_id', $server->id)->first();
|
||||||
|
$project = Project::factory()->create(['team_id' => $this->team->id]);
|
||||||
|
$environment = Environment::factory()->create(['project_id' => $project->id]);
|
||||||
|
|
||||||
|
$database = StandaloneRedis::create([
|
||||||
|
'name' => 'test-redis',
|
||||||
|
'image' => 'redis:7',
|
||||||
|
'redis_password' => 'password',
|
||||||
|
'redis_username' => 'default',
|
||||||
|
'status' => 'exited:unhealthy',
|
||||||
|
'enable_ssl' => true,
|
||||||
|
'is_log_drain_enabled' => false,
|
||||||
|
'environment_id' => $environment->id,
|
||||||
|
'destination_id' => $destination->id,
|
||||||
|
'destination_type' => $destination->getMorphClass(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$component = Livewire::test(RedisStatusInfo::class, ['database' => $database])
|
||||||
|
->assertDontSee('Database should be stopped to change this settings.');
|
||||||
|
|
||||||
|
$database->fill(['status' => 'running:healthy'])->save();
|
||||||
|
|
||||||
|
$component->call('refresh')
|
||||||
|
->assertSee('Database should be stopped to change this settings.');
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue