feat(ui): Shadow UI redesign, domain management, and DNS autoconfigure (#11119)

Co-authored-by: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com>
This commit is contained in:
Andras BacsaiandShadowArcanist authored and GitHub committed 2026-08-03 23:11:54 +02:00
1 parent 0b843bb07c
commit 8ae56587d5
497 files changed
+44045 -22630

No files matched your search

@@ -1,300 +1,254 @@
<nav wire:poll.10000ms="checkStatus" class="pb-6">
<nav wire:poll.10000ms="checkStatus" class="w-full max-w-[1180px] pb-4 md:pb-6 lg:pb-0">
@php
$databasePageItems = [
['label' => 'Configuration', 'route' => 'project.database.configuration', 'active' => request()->routeIs('project.database.configuration')],
[
'label' => 'Settings',
'route' => 'project.database.configuration',
'active' => request()->routeIs('project.database.configuration')
|| request()->routeIs('project.database.environment-variables')
|| request()->routeIs('project.database.servers')
|| request()->routeIs('project.database.import-backup')
|| request()->routeIs('project.database.persistent-storage')
|| request()->routeIs('project.database.healthcheck')
|| request()->routeIs('project.database.webhooks')
|| request()->routeIs('project.database.resource-limits')
|| request()->routeIs('project.database.resource-operations')
|| request()->routeIs('project.database.metrics')
|| request()->routeIs('project.database.tags')
|| request()->routeIs('project.database.danger'),
],
[
'label' => 'Backups',
'route' => 'project.database.backup.index',
'active' => request()->routeIs('project.database.backup.*'),
'visible' => $database->isBackupSolutionAvailable(),
],
['label' => 'Logs', 'route' => 'project.database.logs', 'active' => request()->routeIs('project.database.logs')],
['label' => 'Terminal', 'route' => 'project.database.command', 'active' => request()->routeIs('project.database.command'), 'navigate' => false, 'visible' => auth()->user()?->can('canAccessTerminal')],
[
'label' => 'Runtime Logs',
'route' => 'project.database.logs',
'active' => request()->routeIs('project.database.logs'),
'navigate' => false,
],
[
'label' => 'Console',
'route' => 'project.database.command',
'active' => request()->routeIs('project.database.command'),
'navigate' => false,
'visible' => auth()->user()?->can('canAccessTerminal'),
],
];
$databaseConfigurationItems = [
['label' => 'General', 'route' => 'project.database.configuration', 'active' => request()->routeIs('project.database.configuration')],
['label' => 'Environment Variables', 'route' => 'project.database.environment-variables', 'active' => request()->routeIs('project.database.environment-variables')],
['label' => 'Servers', 'route' => 'project.database.servers', 'active' => request()->routeIs('project.database.servers')],
['label' => 'Persistent Storage', 'route' => 'project.database.persistent-storage', 'active' => request()->routeIs('project.database.persistent-storage')],
['label' => 'Import Backup', 'route' => 'project.database.import-backup', 'active' => request()->routeIs('project.database.import-backup'), 'visible' => auth()->user()?->can('update', $database)],
['label' => 'Webhooks', 'route' => 'project.database.webhooks', 'active' => request()->routeIs('project.database.webhooks')],
['label' => 'Healthcheck', 'route' => 'project.database.healthcheck', 'active' => request()->routeIs('project.database.healthcheck')],
['label' => 'Resource Limits', 'route' => 'project.database.resource-limits', 'active' => request()->routeIs('project.database.resource-limits')],
['label' => 'Resource Operations', 'route' => 'project.database.resource-operations', 'active' => request()->routeIs('project.database.resource-operations')],
['label' => 'Metrics', 'route' => 'project.database.metrics', 'active' => request()->routeIs('project.database.metrics')],
['label' => 'Tags', 'route' => 'project.database.tags', 'active' => request()->routeIs('project.database.tags')],
['label' => 'Danger Zone', 'route' => 'project.database.danger', 'active' => request()->routeIs('project.database.danger')],
];
$databasePageItems = array_values(array_filter(
$databasePageItems,
fn (array $item): bool => $item['visible'] ?? true,
));
$databasePageItems = array_values(array_filter($databasePageItems, fn (array $item): bool => $item['visible'] ?? true));
$databaseConfigurationItems = array_values(array_filter($databaseConfigurationItems, fn (array $item): bool => $item['visible'] ?? true));
$activeDatabaseConfigurationItem = collect($databaseConfigurationItems)->firstWhere('active', true);
$activeDatabasePageItem = collect($databasePageItems)->firstWhere('active', true);
$activeDatabaseMobileItem = $activeDatabaseConfigurationItem ?? $activeDatabasePageItem ?? $databasePageItems[0];
$activeDatabaseMobileGroup = $activeDatabaseConfigurationItem ? 'configuration' : 'database';
$activeDatabaseMobileNavigation = ($activeDatabaseMobileItem['navigate'] ?? true) ? 'navigate' : 'location';
$activeDatabaseMobileValue = $activeDatabaseMobileNavigation.'|'.$activeDatabaseMobileGroup.'|'.route($activeDatabaseMobileItem['route'], $parameters);
$databaseMobileMenuChangeHandler = <<<'JS'
const value = $event.target.value;
if (!value) {
return;
}
if (value.startsWith('navigate|')) {
const url = value.split('|').slice(2).join('|');
window.Livewire?.navigate ? window.Livewire.navigate(url) : window.location.href = url;
return;
}
if (value.startsWith('location|')) {
const url = value.split('|').slice(2).join('|');
window.location.href = url;
return;
}
resetToCurrent();
if (value.startsWith('external:')) {
window.open(value.slice(9), '_blank', 'noopener');
return;
}
document.getElementById(`database-${value.slice(7)}-trigger`)?.click();
JS;
$databaseStatus = str($database->status ?? 'exited');
[$databaseStatusLabel, $databaseStatusType] = match (true) {
$databaseStatus->startsWith('running') => ['Running', 'success'],
$databaseStatus->startsWith('degraded') => ['Degraded', 'warning'],
$databaseStatus->startsWith('restarting'),
$databaseStatus->startsWith('starting') => ['Starting', 'warning'],
default => ['Stopped', 'neutral'],
};
@endphp
<x-resources.breadcrumbs :resource="$database" :parameters="$parameters" />
<x-slide-over @startdatabase.window="slideOverOpen = true" closeWithX fullScreen>
<livewire:project.shared.configuration-checker :resource="$database" />
<x-process-dialog @startdatabase.window="processDialogOpen = true" closeWithX>
<x-slot:title>Database Startup</x-slot:title>
<x-slot:content>
<div wire:ignore class="h-full min-h-0 min-w-0 max-w-full">
<div wire:ignore class="flex h-full min-h-0 min-w-0 max-w-full flex-col">
<livewire:activity-monitor header="Logs" fullHeight />
</div>
</x-slot:content>
</x-slide-over>
<div class="navbar-main">
</x-process-dialog>
@teleport('#server-topbar-context')
<div class="flex min-w-0 items-center gap-1 text-[13px]">
<span class="shrink-0 px-0.5 text-neutral-300 dark:text-fg-faint">/</span>
<span class="flex min-w-0 shrink items-center gap-2 px-1">
<span class="max-w-48 min-w-0 truncate font-semibold text-black dark:text-fg xl:max-w-64">
{{ $database->name }}
</span>
<x-status-badge :status="$databaseStatusLabel" :type="$databaseStatusType" />
</span>
</div>
@endteleport
<div x-data>
<div class="mb-3 w-full lg:hidden">
<div class="flex min-w-0 flex-wrap items-center gap-2">
<h1 class="min-w-0 truncate text-[24px]! leading-7! font-semibold! tracking-tight! text-black dark:text-fg">
{{ $database->name }}
</h1>
<x-status-badge :status="$databaseStatusLabel" :type="$databaseStatusType" />
</div>
</div>
<div class="w-full md:hidden">
@if ($database->destination->server->isFunctional())
<div id="database-mobile-actions" class="mt-2 mb-3 md:hidden">
<div class="mb-1 text-xs font-semibold uppercase tracking-wide text-neutral-500 dark:text-neutral-400">Actions</div>
<div class="flex flex-nowrap items-center gap-2 overflow-x-auto">
@if (!str($database->status)->startsWith('exited'))
<button type="button" class="button shrink-0"
@click="document.getElementById('database-restart-trigger')?.click()">
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2">
<path d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
<path d="M20 4v5h-5" />
</g>
</svg>
Restart
</button>
<x-forms.button isError class="shrink-0"
@click="document.getElementById('database-stop-trigger')?.click()">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
<path
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
</svg>
Stop
</x-forms.button>
@else
<button type="button" class="button shrink-0"
@click="document.getElementById('database-start-trigger')?.click()">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
Start
</button>
@endif
<div id="database-mobile-actions" class="relative mb-3"
x-data="{ open: false }" @click.outside="open = false"
@keydown.escape.window="open = false">
<button type="button" class="button w-full justify-between" @click="open = !open"
:aria-expanded="open" aria-haspopup="menu">
<span class="inline-flex items-center gap-2">
<x-reicon name="play-circle" class="size-3.5 opacity-70" />
Actions
</span>
<span class="inline-flex transition-transform" :class="open && 'rotate-180'">
<x-reicon name="chevron-down" class="size-3 opacity-55" />
</span>
</button>
<div x-cloak x-show="open" x-transition.origin.top.left
class="listbox-panel top-full! left-0! right-0! mt-1! w-full! min-w-0!" role="menu">
@if (! $databaseStatus->startsWith('exited'))
@can('manage', $database)
<button type="button" class="listbox-option justify-start! gap-2.5!"
@click="open = false; document.getElementById('database-restart-trigger')?.click()"
role="menuitem">
<x-reicon name="restart" class="size-3.5 text-orange-500 dark:text-warning" />
Restart
</button>
<button type="button" class="listbox-option justify-start! gap-2.5!"
@click="open = false; document.getElementById('database-stop-trigger')?.click()"
role="menuitem">
<x-reicon name="stop" class="size-3.5 text-error" />
Stop
</button>
@else
<button type="button" class="listbox-option justify-start! gap-2.5!" disabled
role="menuitem">
<x-reicon name="restart" class="size-3.5 opacity-70" />
Restart
</button>
<button type="button" class="listbox-option justify-start! gap-2.5!" disabled
role="menuitem">
<x-reicon name="stop" class="size-3.5 opacity-70" />
Stop
</button>
@endcan
@else
@can('manage', $database)
<button type="button" class="listbox-option justify-start! gap-2.5!"
@click="open = false; $wire.dispatch('startEvent')" role="menuitem">
<x-reicon name="play-circle" class="size-3.5 text-warning" />
Start
</button>
@else
<button type="button" class="listbox-option justify-start! gap-2.5!" disabled
role="menuitem">
<x-reicon name="play-circle" class="size-3.5 opacity-70" />
Start
</button>
@endcan
@endif
</div>
</div>
@endif
<label id="database-mobile-section-label" for="database-mobile-section" class="mb-1 block text-xs font-semibold uppercase tracking-wide text-neutral-500 dark:text-neutral-400">Section</label>
<select id="database-mobile-section" class="select w-full" aria-label="Database menu"
data-current-value="{{ $activeDatabaseMobileValue }}"
x-data="{
init() {
this.syncFromLocation();
window.Livewire?.hook?.('morphed', ({ el }) => {
if (el.contains(this.$el)) {
queueMicrotask(() => this.syncFromLocation());
}
});
},
selected: $el.dataset.currentValue,
current: $el.dataset.currentValue,
syncFromLocation() {
const currentUrl = new URL(window.location.href);
const matchingOptions = Array.from(this.$el.options).filter((option) => {
if (!option.value.startsWith('navigate|') && !option.value.startsWith('location|')) {
return false;
}
const optionUrl = new URL(option.value.split('|').slice(2).join('|'), window.location.origin);
return optionUrl.pathname === currentUrl.pathname;
});
const selectedOption = matchingOptions.find((option) => {
return option.value.startsWith('navigate|configuration|') || option.value.startsWith('navigate|resource|');
}) || matchingOptions[0];
if (selectedOption) {
this.current = selectedOption.value;
this.selected = selectedOption.value;
}
},
resetToCurrent() {
this.selected = this.current;
},
}"
x-on:livewire:navigated.window="syncFromLocation()"
x-model="selected"
x-on:change="{{ $databaseMobileMenuChangeHandler }}">
<optgroup label="Database">
@foreach ($databasePageItems as $menuItem)
<option value="{{ ($menuItem['navigate'] ?? true) ? 'navigate' : 'location' }}|database|{{ route($menuItem['route'], $parameters) }}">
{{ $menuItem['label'] }}
</option>
@endforeach
</optgroup>
<optgroup label="Configuration">
@foreach ($databaseConfigurationItems as $menuItem)
<option value="navigate|configuration|{{ route($menuItem['route'], $parameters) }}">
{{ $menuItem['label'] }}
</option>
@endforeach
</optgroup>
</select>
<div
class="flex min-w-0 items-center gap-0.5 overflow-x-auto rounded-[10px] border border-neutral-200 bg-neutral-100 p-1 dark:border-white/[0.07] dark:bg-white/[0.035]">
@foreach ($databasePageItems as $menuItem)
<a @class([
'app-tab shrink-0',
'bg-coollabs/10 text-coollabs ring-1 ring-coollabs/25 dark:bg-warning/15 dark:text-warning dark:ring-warning/25' => $menuItem['active'],
])
@if ($menuItem['navigate'] ?? true) {{ wireNavigate() }} @endif
href="{{ route($menuItem['route'], $parameters) }}">
{{ $menuItem['label'] }}
</a>
@endforeach
</div>
</div>
<nav
class="scrollbar hidden min-h-10 w-full flex-nowrap items-center gap-6 overflow-x-scroll overflow-y-hidden pb-1 whitespace-nowrap md:flex md:w-auto md:overflow-visible">
<a class="shrink-0 {{ request()->routeIs('project.database.configuration') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
href="{{ route('project.database.configuration', $parameters) }}">
Configuration
</a>
@if ($database->isBackupSolutionAvailable())
<a class="shrink-0 {{ request()->routeIs('project.database.backup.*') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
href="{{ route('project.database.backup.index', $parameters) }}">
Backups
</a>
@endif
<a class="shrink-0 {{ request()->routeIs('project.database.logs') ? 'dark:text-white' : '' }}"
href="{{ route('project.database.logs', $parameters) }}">
Logs
</a>
@can('canAccessTerminal')
<a class="shrink-0 {{ request()->routeIs('project.database.command') ? 'dark:text-white' : '' }}"
href="{{ route('project.database.command', $parameters) }}">
Terminal
</a>
@endcan
<div class="hidden w-full items-center md:flex lg:fixed lg:top-12 lg:right-0 lg:z-30 lg:h-12 lg:w-auto lg:border-b lg:border-neutral-200 lg:bg-white/95 lg:pr-4 lg:pl-2 lg:backdrop-blur lg:transition-[left] lg:duration-200 lg:dark:border-white/[0.06] lg:dark:bg-panel/95"
:class="[typeof collapsed !== 'undefined' && collapsed ? 'lg:left-16' : 'lg:left-56']">
<div
class="resource-heading-navbar application-heading-actions flex w-full min-w-0 items-center justify-between gap-2 overflow-visible rounded-[10px] border border-neutral-200 bg-neutral-100 p-1 dark:border-white/[0.07] dark:bg-white/[0.035]">
<div class="resource-heading-tabs flex min-w-0 items-center gap-0.5 overflow-x-auto">
@foreach ($databasePageItems as $menuItem)
<a wire:key="database-primary-nav-{{ str($menuItem['label'])->slug() }}"
@class([
'app-tab shrink-0',
'bg-coollabs/10 text-coollabs shadow-sm ring-1 ring-coollabs/25 hover:bg-coollabs/15 dark:bg-warning/15 dark:text-warning dark:ring-warning/25 dark:hover:bg-warning/20' => $menuItem['active'],
])
@if ($menuItem['navigate'] ?? true) {{ wireNavigate() }} @endif
href="{{ route($menuItem['route'], $parameters) }}">
{{ $menuItem['label'] }}
</a>
@endforeach
</div>
</nav>
@if ($database->destination->server->isFunctional())
<div class="flex flex-wrap gap-2 items-center">
<div class="hidden flex-wrap items-center gap-2 md:flex">
@if (!str($database->status)->startsWith('exited'))
<x-forms.button canGate="manage" :canResource="$database" title="Restart" @click="document.getElementById('database-restart-trigger')?.click()">
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2">
<path d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
<path d="M20 4v5h-5" />
</g>
</svg>
Restart
</x-forms.button>
<x-forms.button canGate="manage" :canResource="$database" isError title="Stop" @click="document.getElementById('database-stop-trigger')?.click()">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
<path
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
</svg>
Stop
</x-forms.button>
<div class="resource-heading-actions flex shrink-0 items-center gap-0.5 border-l border-neutral-200 pl-1 dark:border-white/[0.08]">
@if ($database->destination->server->isFunctional())
@if (! $databaseStatus->startsWith('exited'))
<x-forms.button canGate="manage" :canResource="$database"
@click="document.getElementById('database-restart-trigger')?.click()">
<x-reicon name="restart" class="size-4 text-orange-500 dark:text-warning" />
Restart
</x-forms.button>
<x-forms.button canGate="manage" :canResource="$database" isError
@click="document.getElementById('database-stop-trigger')?.click()">
<x-reicon name="stop" class="size-4 text-error" />
Stop
</x-forms.button>
@else
<x-forms.button canGate="manage" :canResource="$database"
@click="$wire.dispatch('startEvent')">
<x-reicon name="play-circle" class="size-4 text-warning" />
Start
</x-forms.button>
@endif
@else
<x-forms.button canGate="manage" :canResource="$database" @click="document.getElementById('database-start-trigger')?.click()" class="gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
Start
</x-forms.button>
<x-status-badge status="Server unavailable" type="error" />
@endif
</div>
@script
<script>
$wire.$on('startEvent', () => {
window.dispatchEvent(new CustomEvent('startdatabase'));
$wire.$call('start');
});
$wire.$on('restartEvent', () => {
$wire.$dispatch('info', 'Restarting database.');
window.dispatchEvent(new CustomEvent('startdatabase'));
$wire.$call('restart');
});
</script>
@endscript
</div>
@else
<div class="text-error">Underlying server is not functional.</div>
@endif
</div>
<div class="hidden lg:block lg:h-12" aria-hidden="true"></div>
</div>
@if ($database->destination->server->isFunctional())
<x-modal-confirmation title="Confirm Database Restart?" buttonTitle="Restart" submitAction="restartEvent"
:actions="[
<div class="hidden" aria-hidden="true">
<x-modal-confirmation title="Confirm Database Restart?" buttonTitle="Restart"
submitAction="restartEvent" :actions="[
'This database will be unavailable during the restart.',
'If the database is currently in use data could be lost.',
'If the database is currently in use, data could be lost.',
]" :confirmWithText="false" :confirmWithPassword="false" step2ButtonText="Restart Database"
:dispatchAction="true">
<x-slot:trigger>
<button id="database-restart-trigger" type="button" class="hidden">Restart</button>
<button id="database-restart-trigger" type="button">Restart</button>
</x-slot:trigger>
</x-modal-confirmation>
<x-modal-confirmation title="Confirm Database Stopping?" buttonTitle="Stop" submitAction="stop"
:checkboxes="$checkboxes" :actions="[
'This database will be stopped.',
'If the database is currently in use data could be lost.',
'All non-persistent data of this database (containers, networks, unused images) will be deleted (don\'t worry, no data is lost and you can start the database again).',
'If the database is currently in use, data could be lost.',
'Non-persistent containers, networks, and unused images will be removed.',
]" :confirmWithText="false" :confirmWithPassword="false"
step1ButtonText="Continue" step2ButtonText="Confirm">
<x-slot:trigger>
<button id="database-stop-trigger" type="button" class="hidden">Stop</button>
</x-slot:trigger>
</x-modal-confirmation>
<x-modal-confirmation title="Confirm Database Start?" buttonTitle="Start" submitAction="startEvent"
:actions="[
'This database will be started.',
]" :confirmWithText="false" :confirmWithPassword="false" step2ButtonText="Start Database"
:dispatchAction="true">
<x-slot:trigger>
<button id="database-start-trigger" type="button" class="hidden">Start</button>
<button id="database-stop-trigger" type="button">Stop</button>
</x-slot:trigger>
</x-modal-confirmation>
</div>
@endif
@script
<script>
$wire.$on('startEvent', () => {
window.dispatchEvent(new CustomEvent('startdatabase'));
$wire.$call('start');
});
$wire.$on('restartEvent', () => {
$wire.$dispatch('info', 'Restarting database.');
window.dispatchEvent(new CustomEvent('startdatabase'));
$wire.$call('restart');
});
</script>
@endscript
</nav>