fix(domains): preserve ports in split URL inputs (#11328)

This commit is contained in:
Andras Bacsai authored and GitHub committed 2026-08-17 16:05:21 +02:00
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 />