mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-13 20:14:45 +00:00
feat(ui): show internal hostname and densify config UI
Add an application Internal access section that loads the running container hostname (skipped for Compose apps), densify configuration diff and popup-small defaults, dock the server-timing HUD into the mobile top bar, restyle the 2FA challenge with the auth shell, and enforce 16px mobile form fonts to prevent iOS zoom.
This commit is contained in:
1 parent
8046e5a28d
commit
4d35c63d89
22 files changed
+369
-213
No files matched your search
@@ -176,6 +176,7 @@
|
||||
'project.application.configuration' => array_values(array_filter([
|
||||
['id' => 'application-details-section', 'label' => 'Application details'],
|
||||
['id' => 'public-access-section', 'label' => 'Public access'],
|
||||
$isComposeApp ? null : ['id' => 'internal-access-section', 'label' => 'Internal access'],
|
||||
['id' => 'build-pipeline-section', 'label' => 'Build pipeline'],
|
||||
$isComposeApp ? null : ['id' => 'container-image-section', 'label' => 'Container image'],
|
||||
$isComposeApp ? null : ['id' => 'networking-section', 'label' => 'Networking'],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@props([
|
||||
'diff' => null,
|
||||
'compact' => false,
|
||||
'compact' => true,
|
||||
])
|
||||
|
||||
@php
|
||||
@@ -8,18 +8,26 @@
|
||||
@endphp
|
||||
|
||||
@if ($changes->isNotEmpty())
|
||||
<div class="space-y-5">
|
||||
<div @class(['space-y-3' => $compact, 'space-y-5' => ! $compact])>
|
||||
@foreach ($changes->groupBy('section_label') as $sectionLabel => $sectionChanges)
|
||||
<div>
|
||||
<h4
|
||||
class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-neutral-500 dark:text-fg-faint">
|
||||
@class([
|
||||
'font-semibold uppercase tracking-wide text-neutral-500 dark:text-fg-faint',
|
||||
'mb-1 text-[10px]' => $compact,
|
||||
'mb-2 text-[11px]' => ! $compact,
|
||||
])>
|
||||
{{ $sectionLabel }}
|
||||
</h4>
|
||||
|
||||
<div class="overflow-x-auto rounded-lg ring-1 ring-neutral-200 dark:ring-white/[0.08]">
|
||||
<div class="min-w-[640px]">
|
||||
<div class="overflow-x-auto rounded-md ring-1 ring-neutral-200 dark:ring-white/[0.08]">
|
||||
<div class="min-w-[560px]">
|
||||
<div
|
||||
class="grid grid-cols-[minmax(10rem,0.8fr)_minmax(14rem,1fr)_1.5rem_minmax(14rem,1fr)_1.75rem] items-center gap-3 bg-neutral-100 px-3 py-2 text-[11px] font-medium text-neutral-500 dark:bg-white/[0.04] dark:text-fg-faint">
|
||||
@class([
|
||||
'grid grid-cols-[minmax(8rem,0.75fr)_minmax(12rem,1fr)_1.25rem_minmax(12rem,1fr)_1.5rem] items-center text-[10px] font-medium text-neutral-500 dark:bg-white/[0.04] dark:text-fg-faint bg-neutral-100',
|
||||
'gap-2 px-2 py-1' => $compact,
|
||||
'gap-3 px-3 py-2' => ! $compact,
|
||||
])>
|
||||
<div>Field</div>
|
||||
<div>Current</div>
|
||||
<div></div>
|
||||
@@ -42,7 +50,11 @@
|
||||
@endphp
|
||||
|
||||
<div
|
||||
class="grid grid-cols-[minmax(10rem,0.8fr)_minmax(14rem,1fr)_1.5rem_minmax(14rem,1fr)_1.75rem] items-start gap-3 px-3 py-2.5 text-sm transition-colors hover:bg-neutral-50 dark:hover:bg-white/[0.025]">
|
||||
@class([
|
||||
'grid grid-cols-[minmax(8rem,0.75fr)_minmax(12rem,1fr)_1.25rem_minmax(12rem,1fr)_1.5rem] items-start transition-colors hover:bg-neutral-50 dark:hover:bg-white/[0.025]',
|
||||
'gap-2 px-2 py-1.5 text-xs' => $compact,
|
||||
'gap-3 px-3 py-2.5 text-sm' => ! $compact,
|
||||
])>
|
||||
<div class="min-w-0 font-medium text-neutral-900 dark:text-fg">
|
||||
@if ($rowExpandable)
|
||||
<div class="break-words"
|
||||
@@ -66,7 +78,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center pt-0.5 text-neutral-400 dark:text-fg-faint">
|
||||
<x-reicon name="arrow-right" class="size-3.5" />
|
||||
<x-reicon name="arrow-right" class="size-3" />
|
||||
</div>
|
||||
|
||||
<div class="min-w-0 text-emerald-600 dark:text-emerald-400">
|
||||
@@ -86,13 +98,13 @@
|
||||
x-on:click="expandedRows['{{ $changeKey }}'] = ! expandedRows['{{ $changeKey }}']"
|
||||
:aria-expanded="!! expandedRows['{{ $changeKey }}']"
|
||||
title="Toggle full value"
|
||||
class="flex size-6 items-center justify-center rounded-md text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-neutral-700 dark:text-fg-faint dark:hover:bg-white/[0.06] dark:hover:text-fg">
|
||||
class="flex size-5 items-center justify-center rounded text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-neutral-700 dark:text-fg-faint dark:hover:bg-white/[0.06] dark:hover:text-fg">
|
||||
<x-reicon name="eye"
|
||||
x-show="! expandedRows['{{ $changeKey }}']"
|
||||
class="size-3.5" />
|
||||
class="size-3" />
|
||||
<x-reicon name="eye-off2"
|
||||
x-show="expandedRows['{{ $changeKey }}']"
|
||||
x-cloak class="size-3.5" />
|
||||
x-cloak class="size-3" />
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@@ -7,27 +7,27 @@
|
||||
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 w-[calc(100%-2rem)] max-w-md">
|
||||
<div class="relative flex items-start gap-3 rounded-lg p-4 pr-12"
|
||||
class="fixed bottom-4 right-4 z-999 w-[calc(100%-2rem)] max-w-sm">
|
||||
<div class="relative flex items-start gap-2.5 rounded-lg p-3 pr-10"
|
||||
style="background: var(--coollabs-elevated); box-shadow: 0 0 0 1px var(--coollabs-line), var(--shadow-modal);">
|
||||
@isset($icon)
|
||||
<div
|
||||
class="flex size-9 shrink-0 items-center justify-center rounded-lg bg-amber-100 text-amber-700 dark:bg-warning/10 dark:text-warning">
|
||||
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 }}
|
||||
</div>
|
||||
@endisset
|
||||
|
||||
<div class="min-w-0 flex-1 pt-0.5">
|
||||
<h4 class="text-sm font-semibold leading-5 text-neutral-950 dark:text-fg">
|
||||
<div class="min-w-0 flex-1">
|
||||
<h4 class="text-[13px] font-semibold leading-4 text-neutral-950 dark:text-fg">
|
||||
{{ $title }}
|
||||
</h4>
|
||||
<div class="mt-1 text-xs leading-5 text-neutral-600 dark:text-fg-dim">
|
||||
<div class="mt-0.5 text-[11px] leading-4 text-neutral-600 dark:text-fg-dim">
|
||||
{{ $description }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" @click="bannerVisible = false" aria-label="Dismiss"
|
||||
class="absolute right-3 top-3 flex size-7 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">
|
||||
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>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{{-- Dev-only Server-Timing HUD. Injected by AddServerTimingHeaders on full HTML responses.
|
||||
JS docks into #server-timing-hud-slot in the desktop top bar (lg+); otherwise floats bottom-left. --}}
|
||||
JS docks into the top bar: #server-timing-hud-slot (lg+) or #server-timing-hud-slot-mobile (<lg);
|
||||
floats bottom-left only if no navbar slot is available. --}}
|
||||
<div id="server-timing-hud" data-server-timing-hud data-metrics='@json($metrics)' data-path="{{ $path }}"
|
||||
data-sth-mode="float"
|
||||
style="position:fixed;bottom:12px;left:12px;z-index:2147483000;font:12px/1.4 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;color:#e5e5e5;pointer-events:auto">
|
||||
@@ -52,16 +53,21 @@
|
||||
|
||||
function pickSlot() {
|
||||
const desktop = document.getElementById('server-timing-hud-slot');
|
||||
// Match Tailwind `lg` (1024px): only dock into the desktop top bar.
|
||||
// On mobile the header is cramped — float bottom-left instead of docking.
|
||||
const mobile = document.getElementById('server-timing-hud-slot-mobile');
|
||||
// Match Tailwind `lg` (1024px): desktop top bar vs mobile top bar.
|
||||
const isLg = window.matchMedia('(min-width: 1024px)').matches;
|
||||
if (isLg && desktop) {
|
||||
return desktop;
|
||||
}
|
||||
return null;
|
||||
if (!isLg && mobile) {
|
||||
return mobile;
|
||||
}
|
||||
// Fallback if one breakpoint's slot is missing from the shell.
|
||||
return desktop || mobile || null;
|
||||
}
|
||||
|
||||
function applyDockedStyles(root) {
|
||||
const isMobileSlot = root.parentElement && root.parentElement.id === 'server-timing-hud-slot-mobile';
|
||||
root.setAttribute('data-sth-mode', 'docked');
|
||||
root.style.cssText = [
|
||||
'position:relative',
|
||||
@@ -71,15 +77,35 @@
|
||||
'right:auto',
|
||||
'z-index:60',
|
||||
'display:block',
|
||||
'flex-shrink:0',
|
||||
'flex-shrink:1',
|
||||
'min-width:0',
|
||||
'max-width:100%',
|
||||
'visibility:visible',
|
||||
'opacity:1',
|
||||
'margin-right:8px',
|
||||
isMobileSlot ? 'margin-right:4px' : 'margin-right:8px',
|
||||
'font:11px/1.3 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace',
|
||||
'color:#e5e5e5',
|
||||
'pointer-events:auto',
|
||||
].join(';');
|
||||
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
const panel = qs(root, '[data-sth-panel]');
|
||||
if (panel) {
|
||||
// Dropdown below the pill inside the top bar.
|
||||
@@ -89,6 +115,10 @@
|
||||
panel.style.top = 'calc(100% + 8px)';
|
||||
panel.style.bottom = 'auto';
|
||||
panel.style.zIndex = '2147483001';
|
||||
if (isMobileSlot) {
|
||||
// Keep panel on-screen: open near the right edge of the viewport.
|
||||
panel.style.width = 'min(420px, calc(100vw - 16px))';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -563,7 +593,13 @@
|
||||
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;
|
||||
summary.textContent = 'ST ' + app + ' · ' + db + ' · ' + q + (n > 1 ? ' · ×' + n : '');
|
||||
// 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';
|
||||
summary.textContent = compactSummary
|
||||
? app
|
||||
: ('ST ' + app + ' · ' + db + ' · ' + q + (n > 1 ? ' · ×' + n : ''));
|
||||
if (countEl) {
|
||||
countEl.textContent = n + (n === 1 ? ' request' : ' requests');
|
||||
}
|
||||
|
||||
@@ -64,13 +64,15 @@
|
||||
</div>
|
||||
@endif
|
||||
@if ($server->proxySet())
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!" wire:click="checkProxyStatus"
|
||||
wire:loading.attr="disabled" wire:target="checkProxyStatus" role="menuitem">
|
||||
<button type="button"
|
||||
class="listbox-option justify-start! min-h-0! h-7! gap-1.5! px-2! py-1! text-[11px] text-neutral-500 dark:text-fg-muted"
|
||||
wire:click="checkProxyStatus" wire:loading.attr="disabled" wire:target="checkProxyStatus"
|
||||
role="menuitem">
|
||||
<span class="contents" wire:loading.remove wire:target="checkProxyStatus">
|
||||
<x-reicon name="refresh" class="size-3 opacity-70" />
|
||||
<x-reicon name="refresh" class="size-2.5 opacity-70" />
|
||||
Refresh status
|
||||
</span>
|
||||
<span class="hidden items-center gap-2.5" wire:loading.flex wire:target="checkProxyStatus">
|
||||
<span class="hidden items-center gap-1.5" wire:loading.flex wire:target="checkProxyStatus">
|
||||
<x-loading compact />
|
||||
Refreshing status
|
||||
</span>
|
||||
|
||||
@@ -127,7 +127,7 @@
|
||||
'active' => $storage->uuid === $currentStorage->uuid,
|
||||
])">
|
||||
<x-slot:meta>
|
||||
<span class="inline-flex h-[22px] shrink-0 items-center gap-1.5 rounded-full bg-neutral-100 px-2.5 text-xs font-medium text-black dark:bg-white/[0.08] dark:text-fg"
|
||||
<span class="inline-flex h-[22px] shrink-0 items-center gap-1.5 rounded-full border border-neutral-200 bg-neutral-100 px-2.5 text-xs font-medium text-black dark:border-white/[0.12] dark:bg-white/[0.08] dark:text-fg"
|
||||
x-data="{ usable: @js((bool) $currentStorage->is_usable) }"
|
||||
@storage-status-changed.window="usable = $event.detail.isUsable">
|
||||
<span class="size-1.5 rounded-full" :class="usable ? 'bg-[#3fb950]' : 'bg-red-500'"></span>
|
||||
@@ -151,7 +151,7 @@
|
||||
'active' => $source->getMorphClass() === $currentSource->getMorphClass() && $source->uuid === $currentSource->uuid,
|
||||
])">
|
||||
<x-slot:meta>
|
||||
<span class="inline-flex h-[22px] shrink-0 items-center gap-1.5 rounded-full bg-neutral-100 px-2.5 text-xs font-medium text-black dark:bg-white/[0.08] dark:text-fg">
|
||||
<span class="inline-flex h-[22px] shrink-0 items-center gap-1.5 rounded-full border border-neutral-200 bg-neutral-100 px-2.5 text-xs font-medium text-black dark:border-white/[0.12] dark:bg-white/[0.08] dark:text-fg">
|
||||
<span @class([
|
||||
'size-1.5 rounded-full',
|
||||
'bg-[#3fb950]' => $sourceConnected,
|
||||
@@ -170,7 +170,7 @@
|
||||
'active' => $destination->getMorphClass() === $currentDestination->getMorphClass() && $destination->uuid === $currentDestination->uuid,
|
||||
])">
|
||||
<x-slot:meta>
|
||||
<span class="inline-flex h-[22px] shrink-0 items-center gap-1.5 rounded-full bg-neutral-100 px-2.5 text-xs font-medium text-black dark:bg-white/[0.08] dark:text-fg">
|
||||
<span class="inline-flex h-[22px] shrink-0 items-center gap-1.5 rounded-full border border-neutral-200 bg-neutral-100 px-2.5 text-xs font-medium text-black dark:border-white/[0.12] dark:bg-white/[0.08] dark:text-fg">
|
||||
<span @class([
|
||||
'size-1.5 rounded-full',
|
||||
'bg-[#3fb950]' => $currentDestination->getMorphClass() === 'App\\Models\\StandaloneDocker',
|
||||
@@ -266,7 +266,7 @@
|
||||
<span class="shrink-0 text-neutral-300 dark:text-fg-faint px-0.5">/</span>
|
||||
<span class="flex min-w-0 shrink items-center gap-2 h-8 px-2">
|
||||
<span class="min-w-0 truncate font-semibold text-black dark:text-fg">{{ $currentApplication->name }}</span>
|
||||
<span class="inline-flex h-[22px] shrink-0 items-center gap-1.5 rounded-full bg-neutral-100 px-2.5 text-xs font-medium text-black dark:bg-white/[0.08] dark:text-fg"
|
||||
<span class="inline-flex h-[22px] shrink-0 items-center gap-1.5 rounded-full border border-neutral-200 bg-neutral-100 px-2.5 text-xs font-medium text-black dark:border-white/[0.12] dark:bg-white/[0.08] dark:text-fg"
|
||||
title="{{ $currentApplication->status }}">
|
||||
<span class="size-1.5 rounded-full {{ $statusDotClass }}"></span>
|
||||
{{ $statusLabel }}
|
||||
|
||||
Reference in new issue
Block a user