Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
f77ad4c
Complete Livewire legacy model binding migration (25+ components)
andrasbacsai Oct 13, 2025
53b605c
Disable legacy_model_binding flag in Livewire config
andrasbacsai Oct 13, 2025
043b144
Merge branch 'next' into andrasbacsai/livewire-model-binding
andrasbacsai Oct 14, 2025
56481b3
Remove migration report for Livewire legacy model binding as all comp…
andrasbacsai Oct 14, 2025
a514c83
Fix duplicate HTML ID warnings in form components
andrasbacsai Oct 14, 2025
598984f
Fix wire:model warnings and ensure truly unique HTML IDs
andrasbacsai Oct 14, 2025
ff71b28
Fix Monaco editor @entangle error with unique HTML IDs
andrasbacsai Oct 14, 2025
837a0f4
Merge branch 'next' into andrasbacsai/livewire-model-binding
andrasbacsai Oct 16, 2025
a5c6f53
Fix wire:dirty indicator appearing on readonly fields without wire:mo…
andrasbacsai Oct 16, 2025
d2a334d
refactor: replace random ID generation with Cuid2 for unique HTML IDs…
andrasbacsai Oct 16, 2025
db3514c
Fix json_decode null handling in PreviewsCompose
andrasbacsai Oct 16, 2025
6e8c557
fix: ensure authorization checks are in place for viewing and updatin…
andrasbacsai Oct 16, 2025
cdf6b5f
Fix preview domain generation for services with multiple domains
andrasbacsai Oct 16, 2025
d4fb69e
fix: ensure authorization check is performed during component mount
andrasbacsai Oct 16, 2025
543d6fb
Merge branch 'next' into andrasbacsai/livewire-model-binding
andrasbacsai Oct 16, 2025
e2c254a
Changes auto-committed by Conductor
andrasbacsai Oct 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
443 changes: 323 additions & 120 deletions app/Livewire/Project/Application/General.php

Large diffs are not rendered by default.

91 changes: 66 additions & 25 deletions app/Livewire/Project/Application/Previews.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,34 @@ class Previews extends Component

public $pendingPreviewId = null;

public array $previewFqdns = [];

protected $rules = [
'application.previews.*.fqdn' => 'string|nullable',
'previewFqdns.*' => 'string|nullable',
];

public function mount()
{
$this->pull_requests = collect();
$this->parameters = get_route_parameters();
$this->syncData(false);
}

private function syncData(bool $toModel = false): void
{
if ($toModel) {
foreach ($this->previewFqdns as $key => $fqdn) {
$preview = $this->application->previews->get($key);
if ($preview) {
$preview->fqdn = $fqdn;
}
}
} else {
$this->previewFqdns = [];
foreach ($this->application->previews as $key => $preview) {
$this->previewFqdns[$key] = $preview->fqdn;
}
}
}

public function load_prs()
Expand Down Expand Up @@ -73,35 +93,52 @@ public function save_preview($preview_id)
$this->authorize('update', $this->application);
$success = true;
$preview = $this->application->previews->find($preview_id);
if (data_get_str($preview, 'fqdn')->isNotEmpty()) {
$preview->fqdn = str($preview->fqdn)->replaceEnd(',', '')->trim();
$preview->fqdn = str($preview->fqdn)->replaceStart(',', '')->trim();
$preview->fqdn = str($preview->fqdn)->trim()->lower();
if (! validateDNSEntry($preview->fqdn, $this->application->destination->server)) {
$this->dispatch('error', 'Validating DNS failed.', "Make sure you have added the DNS records correctly.<br><br>$preview->fqdn->{$this->application->destination->server->ip}<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help.");
$success = false;
}
// Check for domain conflicts if not forcing save
if (! $this->forceSaveDomains) {
$result = checkDomainUsage(resource: $this->application, domain: $preview->fqdn);
if ($result['hasConflicts']) {
$this->domainConflicts = $result['conflicts'];
$this->showDomainConflictModal = true;
$this->pendingPreviewId = $preview_id;

return;

if (! $preview) {
throw new \Exception('Preview not found');
}

// Find the key for this preview in the collection
$previewKey = $this->application->previews->search(function ($item) use ($preview_id) {
return $item->id == $preview_id;
});

if ($previewKey !== false && isset($this->previewFqdns[$previewKey])) {
$fqdn = $this->previewFqdns[$previewKey];

if (! empty($fqdn)) {
$fqdn = str($fqdn)->replaceEnd(',', '')->trim();
$fqdn = str($fqdn)->replaceStart(',', '')->trim();
$fqdn = str($fqdn)->trim()->lower();
$this->previewFqdns[$previewKey] = $fqdn;

if (! validateDNSEntry($fqdn, $this->application->destination->server)) {
$this->dispatch('error', 'Validating DNS failed.', "Make sure you have added the DNS records correctly.<br><br>$fqdn->{$this->application->destination->server->ip}<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help.");
$success = false;
}

// Check for domain conflicts if not forcing save
if (! $this->forceSaveDomains) {
$result = checkDomainUsage(resource: $this->application, domain: $fqdn);
if ($result['hasConflicts']) {
$this->domainConflicts = $result['conflicts'];
$this->showDomainConflictModal = true;
$this->pendingPreviewId = $preview_id;

return;
}
} else {
// Reset the force flag after using it
$this->forceSaveDomains = false;
}
} else {
// Reset the force flag after using it
$this->forceSaveDomains = false;
}
}

if (! $preview) {
throw new \Exception('Preview not found');
if ($success) {
$this->syncData(true);
$preview->save();
$this->dispatch('success', 'Preview saved.<br><br>Do not forget to redeploy the preview to apply the changes.');
}
$success && $preview->save();
$success && $this->dispatch('success', 'Preview saved.<br><br>Do not forget to redeploy the preview to apply the changes.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
Expand All @@ -121,13 +158,15 @@ public function generate_preview($preview_id)
if ($this->application->build_pack === 'dockercompose') {
$preview->generate_preview_fqdn_compose();
$this->application->refresh();
$this->syncData(false);
$this->dispatch('success', 'Domain generated.');

return;
}

$preview->generate_preview_fqdn();
$this->application->refresh();
$this->syncData(false);
$this->dispatch('update_links');
$this->dispatch('success', 'Domain generated.');
} catch (\Throwable $e) {
Expand All @@ -152,6 +191,7 @@ public function add(int $pull_request_id, ?string $pull_request_html_url = null)
}
$found->generate_preview_fqdn_compose();
$this->application->refresh();
$this->syncData(false);
} else {
$this->setDeploymentUuid();
$found = ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first();
Expand All @@ -164,6 +204,7 @@ public function add(int $pull_request_id, ?string $pull_request_html_url = null)
}
$found->generate_preview_fqdn();
$this->application->refresh();
$this->syncData(false);
$this->dispatch('update_links');
$this->dispatch('success', 'Preview added.');
}
Expand Down
13 changes: 10 additions & 3 deletions app/Livewire/Project/Application/PreviewsCompose.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ class PreviewsCompose extends Component

public ApplicationPreview $preview;

public ?string $domain = null;

public function mount()
{
$this->domain = data_get($this->service, 'domain');
}

public function render()
{
return view('livewire.project.application.previews-compose');
Expand All @@ -28,10 +35,9 @@ public function save()
try {
$this->authorize('update', $this->preview->application);

$domain = data_get($this->service, 'domain');
$docker_compose_domains = data_get($this->preview, 'docker_compose_domains');
$docker_compose_domains = json_decode($docker_compose_domains, true);
$docker_compose_domains[$this->serviceName]['domain'] = $domain;
$docker_compose_domains[$this->serviceName]['domain'] = $this->domain;
$this->preview->docker_compose_domains = json_encode($docker_compose_domains);
$this->preview->save();
$this->dispatch('update_links');
Expand Down Expand Up @@ -83,9 +89,10 @@ public function generate()
}

// Save the generated domain
$this->domain = $preview_fqdn;
$docker_compose_domains = data_get($this->preview, 'docker_compose_domains');
$docker_compose_domains = json_decode($docker_compose_domains, true);
$docker_compose_domains[$this->serviceName]['domain'] = $this->service->domain = $preview_fqdn;
$docker_compose_domains[$this->serviceName]['domain'] = $this->domain;
$this->preview->docker_compose_domains = json_encode($docker_compose_domains);
$this->preview->save();

Expand Down
61 changes: 51 additions & 10 deletions app/Livewire/Project/Service/Database.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,30 @@ class Database extends Component

public $parameters;

public ?string $humanName = null;

public ?string $description = null;

public ?string $image = null;

public bool $excludeFromStatus = false;

public ?int $publicPort = null;

public bool $isPublic = false;

public bool $isLogDrainEnabled = false;

protected $listeners = ['refreshFileStorages'];

protected $rules = [
'database.human_name' => 'nullable',
'database.description' => 'nullable',
'database.image' => 'required',
'database.exclude_from_status' => 'required|boolean',
'database.public_port' => 'nullable|integer',
'database.is_public' => 'required|boolean',
'database.is_log_drain_enabled' => 'required|boolean',
'humanName' => 'nullable',
'description' => 'nullable',
'image' => 'required',
'excludeFromStatus' => 'required|boolean',
'publicPort' => 'nullable|integer',
'isPublic' => 'required|boolean',
'isLogDrainEnabled' => 'required|boolean',
];

public function render()
Expand All @@ -50,11 +64,33 @@ public function mount()
$this->db_url_public = $this->database->getServiceDatabaseUrl();
}
$this->refreshFileStorages();
$this->syncData(false);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}

private function syncData(bool $toModel = false): void
{
if ($toModel) {
$this->database->human_name = $this->humanName;
$this->database->description = $this->description;
$this->database->image = $this->image;
$this->database->exclude_from_status = $this->excludeFromStatus;
$this->database->public_port = $this->publicPort;
$this->database->is_public = $this->isPublic;
$this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
} else {
$this->humanName = $this->database->human_name;
$this->description = $this->database->description;
$this->image = $this->database->image;
$this->excludeFromStatus = $this->database->exclude_from_status ?? false;
$this->publicPort = $this->database->public_port;
$this->isPublic = $this->database->is_public ?? false;
$this->isLogDrainEnabled = $this->database->is_log_drain_enabled ?? false;
}
}

public function delete($password)
{
try {
Expand Down Expand Up @@ -92,7 +128,7 @@ public function instantSaveLogDrain()
try {
$this->authorize('update', $this->database);
if (! $this->database->service->destination->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->isLogDrainEnabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');

return;
Expand Down Expand Up @@ -145,15 +181,17 @@ public function instantSave()
{
try {
$this->authorize('update', $this->database);
if ($this->database->is_public && ! $this->database->public_port) {
if ($this->isPublic && ! $this->publicPort) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
$this->isPublic = false;

return;
}
$this->syncData(true);
if ($this->database->is_public) {
if (! str($this->database->status)->startsWith('running')) {
$this->dispatch('error', 'Database must be started to be publicly accessible.');
$this->isPublic = false;
$this->database->is_public = false;

return;
Expand Down Expand Up @@ -182,7 +220,10 @@ public function submit()
try {
$this->authorize('update', $this->database);
$this->validate();
$this->syncData(true);
$this->database->save();
$this->database->refresh();
$this->syncData(false);
updateCompose($this->database);
$this->dispatch('success', 'Database saved.');
} catch (\Throwable $e) {
Expand Down
38 changes: 30 additions & 8 deletions app/Livewire/Project/Service/EditCompose.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,37 +11,58 @@ class EditCompose extends Component

public $serviceId;

public ?string $dockerComposeRaw = null;

public ?string $dockerCompose = null;

public bool $isContainerLabelEscapeEnabled = false;

protected $listeners = [
'refreshEnvs',
'envsUpdated',
'refresh' => 'envsUpdated',
];

protected $rules = [
'service.docker_compose_raw' => 'required',
'service.docker_compose' => 'required',
'service.is_container_label_escape_enabled' => 'required',
'dockerComposeRaw' => 'required',
'dockerCompose' => 'required',
'isContainerLabelEscapeEnabled' => 'required',
];

public function envsUpdated()
{
$this->dispatch('saveCompose', $this->service->docker_compose_raw);
$this->dispatch('saveCompose', $this->dockerComposeRaw);
$this->refreshEnvs();
}

public function refreshEnvs()
{
$this->service = Service::ownedByCurrentTeam()->find($this->serviceId);
$this->syncData(false);
}

public function mount()
{
$this->service = Service::ownedByCurrentTeam()->find($this->serviceId);
$this->syncData(false);
}

private function syncData(bool $toModel = false): void
{
if ($toModel) {
$this->service->docker_compose_raw = $this->dockerComposeRaw;
$this->service->docker_compose = $this->dockerCompose;
$this->service->is_container_label_escape_enabled = $this->isContainerLabelEscapeEnabled;
} else {
$this->dockerComposeRaw = $this->service->docker_compose_raw;
$this->dockerCompose = $this->service->docker_compose;
$this->isContainerLabelEscapeEnabled = $this->service->is_container_label_escape_enabled ?? false;
}
}

public function validateCompose()
{
$isValid = validateComposeFile($this->service->docker_compose_raw, $this->service->server_id);
$isValid = validateComposeFile($this->dockerComposeRaw, $this->service->server_id);
if ($isValid !== 'OK') {
$this->dispatch('error', "Invalid docker-compose file.\n$isValid");
} else {
Expand All @@ -52,16 +73,17 @@ public function validateCompose()
public function saveEditedCompose()
{
$this->dispatch('info', 'Saving new docker compose...');
$this->dispatch('saveCompose', $this->service->docker_compose_raw);
$this->dispatch('saveCompose', $this->dockerComposeRaw);
$this->dispatch('refreshStorages');
}

public function instantSave()
{
$this->validate([
'service.is_container_label_escape_enabled' => 'required',
'isContainerLabelEscapeEnabled' => 'required',
]);
$this->service->save(['is_container_label_escape_enabled' => $this->service->is_container_label_escape_enabled]);
$this->syncData(true);
$this->service->save(['is_container_label_escape_enabled' => $this->isContainerLabelEscapeEnabled]);
$this->dispatch('success', 'Service updated successfully');
}

Expand Down
Loading