mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-14 08:18:08 +00:00
fix(domains): preserve ports in split URL inputs (#11328)
This commit is contained in:
1 parent
5872cc095c
commit
282227b238
17 files changed
+182
-161
No files matched your search
@@ -1,68 +1,27 @@
|
||||
@props([
|
||||
'id',
|
||||
'wire' => true,
|
||||
'value' => '',
|
||||
'errorId' => null,
|
||||
'hostLabel' => 'Domain',
|
||||
'hostPlaceholder' => 'app.example.com',
|
||||
])
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-[8rem_minmax(0,1fr)_8rem]" x-data="{
|
||||
value: @if ($wire) @entangle($id) @else @js($value) @endif,
|
||||
scheme: 'https',
|
||||
host: '',
|
||||
port: '',
|
||||
path: '',
|
||||
syncing: false,
|
||||
init() {
|
||||
this.read(this.value);
|
||||
this.$watch('value', value => {
|
||||
if (!this.syncing) this.read(value);
|
||||
});
|
||||
['scheme', 'host', 'port', 'path'].forEach(part => this.$watch(part, () => this.write()));
|
||||
},
|
||||
read(value) {
|
||||
if (!value) return;
|
||||
try {
|
||||
const url = new URL(value);
|
||||
const authority = value.match(/^[a-z][a-z0-9+.-]*:\/\/(?:\[[^\]]+\]|[^\/:?#]+)(?::(\d+))?/i);
|
||||
this.syncing = true;
|
||||
this.scheme = url.protocol.replace(':', '') === 'http' ? 'http' : 'https';
|
||||
this.host = url.hostname;
|
||||
this.port = authority?.[1] || url.port;
|
||||
this.path = `${url.pathname === '/' ? '' : url.pathname}${url.search}${url.hash}`;
|
||||
this.$nextTick(() => this.syncing = false);
|
||||
} catch (_) {}
|
||||
},
|
||||
write() {
|
||||
if (this.syncing) return;
|
||||
const path = this.path.trim();
|
||||
const normalizedPath = path && !['/', '?', '#'].includes(path[0]) ? `/${path}` : path;
|
||||
const next = `${this.scheme}://${this.host.trim()}${this.port ? `:${this.port}` : ''}${normalizedPath}`;
|
||||
if (this.value !== next) {
|
||||
this.syncing = true;
|
||||
this.value = next;
|
||||
this.$nextTick(() => this.syncing = false);
|
||||
}
|
||||
},
|
||||
}" x-modelable="value" {{ $attributes->whereStartsWith('x-model') }}>
|
||||
<div class="grid gap-4 sm:grid-cols-[8rem_minmax(0,1fr)_8rem]">
|
||||
<div class="min-w-0">
|
||||
<x-forms.listbox id="{{ $id }}-protocol" label="Protocol" :wire="false" value="https"
|
||||
x-model="scheme" portal :options="[
|
||||
['value' => 'https', 'label' => 'https'],
|
||||
['value' => 'http', 'label' => 'http'],
|
||||
]" />
|
||||
<x-forms.listbox id="{{ $id }}.scheme" htmlId="{{ $id }}-protocol" label="Protocol" portal :options="[
|
||||
['value' => 'https', 'label' => 'https'],
|
||||
['value' => 'http', 'label' => 'http'],
|
||||
]" />
|
||||
</div>
|
||||
|
||||
<div class="min-w-0">
|
||||
<div class="mb-1.5 flex h-4 w-full items-center gap-1.5">
|
||||
<label for="{{ $id }}" class="mb-0! flex items-center gap-1.5 leading-4">
|
||||
<label for="{{ $id }}-host" class="mb-0! flex items-center gap-1.5 leading-4">
|
||||
{{ $hostLabel }} <x-highlighted text="*" />
|
||||
</label>
|
||||
</div>
|
||||
<input id="{{ $id }}" type="text" class="input" x-model="host" placeholder="{{ $hostPlaceholder }}"
|
||||
autocomplete="off" required />
|
||||
@error($errorId ?? $id)
|
||||
<input id="{{ $id }}-host" type="text" class="input" wire:model="{{ $id }}.host"
|
||||
placeholder="{{ $hostPlaceholder }}" autocomplete="off" required />
|
||||
@error($errorId ?? "{$id}.host")
|
||||
@php
|
||||
preg_match('/(https?:\/\/\S+)$/', $message, $validationLinkMatches);
|
||||
$validationLink = $validationLinkMatches[1] ?? null;
|
||||
@@ -82,16 +41,16 @@
|
||||
<div class="mb-1.5 flex h-4 w-full items-center gap-1.5">
|
||||
<label for="{{ $id }}-port" class="mb-0! flex items-center gap-1.5 leading-4">Port</label>
|
||||
</div>
|
||||
<input id="{{ $id }}-port" type="number" class="input" x-model="port" placeholder="3000"
|
||||
min="1" max="65535" inputmode="numeric" />
|
||||
<input id="{{ $id }}-port" type="number" class="input" wire:model="{{ $id }}.port"
|
||||
placeholder="3000" min="1" max="65535" inputmode="numeric" />
|
||||
</div>
|
||||
|
||||
<div class="min-w-0 sm:col-span-3">
|
||||
<div class="mb-1.5 flex h-4 w-full items-center gap-1.5">
|
||||
<label for="{{ $id }}-path" class="mb-0! flex items-center gap-1.5 leading-4">Path</label>
|
||||
</div>
|
||||
<input id="{{ $id }}-path" type="text" class="input" x-model="path" placeholder="/api/v3"
|
||||
autocomplete="off" />
|
||||
<input id="{{ $id }}-path" type="text" class="input" wire:model="{{ $id }}.path"
|
||||
placeholder="/api/v3" autocomplete="off" />
|
||||
<p class="mt-1 text-[12px] text-neutral-500 dark:text-fg-dim">
|
||||
Optional path, query, or fragment appended after the domain and port.
|
||||
</p>
|
||||
|
||||
@@ -90,6 +90,9 @@
|
||||
const gap = 4;
|
||||
const edge = 12;
|
||||
const triggerRect = trigger.getBoundingClientRect();
|
||||
panel.style.width = 'max-content';
|
||||
panel.style.minWidth = `${triggerRect.width}px`;
|
||||
panel.style.maxWidth = `${window.innerWidth - (edge * 2)}px`;
|
||||
const panelWidth = Math.min(
|
||||
Math.max(triggerRect.width, panel.offsetWidth),
|
||||
window.innerWidth - (edge * 2),
|
||||
@@ -107,8 +110,6 @@
|
||||
panel.style.top = `${top}px`;
|
||||
panel.style.left = `${left}px`;
|
||||
panel.style.width = `${panelWidth}px`;
|
||||
panel.style.maxWidth = `${window.innerWidth - (edge * 2)}px`;
|
||||
panel.style.minWidth = `${triggerRect.width}px`;
|
||||
this.positioned = true;
|
||||
},
|
||||
}" x-modelable="value" :class="{ 'pointer-events-none opacity-70': saving }"
|
||||
|
||||
@@ -15,30 +15,14 @@
|
||||
domainSearch: '',
|
||||
modalOpen: @js($showEditDomainModal || $editDomainDnsFailed),
|
||||
editingServiceLabel: @js($editingService ?? ''),
|
||||
localEditingIndex: @js($editingIndex),
|
||||
localEditingDomain: @js($editingDomain),
|
||||
localEditingService: @js($editingService),
|
||||
openEditDomain(index, url, service) {
|
||||
this.localEditingIndex = index;
|
||||
this.localEditingDomain = url;
|
||||
this.localEditingService = service;
|
||||
this.editingServiceLabel = service || '';
|
||||
openEditDomain() {
|
||||
this.editingServiceLabel = $wire.editingService || '';
|
||||
this.modalOpen = true;
|
||||
this.$nextTick(() => document.getElementById('editingDomainLocal')?.focus?.());
|
||||
},
|
||||
closeEditDomain() {
|
||||
this.modalOpen = false;
|
||||
this.editingServiceLabel = '';
|
||||
this.localEditingIndex = null;
|
||||
this.localEditingDomain = '';
|
||||
this.localEditingService = null;
|
||||
},
|
||||
prepareEditSubmit() {
|
||||
// Sync Alpine → Livewire only when the user actually saves (one request).
|
||||
$wire.editingIndex = this.localEditingIndex;
|
||||
$wire.editingDomain = this.localEditingDomain;
|
||||
$wire.editingService = this.localEditingService;
|
||||
$wire.showEditDomainModal = true;
|
||||
},
|
||||
matchesDomainSearch(value) {
|
||||
return !this.domainSearch.trim() || value.toLowerCase().includes(this.domainSearch.trim().toLowerCase());
|
||||
@@ -47,7 +31,7 @@
|
||||
return values.some((value) => this.matchesDomainSearch(value));
|
||||
},
|
||||
}"
|
||||
@open-edit-domain.window="openEditDomain($event.detail.index, $event.detail.url, $event.detail.service)"
|
||||
@open-edit-domain.window="openEditDomain()"
|
||||
@edit-domain-saved.window="closeEditDomain()">
|
||||
<x-application.settings-section id="domains-section" title="Domains">
|
||||
@can('update', $application)
|
||||
@@ -128,7 +112,7 @@
|
||||
:disabled="! auth()->user()->can('update', $application)" />
|
||||
@endif
|
||||
|
||||
<x-forms.domain-input id="newDomain" />
|
||||
<x-forms.domain-input id="newDomainParts" errorId="newDomain" />
|
||||
|
||||
@if ($addDomainDnsFailed)
|
||||
<x-callout type="danger" title="DNS is not pointing to the right IP">
|
||||
@@ -320,7 +304,7 @@
|
||||
</header>
|
||||
<div class="application-settings-section-body relative min-h-0 flex-1 overflow-y-auto"
|
||||
style="-webkit-overflow-scrolling: touch;">
|
||||
<form @submit.prevent="prepareEditSubmit(); $wire.updateDomain()" class="flex flex-col gap-4">
|
||||
<form wire:submit="updateDomain" class="flex flex-col gap-4">
|
||||
<div x-show="editingServiceLabel" x-cloak class="w-full">
|
||||
<div class="mb-1.5 flex h-4 w-full items-center gap-1.5">
|
||||
<label class="mb-0! flex items-center gap-1 text-sm font-medium leading-4">Service</label>
|
||||
@@ -328,8 +312,7 @@
|
||||
<input type="text" class="input" readonly x-bind:value="editingServiceLabel" />
|
||||
</div>
|
||||
|
||||
<x-forms.domain-input id="editingDomainLocal" errorId="editingDomain" :wire="false"
|
||||
x-model="localEditingDomain" />
|
||||
<x-forms.domain-input id="editingDomainParts" errorId="editingDomain" />
|
||||
|
||||
@if ($editDomainDnsFailed)
|
||||
<x-callout type="danger" title="DNS is not pointing to the right IP">
|
||||
@@ -345,7 +328,7 @@
|
||||
<div class="flex flex-wrap items-center justify-end gap-2 pt-2">
|
||||
@if ($editDomainDnsFailed)
|
||||
<x-forms.button type="button" isError
|
||||
@click="prepareEditSubmit(); $wire.forceSaveEditDns = true; $wire.confirmUpdateDomainDespiteDns()">
|
||||
wire:click="confirmUpdateDomainDespiteDns">
|
||||
Continue
|
||||
</x-forms.button>
|
||||
@else
|
||||
|
||||
@@ -178,12 +178,7 @@
|
||||
</x-forms.button>
|
||||
@endif
|
||||
@else
|
||||
<button type="button"
|
||||
@click="$dispatch('open-edit-domain', {
|
||||
index: {{ $index }},
|
||||
url: @js($row['url']),
|
||||
service: @js($row['service'] ?? null),
|
||||
})"
|
||||
<button type="button" wire:click="startEdit({{ $index }})"
|
||||
class="icon-button shrink-0"
|
||||
title="Edit domain" aria-label="Edit domain">
|
||||
<x-reicon name="settings" class="size-3.5" />
|
||||
|
||||
@@ -19,29 +19,14 @@
|
||||
domainSearch: '',
|
||||
modalOpen: @js($showEditDomainModal || $editDomainDnsFailed),
|
||||
editingServiceLabel: '',
|
||||
localEditingIndex: @js($editingIndex),
|
||||
localEditingDomain: @js($editingDomain),
|
||||
localEditingServiceApplicationId: @js($editingServiceApplicationId),
|
||||
openEditDomain(index, url, serviceApplicationId, serviceLabel) {
|
||||
this.localEditingIndex = index;
|
||||
this.localEditingDomain = url;
|
||||
this.localEditingServiceApplicationId = serviceApplicationId;
|
||||
this.editingServiceLabel = serviceLabel || '';
|
||||
openEditDomain() {
|
||||
this.editingServiceLabel = $wire.serviceApps.find(app => app.id === $wire.editingServiceApplicationId)?.name || '';
|
||||
this.modalOpen = true;
|
||||
this.$nextTick(() => document.getElementById('editingDomainLocal')?.focus?.());
|
||||
},
|
||||
closeEditDomain() {
|
||||
this.modalOpen = false;
|
||||
this.editingServiceLabel = '';
|
||||
this.localEditingIndex = null;
|
||||
this.localEditingDomain = '';
|
||||
this.localEditingServiceApplicationId = null;
|
||||
},
|
||||
prepareEditSubmit() {
|
||||
$wire.editingIndex = this.localEditingIndex;
|
||||
$wire.editingDomain = this.localEditingDomain;
|
||||
$wire.editingServiceApplicationId = this.localEditingServiceApplicationId;
|
||||
$wire.showEditDomainModal = true;
|
||||
},
|
||||
matchesDomainSearch(value) {
|
||||
return !this.domainSearch.trim() || value.toLowerCase().includes(this.domainSearch.trim().toLowerCase());
|
||||
@@ -50,7 +35,7 @@
|
||||
return values.some((value) => this.matchesDomainSearch(value));
|
||||
},
|
||||
}"
|
||||
@open-edit-domain.window="openEditDomain($event.detail.index, $event.detail.url, $event.detail.serviceApplicationId, $event.detail.serviceLabel)"
|
||||
@open-edit-domain.window="openEditDomain()"
|
||||
@edit-domain-saved.window="closeEditDomain()">
|
||||
<x-application.settings-section id="service-domains-section" title="Domains">
|
||||
@can('update', $service)
|
||||
@@ -116,7 +101,7 @@
|
||||
])->values()->all()"
|
||||
:disabled="! auth()->user()->can('update', $service)" />
|
||||
|
||||
<x-forms.domain-input id="newDomain" />
|
||||
<x-forms.domain-input id="newDomainParts" errorId="newDomain" />
|
||||
|
||||
@if ($addDomainDnsFailed)
|
||||
<x-callout type="danger" title="DNS is not pointing to the right IP">
|
||||
@@ -232,7 +217,7 @@
|
||||
</header>
|
||||
<div class="application-settings-section-body relative min-h-0 flex-1 overflow-y-auto"
|
||||
style="-webkit-overflow-scrolling: touch;">
|
||||
<form @submit.prevent="prepareEditSubmit(); $wire.updateDomain()" class="flex flex-col gap-4">
|
||||
<form wire:submit="updateDomain" class="flex flex-col gap-4">
|
||||
<div x-show="editingServiceLabel" x-cloak class="w-full">
|
||||
<div class="mb-1.5 flex h-4 w-full items-center gap-1.5">
|
||||
<label class="mb-0! flex items-center gap-1 text-sm font-medium leading-4">Service application</label>
|
||||
@@ -243,8 +228,7 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<x-forms.domain-input id="editingDomainLocal" errorId="editingDomain" :wire="false"
|
||||
x-model="localEditingDomain" />
|
||||
<x-forms.domain-input id="editingDomainParts" errorId="editingDomain" />
|
||||
|
||||
@if ($editDomainDnsFailed)
|
||||
<x-callout type="danger" title="DNS is not pointing to the right IP">
|
||||
@@ -260,7 +244,7 @@
|
||||
<div class="flex flex-wrap items-center justify-end gap-2 pt-2">
|
||||
@if ($editDomainDnsFailed)
|
||||
<x-forms.button type="button" isError
|
||||
@click="prepareEditSubmit(); $wire.forceSaveEditDns = true; $wire.confirmUpdateDomainDespiteDns()">
|
||||
wire:click="confirmUpdateDomainDespiteDns">
|
||||
Continue
|
||||
</x-forms.button>
|
||||
@else
|
||||
|
||||
@@ -201,13 +201,7 @@
|
||||
</x-forms.button>
|
||||
@endif
|
||||
@else
|
||||
<button type="button"
|
||||
@click="$dispatch('open-edit-domain', {
|
||||
index: {{ $index }},
|
||||
url: @js($row['url']),
|
||||
serviceApplicationId: {{ (int) ($row['service_application_id'] ?? 0) }},
|
||||
serviceLabel: @js($serviceLabel),
|
||||
})"
|
||||
<button type="button" wire:click="startEdit({{ $index }})"
|
||||
class="icon-button shrink-0" title="Edit domain" aria-label="Edit domain">
|
||||
<x-reicon name="settings" class="size-3.5" />
|
||||
</button>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<x-forms.input required label="Name" id="name" />
|
||||
<x-forms.input label="Description" id="description" />
|
||||
</div>
|
||||
<x-forms.domain-input id="endpoint" host-label="Host"
|
||||
<x-forms.domain-input id="endpointParts" errorId="endpoint" host-label="Host"
|
||||
host-placeholder="minio.internal or 192.168.1.50" />
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input required label="Bucket" id="bucket" />
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<x-forms.input canGate="update" :canResource="$storage" label="Description" id="description" />
|
||||
<div class="lg:col-span-2">
|
||||
@can('update', $storage)
|
||||
<x-forms.domain-input id="endpoint" host-label="Host"
|
||||
<x-forms.domain-input id="endpointParts" errorId="endpoint" host-label="Host"
|
||||
host-placeholder="minio.internal or 192.168.1.50" />
|
||||
@else
|
||||
<x-forms.input label="Endpoint" :value="$endpoint" disabled />
|
||||
|
||||
Reference in new issue
Block a user