mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-14 22:17:19 +00:00
feat(domains): add structured URL editing and responsive domain controls
Add reusable domain URL parsing and input components, expose per-domain redirect and indexing controls, and improve application and service domain layouts across responsive breakpoints.
This commit is contained in:
1 parent
61a2b183d3
commit
32bf1860d4
30 files changed
+693
-292
No files matched your search
@@ -0,0 +1,76 @@
|
||||
@props([
|
||||
'id',
|
||||
'wire' => true,
|
||||
'value' => '',
|
||||
'errorId' => null,
|
||||
])
|
||||
|
||||
<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.value = next;
|
||||
},
|
||||
}" x-modelable="value" {{ $attributes->whereStartsWith('x-model') }}>
|
||||
<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'],
|
||||
]" />
|
||||
</div>
|
||||
|
||||
<div class="min-w-0">
|
||||
<label for="{{ $id }}" class="mb-1.5 block text-sm font-medium">
|
||||
Domain <x-highlighted text="*" />
|
||||
</label>
|
||||
<input id="{{ $id }}" type="text" class="input" x-model="host" placeholder="app.example.com"
|
||||
autocomplete="off" required />
|
||||
@error($errorId ?? $id)
|
||||
<p class="mt-1 text-[12px] text-red-500">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="min-w-0">
|
||||
<label for="{{ $id }}-port" class="mb-1.5 block text-sm font-medium">Port</label>
|
||||
<input id="{{ $id }}-port" type="number" class="input" x-model="port" placeholder="3000"
|
||||
min="1" max="65535" inputmode="numeric" />
|
||||
</div>
|
||||
|
||||
<div class="min-w-0 sm:col-span-3">
|
||||
<label for="{{ $id }}-path" class="mb-1.5 block text-sm font-medium">Path</label>
|
||||
<input id="{{ $id }}-path" type="text" class="input" x-model="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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -38,11 +38,11 @@
|
||||
this.visible = true;
|
||||
const rect = target.getBoundingClientRect();
|
||||
this.below = rect.top < 48;
|
||||
this.x = rect.left + rect.width / 2;
|
||||
this.x = rect.left;
|
||||
this.y = this.below ? rect.bottom + 8 : rect.top - 8;
|
||||
this.$nextTick(() => {
|
||||
const width = this.$refs.tooltip?.offsetWidth || 0;
|
||||
this.x = Math.max(width / 2 + 8, Math.min(window.innerWidth - width / 2 - 8, this.x));
|
||||
this.x = Math.max(8, Math.min(window.innerWidth - width - 8, this.x));
|
||||
this.$nextTick(() => this.positioned = true);
|
||||
});
|
||||
},
|
||||
@@ -71,6 +71,6 @@
|
||||
<div x-ref="tooltip" x-show="visible" x-cloak role="tooltip" x-text="text"
|
||||
:style="`left: ${x}px; top: ${y}px;`"
|
||||
:class="[below ? '' : '-translate-y-full', positioned ? 'visible' : 'invisible']"
|
||||
class="pointer-events-none fixed z-[100] -translate-x-1/2 whitespace-nowrap rounded-lg border border-neutral-700 bg-neutral-900 px-2 py-1 text-xs font-medium text-white shadow-lg dark:border-white/10 dark:bg-raised">
|
||||
class="pointer-events-none fixed z-[100] whitespace-nowrap rounded-lg border border-neutral-700 bg-neutral-900 px-2 py-1 text-xs font-medium text-white shadow-lg dark:border-white/10 dark:bg-raised">
|
||||
</div>
|
||||
</div>
|
||||
@@ -50,14 +50,23 @@
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="translate-y-0 opacity-100"
|
||||
x-transition:leave-end="translate-y-3 opacity-0"
|
||||
class="fixed bottom-4 right-4 z-999"
|
||||
:class="iconOnly
|
||||
? 'w-auto max-w-[calc(100%-2rem)]'
|
||||
: (compact
|
||||
? 'w-[calc(100%-2rem)] sm:w-auto sm:max-w-[calc(100%-2rem)]'
|
||||
: 'w-[calc(100%-2rem)] max-w-sm')">
|
||||
<div class="relative flex items-start gap-2.5 rounded-lg p-3 pr-10"
|
||||
:class="compact ? (iconOnly ? 'cursor-pointer p-2! pr-2!' : 'cursor-pointer') : ''" @click="restore()"
|
||||
class="fixed bottom-4 right-4 z-999">
|
||||
<button x-show="iconOnly" type="button" @click="restore()" aria-label="Restore warning"
|
||||
class="flex rounded-lg p-2"
|
||||
style="background: var(--coollabs-elevated); box-shadow: 0 0 0 1px var(--coollabs-line), var(--shadow-modal);">
|
||||
@isset($icon)
|
||||
<span
|
||||
class="flex size-7 shrink-0 items-center justify-center rounded-md bg-amber-100 text-amber-700 dark:bg-warning/10 dark:text-warning">
|
||||
{{ $icon }}
|
||||
</span>
|
||||
@endisset
|
||||
</button>
|
||||
|
||||
<div x-show="!iconOnly" class="relative flex items-start gap-2.5 rounded-lg p-3 pr-10"
|
||||
:class="compact
|
||||
? 'w-[calc(100vw-2rem)] cursor-pointer sm:w-auto sm:max-w-[calc(100vw-2rem)]'
|
||||
: 'w-[calc(100vw-2rem)] max-w-sm'"
|
||||
@click="restore()"
|
||||
style="background: var(--coollabs-elevated); box-shadow: 0 0 0 1px var(--coollabs-line), var(--shadow-modal);">
|
||||
@isset($icon)
|
||||
<div
|
||||
@@ -66,7 +75,7 @@
|
||||
</div>
|
||||
@endisset
|
||||
|
||||
<div x-show="!iconOnly" class="min-w-0 flex-1">
|
||||
<div class="min-w-0 flex-1">
|
||||
<h4 class="text-[13px] font-semibold leading-4 text-neutral-950 dark:text-fg">
|
||||
{{ $title }}
|
||||
</h4>
|
||||
@@ -76,7 +85,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button x-show="!iconOnly" type="button" @click.stop="minimizeToIcon()" aria-label="Minimize warning"
|
||||
<button type="button" @click.stop="minimizeToIcon()" aria-label="Minimize warning"
|
||||
class="absolute right-2 top-2 flex size-6 items-center justify-center rounded-md text-neutral-400 transition-colors hover:bg-black/5 hover:text-neutral-700 dark:text-fg-faint dark:hover:bg-white/[0.06] dark:hover:text-fg">
|
||||
<x-reicon name="x" class="size-3.5" />
|
||||
</button>
|
||||
|
||||
@@ -176,20 +176,12 @@
|
||||
|
||||
const toggle = qs(root, '[data-sth-toggle]');
|
||||
if (toggle) {
|
||||
if (isMobileSlot) {
|
||||
// Compact pill: just "83ms" — no full metric breakdown in the bar.
|
||||
toggle.style.padding = '4px 8px';
|
||||
toggle.style.fontSize = '11px';
|
||||
toggle.style.maxWidth = 'none';
|
||||
toggle.style.overflow = 'visible';
|
||||
toggle.style.whiteSpace = 'nowrap';
|
||||
} else {
|
||||
toggle.style.padding = '';
|
||||
toggle.style.fontSize = '';
|
||||
toggle.style.maxWidth = '100%';
|
||||
toggle.style.overflow = 'hidden';
|
||||
toggle.style.textOverflow = 'ellipsis';
|
||||
}
|
||||
// Keep the navbar pill compact at every breakpoint; details stay in the panel.
|
||||
toggle.style.padding = '4px 8px';
|
||||
toggle.style.fontSize = '11px';
|
||||
toggle.style.maxWidth = 'none';
|
||||
toggle.style.overflow = 'visible';
|
||||
toggle.style.whiteSpace = 'nowrap';
|
||||
}
|
||||
|
||||
const panel = qs(root, '[data-sth-panel]');
|
||||
@@ -680,10 +672,8 @@
|
||||
const q = latest.metrics.queries !== undefined ? Math.round(Number(latest.metrics.queries)) + 'q' : '—';
|
||||
const db = latest.metrics.db !== undefined ? Number(latest.metrics.db).toFixed(0) + 'ms db' : '—';
|
||||
const n = history.length;
|
||||
// Mobile navbar is tight — show only total app time; full breakdown lives in the panel.
|
||||
const compactSummary = root.getAttribute('data-sth-mode') === 'docked'
|
||||
&& root.parentElement
|
||||
&& root.parentElement.id === 'server-timing-hud-slot-mobile';
|
||||
// Navbar pills show only total app time; full breakdown lives in the panel.
|
||||
const compactSummary = root.getAttribute('data-sth-mode') === 'docked';
|
||||
summary.textContent = compactSummary
|
||||
? app
|
||||
: ('ST ' + app + ' · ' + db + ' · ' + q + (n > 1 ? ' · ×' + n : ''));
|
||||
|
||||
Reference in new issue
Block a user