coolify/resources/views/livewire/project/database/heading.blade.php
Andras Bacsai a7a06aa6c8 feat(ui): polish domains, storage, env vars and resource nav
Improve project resource UIs: sort domains by DNS failure, stop re-adding www pairs on refresh, lazy-load storage tabs with counts, tighten env-var tables, keep application tabs active across Livewire polls, unify database type labels, and update related CSS/JS and tests.
2026-08-05 13:50:11 +02:00

258 lines
14 KiB
PHP

<nav wire:poll.10000ms="checkStatus" class="w-full max-w-[1180px] pb-4 md:pb-6 lg:pb-0">
@php
$databasePageItems = [
[
'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' => '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'),
],
];
$databasePageItems = array_values(array_filter(
$databasePageItems,
fn (array $item): bool => $item['visible'] ?? true,
));
$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
<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="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-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="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
<div
class="flex min-w-0 items-center gap-0.5 rounded-[10px] border border-neutral-200 bg-neutral-100 p-1 dark:border-white/[0.07] dark:bg-white/[0.035]">
<x-resource-heading-tabs class="min-w-0 flex-1">
@foreach ($databasePageItems as $menuItem)
<a @class([
'app-tab shrink-0',
'app-tab-active' => $menuItem['active'],
])
@if ($menuItem['active']) aria-current="page" @endif
@if ($menuItem['navigate'] ?? true) {{ wireNavigate() }} @endif
href="{{ route($menuItem['route'], $parameters) }}">
{{ $menuItem['label'] }}
</a>
@endforeach
</x-resource-heading-tabs>
</div>
</div>
<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]">
<x-resource-heading-tabs class="min-w-0">
@foreach ($databasePageItems as $menuItem)
<a wire:key="database-primary-nav-{{ str($menuItem['label'])->slug() }}"
@class([
'app-tab shrink-0',
'app-tab-active' => $menuItem['active'],
])
@if ($menuItem['active']) aria-current="page" @endif
@if ($menuItem['navigate'] ?? true) {{ wireNavigate() }} @endif
href="{{ route($menuItem['route'], $parameters) }}">
{{ $menuItem['label'] }}
</a>
@endforeach
</x-resource-heading-tabs>
<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-status-badge status="Server unavailable" type="error" />
@endif
</div>
</div>
</div>
<div class="hidden lg:block lg:h-12" aria-hidden="true"></div>
</div>
@if ($database->destination->server->isFunctional())
<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.',
]" :confirmWithText="false" :confirmWithPassword="false" step2ButtonText="Restart Database"
:dispatchAction="true">
<x-slot:trigger>
<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.',
'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">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>