Files
coolify/app/Livewire/Project/Database/Mysql/General.php
T
Aditya Tripathi e7e65831a7 fix(livewire): preserve wire:dirty across DB status broadcasts
The earlier refreshStatus fix kept user-typed values intact but Livewire still
absorbed deferred wire:model values into the snapshot on every broadcast-
triggered roundtrip, clearing the unsaved-changes indicator and making the form
look auto-saved. Move all status-derived display (DB URLs, SSL toggle/mode,
cert expiry) out of each DB General form into a sibling StatusInfo Livewire
component, so the form never roundtrips on broadcasts.

Shared scaffolding lives in App\Traits\HasDatabaseStatusInfo plus an x-database-
status-info Blade component, leaving each per-DB StatusInfo class as a ~20-50
line declaration of label, SSL mode options, and SSL save hooks. Parents
dispatch databaseUpdated from save methods so the sibling refreshes after writes.

Tests cover the architecture (no DB form subscribes to status broadcasts) and
the sibling's refresh-on-status-change behavior.
2026-05-21 08:31:08 +00:00

257 lines
9.4 KiB
PHP

<?php
namespace App\Livewire\Project\Database\Mysql;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
use App\Models\Server;
use App\Models\StandaloneMysql;
use App\Support\ValidationPatterns;
use Exception;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
class General extends Component
{
use AuthorizesRequests;
public StandaloneMysql $database;
public ?Server $server = null;
public string $name;
public ?string $description = null;
public string $mysqlRootPassword;
public string $mysqlUser;
public string $mysqlPassword;
public string $mysqlDatabase;
public ?string $mysqlConf = null;
public string $image;
public ?string $portsMappings = null;
public ?bool $isPublic = null;
public mixed $publicPort = null;
public mixed $publicPortTimeout = 3600;
public bool $isLogDrainEnabled = false;
public ?string $customDockerRunOptions = null;
protected function rules(): array
{
return [
'name' => ValidationPatterns::nameRules(),
'description' => ValidationPatterns::descriptionRules(),
'mysqlRootPassword' => ValidationPatterns::databasePasswordRules(
enforcePattern: $this->mysqlRootPassword !== $this->database->mysql_root_password,
),
'mysqlUser' => ValidationPatterns::databaseIdentifierRules(
enforcePattern: $this->mysqlUser !== $this->database->mysql_user,
),
'mysqlPassword' => ValidationPatterns::databasePasswordRules(
enforcePattern: $this->mysqlPassword !== $this->database->mysql_password,
),
'mysqlDatabase' => ValidationPatterns::databaseIdentifierRules(
enforcePattern: $this->mysqlDatabase !== $this->database->mysql_database,
),
'mysqlConf' => 'nullable',
'image' => 'required',
'portsMappings' => ValidationPatterns::portMappingRules(),
'isPublic' => 'nullable|boolean',
'publicPort' => 'nullable|integer|min:1|max:65535',
'publicPortTimeout' => 'nullable|integer|min:1',
'isLogDrainEnabled' => 'nullable|boolean',
'customDockerRunOptions' => 'nullable',
];
}
protected function messages(): array
{
return array_merge(
ValidationPatterns::combinedMessages(),
ValidationPatterns::portMappingMessages(),
[
'name.required' => 'The Name field is required.',
...ValidationPatterns::databasePasswordMessages('mysqlRootPassword', 'Root Password'),
...ValidationPatterns::databaseIdentifierMessages('mysqlUser', 'MySQL User'),
...ValidationPatterns::databasePasswordMessages('mysqlPassword', 'MySQL Password'),
...ValidationPatterns::databaseIdentifierMessages('mysqlDatabase', 'MySQL Database'),
'image.required' => 'The Docker Image field is required.',
'publicPort.integer' => 'The Public Port must be an integer.',
'publicPort.min' => 'The Public Port must be at least 1.',
'publicPort.max' => 'The Public Port must not exceed 65535.',
'publicPortTimeout.integer' => 'The Public Port Timeout must be an integer.',
'publicPortTimeout.min' => 'The Public Port Timeout must be at least 1.',
]
);
}
protected $validationAttributes = [
'name' => 'Name',
'description' => 'Description',
'mysqlRootPassword' => 'Root Password',
'mysqlUser' => 'User',
'mysqlPassword' => 'Password',
'mysqlDatabase' => 'Database',
'mysqlConf' => 'MySQL Configuration',
'image' => 'Image',
'portsMappings' => 'Port Mapping',
'isPublic' => 'Is Public',
'publicPort' => 'Public Port',
'publicPortTimeout' => 'Public Port Timeout',
'customDockerRunOptions' => 'Custom Docker Run Options',
];
public function mount()
{
try {
$this->authorize('view', $this->database);
$this->syncData();
$this->server = data_get($this->database, 'destination.server');
if (! $this->server) {
$this->dispatch('error', 'Database destination server is not configured.');
return;
}
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function syncData(bool $toModel = false)
{
if ($toModel) {
$this->validate();
$this->database->name = $this->name;
$this->database->description = $this->description;
$this->database->mysql_root_password = $this->mysqlRootPassword;
$this->database->mysql_user = $this->mysqlUser;
$this->database->mysql_password = $this->mysqlPassword;
$this->database->mysql_database = $this->mysqlDatabase;
$this->database->mysql_conf = $this->mysqlConf;
$this->database->image = $this->image;
$this->database->ports_mappings = $this->portsMappings;
$this->database->is_public = $this->isPublic;
$this->database->public_port = $this->publicPort ?: null;
$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->save();
} else {
$this->name = $this->database->name;
$this->description = $this->database->description;
$this->mysqlRootPassword = $this->database->mysql_root_password;
$this->mysqlUser = $this->database->mysql_user;
$this->mysqlPassword = $this->database->mysql_password;
$this->mysqlDatabase = $this->database->mysql_database;
$this->mysqlConf = $this->database->mysql_conf;
$this->image = $this->database->image;
$this->portsMappings = $this->database->ports_mappings;
$this->isPublic = $this->database->is_public;
$this->publicPort = $this->database->public_port;
$this->publicPortTimeout = $this->database->public_port_timeout;
$this->isLogDrainEnabled = $this->database->is_log_drain_enabled;
$this->customDockerRunOptions = $this->database->custom_docker_run_options;
}
}
public function instantSaveAdvanced()
{
try {
$this->authorize('update', $this->database);
if (! $this->server->isLogDrainEnabled()) {
$this->isLogDrainEnabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
return;
}
$this->syncData(true);
$this->dispatch('success', 'Database updated.');
$this->dispatch('success', 'You need to restart the service for the changes to take effect.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function submit()
{
try {
$this->authorize('update', $this->database);
if ($this->portsMappings) {
$this->portsMappings = str($this->portsMappings)->replace(' ', '')->trim()->toString();
}
if (str($this->publicPort)->isEmpty()) {
$this->publicPort = null;
}
$this->syncData(true);
$this->dispatch('success', 'Database updated.');
$this->dispatch('databaseUpdated');
} catch (Exception $e) {
return handleError($e, $this);
} finally {
if (is_null($this->database->config_hash)) {
$this->database->isConfigurationChanged(true);
} else {
$this->dispatch('configurationChanged');
}
}
}
public function instantSave()
{
try {
$this->authorize('update', $this->database);
if ($this->isPublic && ! $this->publicPort) {
$this->dispatch('error', 'Public port is required.');
$this->isPublic = false;
return;
}
if ($this->isPublic && ! str($this->database->status)->startsWith('running')) {
$this->dispatch('error', 'Database must be started to be publicly accessible.');
$this->isPublic = false;
return;
}
$this->syncData(true);
if ($this->isPublic) {
StartDatabaseProxy::run($this->database);
$this->dispatch('success', 'Database is now publicly accessible.');
} else {
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);
return handleError($e, $this);
}
}
public function refresh(): void
{
$this->database->refresh();
$this->syncData();
}
public function render()
{
return view('livewire.project.database.mysql.general');
}
}