mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-15 10:14:47 +00:00
feat(ui): unify resource nav sidebars and polish project layouts
Extract application/database/service configuration sidebars and unified headings; improve clone-to-environment, embedded deployments, storage actions, OAuth icons, terminal, and shared form components.
This commit is contained in:
1 parent
2291c3fa1c
commit
5de6f97256
85 files changed
+2087
-981
No files matched your search
@@ -29,15 +29,49 @@
|
||||
@if ($backup->database_type === 'App\Models\StandalonePostgresql' && $backup->database_id !== 0
|
||||
|| $backup->database_type === 'App\Models\StandaloneMysql'
|
||||
|| $backup->database_type === 'App\Models\StandaloneMariadb')
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<div class="grid w-full gap-4">
|
||||
<x-forms.listbox id="dumpAll" label="Database selection" onChange="instantSave" :options="[
|
||||
['value' => true, 'label' => 'Back up all databases'],
|
||||
['value' => false, 'label' => 'Choose databases'],
|
||||
]" />
|
||||
@if (! $backup->dump_all)
|
||||
<x-forms.input label="Databases to back up"
|
||||
helper="Comma-separated database names. Leave empty to include the default database."
|
||||
id="databasesToBackup" />
|
||||
<div class="w-full" x-data="{
|
||||
value: @entangle('databasesToBackup').live,
|
||||
draft: '',
|
||||
get databases() {
|
||||
return (this.value || '').split(',').map(name => name.trim()).filter(Boolean);
|
||||
},
|
||||
addDatabase() {
|
||||
const names = this.draft.split(',').map(name => name.trim()).filter(Boolean);
|
||||
if (names.length === 0) return;
|
||||
this.value = [...new Set([...this.databases, ...names])].join(',');
|
||||
this.draft = '';
|
||||
},
|
||||
removeDatabase(index) {
|
||||
this.value = this.databases.filter((_, itemIndex) => itemIndex !== index).join(',');
|
||||
},
|
||||
}">
|
||||
<label class="mb-1.5 block text-sm font-medium">Databases to back up</label>
|
||||
<div class="chip-input">
|
||||
<template x-for="(database, index) in databases" :key="database">
|
||||
<span class="chip font-mono">
|
||||
<span x-text="database"></span>
|
||||
<button type="button" @click="removeDatabase(index)"
|
||||
class="chip-remove"
|
||||
:aria-label="`Remove ${database}`">
|
||||
<x-reicon name="x" class="size-3" />
|
||||
</button>
|
||||
</span>
|
||||
</template>
|
||||
<input x-model="draft" @keydown.enter.prevent="addDatabase()"
|
||||
@keydown="if ($event.key === ',') { $event.preventDefault(); addDatabase(); }"
|
||||
@blur="addDatabase()" type="text"
|
||||
placeholder="Type a database and press Enter" />
|
||||
</div>
|
||||
<p class="mt-1.5 text-xs text-neutral-500 dark:text-fg-dim">
|
||||
Add one or more database names. Leave empty to include the default database.
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@elseif ($backup->database_type === 'App\Models\StandaloneMongodb')
|
||||
|
||||
@@ -5,22 +5,42 @@
|
||||
|
||||
<livewire:project.database.heading :database="$database" />
|
||||
|
||||
<div class="mt-8 w-full max-w-[1180px] lg:mt-3">
|
||||
<section class="application-settings-section">
|
||||
<div class="application-settings-section-header">
|
||||
<div>
|
||||
<h2>Scheduled backups</h2>
|
||||
<p>Automate database backups and track the latest execution for each schedule.</p>
|
||||
</div>
|
||||
<div class="application-settings-workspace mt-4 grid w-full max-w-[1180px] min-w-0 gap-8 lg:mt-0 xl:grid-cols-[210px_minmax(0,1fr)] xl:gap-10">
|
||||
<x-database.configuration-sidebar :database="$database" current-route="project.database.backup.index" />
|
||||
<div class="application-settings-form flex min-w-0 flex-col gap-6">
|
||||
<x-application.settings-section title="Database backups"
|
||||
description="Automate database backups and track the latest execution for each schedule.">
|
||||
@can('update', $database)
|
||||
<x-modal-input buttonTitle="+ Add" title="New Scheduled Backup" isHighlightedButton>
|
||||
<livewire:project.database.create-scheduled-backup :database="$database" />
|
||||
</x-modal-input>
|
||||
<x-slot:actions>
|
||||
<x-modal-input title="New Scheduled Backup" isHighlightedButton buttonTitle="+ Add">
|
||||
<livewire:project.database.create-scheduled-backup :database="$database" />
|
||||
</x-modal-input>
|
||||
</x-slot:actions>
|
||||
@endcan
|
||||
</div>
|
||||
<div class="application-settings-section-body p-0!">
|
||||
<livewire:project.database.scheduled-backups :database="$database" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-3">
|
||||
<div>
|
||||
<p class="text-xs font-medium text-neutral-500 dark:text-fg-dim">Schedules</p>
|
||||
<p class="mt-1 text-xl font-semibold tabular-nums text-neutral-950 dark:text-fg">
|
||||
{{ $database->scheduledBackups->count() }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-medium text-neutral-500 dark:text-fg-dim">Enabled</p>
|
||||
<p class="mt-1 text-xl font-semibold tabular-nums text-neutral-950 dark:text-fg">
|
||||
{{ $database->scheduledBackups->where('enabled', true)->count() }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-medium text-neutral-500 dark:text-fg-dim">Total executions</p>
|
||||
<p class="mt-1 text-xl font-semibold tabular-nums text-neutral-950 dark:text-fg">
|
||||
{{ $database->scheduledBackups->sum('executions_count') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</x-application.settings-section>
|
||||
|
||||
<livewire:project.database.scheduled-backups :database="$database" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -5,76 +5,10 @@
|
||||
|
||||
<livewire:project.database.heading :database="$database" />
|
||||
|
||||
@php
|
||||
$databaseRouteParameters = [
|
||||
'project_uuid' => $project->uuid,
|
||||
'environment_uuid' => $environment->uuid,
|
||||
'database_uuid' => $database->uuid,
|
||||
];
|
||||
|
||||
$configurationItems = [
|
||||
['label' => 'General', 'route' => 'project.database.configuration', 'icon' => 'settings'],
|
||||
['label' => 'Environment Variables', 'route' => 'project.database.environment-variables', 'icon' => 'variables'],
|
||||
['label' => 'Servers', 'route' => 'project.database.servers', 'icon' => 'servers'],
|
||||
['label' => 'Persistent Storage', 'route' => 'project.database.persistent-storage', 'icon' => 'storages'],
|
||||
[
|
||||
'label' => 'Import Backup',
|
||||
'route' => 'project.database.import-backup',
|
||||
'icon' => 'upload',
|
||||
'visible' => auth()->user()?->can('update', $database),
|
||||
],
|
||||
['label' => 'Webhooks', 'route' => 'project.database.webhooks', 'icon' => 'notifications'],
|
||||
['label' => 'Healthcheck', 'route' => 'project.database.healthcheck', 'icon' => 'feedback'],
|
||||
['label' => 'Resource Limits', 'route' => 'project.database.resource-limits', 'icon' => 'cpu'],
|
||||
['label' => 'Resource Operations', 'route' => 'project.database.resource-operations', 'icon' => 'server-update'],
|
||||
['label' => 'Metrics', 'route' => 'project.database.metrics', 'icon' => 'graph'],
|
||||
['label' => 'Tags', 'route' => 'project.database.tags', 'icon' => 'tags'],
|
||||
['label' => 'Danger Zone', 'route' => 'project.database.danger', 'icon' => 'shield-alert'],
|
||||
];
|
||||
|
||||
$configurationItems = collect($configurationItems)
|
||||
->filter(fn (array $item): bool => $item['visible'] ?? true)
|
||||
->map(fn (array $item): array => [
|
||||
...$item,
|
||||
'active' => $currentRoute === $item['route'],
|
||||
]);
|
||||
|
||||
$menuGroups = [
|
||||
'Settings' => ['General', 'Environment Variables', 'Servers', 'Persistent Storage', 'Import Backup'],
|
||||
'Automation' => ['Webhooks', 'Healthcheck'],
|
||||
'Operations' => ['Resource Limits', 'Resource Operations', 'Metrics', 'Tags', 'Danger Zone'],
|
||||
];
|
||||
|
||||
$groupedItems = collect($menuGroups)
|
||||
->map(fn (array $labels) => $configurationItems->whereIn('label', $labels)->values())
|
||||
->filter(fn ($items) => $items->isNotEmpty());
|
||||
@endphp
|
||||
|
||||
<section class="application-settings-workspace mt-4 w-full max-w-[1180px] lg:mt-0">
|
||||
<div class="grid min-w-0 gap-8 xl:grid-cols-[210px_minmax(0,1fr)] xl:gap-10">
|
||||
<aside class="application-settings-navigation min-w-0 xl:self-start">
|
||||
<nav aria-label="Database settings"
|
||||
class="grid grid-cols-2 gap-0.5 border-y border-neutral-200 py-3 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-1 xl:border-y-0 xl:py-0 dark:border-white/[0.06]">
|
||||
@foreach ($groupedItems as $groupLabel => $groupItems)
|
||||
@unless ($loop->first)
|
||||
<div class="my-2 hidden border-t border-neutral-200 xl:block dark:border-white/[0.06]"
|
||||
aria-hidden="true"></div>
|
||||
@endunless
|
||||
<div class="nav-section hidden xl:block">{{ $groupLabel }}</div>
|
||||
@foreach ($groupItems as $menuItem)
|
||||
<a @class([
|
||||
'menu-item',
|
||||
'menu-item-active' => $menuItem['active'],
|
||||
])
|
||||
{{ wireNavigate() }}
|
||||
href="{{ route($menuItem['route'], $databaseRouteParameters) }}">
|
||||
<x-reicon :name="$menuItem['icon']" class="menu-item-icon" />
|
||||
<span class="menu-item-label">{{ $menuItem['label'] }}</span>
|
||||
</a>
|
||||
@endforeach
|
||||
@endforeach
|
||||
</nav>
|
||||
</aside>
|
||||
<x-database.configuration-sidebar :database="$database" :current-route="$currentRoute" />
|
||||
|
||||
<div class="min-w-0">
|
||||
@if ($currentRoute === 'project.database.configuration')
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
'navigate' => false,
|
||||
],
|
||||
[
|
||||
'label' => 'Console',
|
||||
'label' => 'Terminal',
|
||||
'route' => 'project.database.command',
|
||||
'active' => request()->routeIs('project.database.command'),
|
||||
'navigate' => false,
|
||||
@@ -77,7 +77,7 @@
|
||||
@endteleport
|
||||
|
||||
<div x-data>
|
||||
<div class="mb-3 w-full lg:hidden">
|
||||
<div class="mb-3 w-full xl: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 }}
|
||||
@@ -86,7 +86,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full md:hidden">
|
||||
<div class="w-full xl:hidden">
|
||||
@if ($database->destination->server->isFunctional())
|
||||
<div id="database-mobile-actions" class="relative mb-3"
|
||||
x-data="{ open: false }" @click.outside="open = false"
|
||||
@@ -149,56 +149,37 @@
|
||||
</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="hidden w-full items-center xl:fixed xl:top-14 xl:right-4 xl:z-30 xl:flex xl:h-12 xl:w-auto">
|
||||
<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]">
|
||||
class="resource-heading-navbar application-heading-actions flex w-auto min-w-0 items-center justify-end 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-actions flex shrink-0 items-center gap-0.5">
|
||||
@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>
|
||||
<div id="database-desktop-actions" class="relative" x-data="{ open: false }"
|
||||
@click.outside="open = false" @keydown.escape.window="open = false">
|
||||
<button type="button" class="button" @click="open = !open" :aria-expanded="open">
|
||||
<x-reicon name="play-circle" class="size-3.5 opacity-70" />
|
||||
Actions
|
||||
<x-reicon name="chevron-down" class="size-3 opacity-55" />
|
||||
</button>
|
||||
<div x-cloak x-show="open" x-transition.origin.top.right
|
||||
class="listbox-panel top-full! right-0! left-auto! mt-1! w-52! min-w-0!" role="menu">
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@disabled(!auth()->user()->can('manage', $database))
|
||||
@click="open = false; document.getElementById('database-restart-trigger')?.click()">
|
||||
<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!"
|
||||
@disabled(!auth()->user()->can('manage', $database))
|
||||
@click="open = false; document.getElementById('database-stop-trigger')?.click()">
|
||||
<x-reicon name="stop" class="size-3.5 text-error" />
|
||||
Stop
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<x-forms.button canGate="manage" :canResource="$database"
|
||||
@click="$wire.dispatch('startEvent')">
|
||||
@@ -213,7 +194,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hidden lg:block lg:h-12" aria-hidden="true"></div>
|
||||
</div>
|
||||
|
||||
@if ($database->destination->server->isFunctional())
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div x-data="{
|
||||
<div class="flex min-w-0 flex-col gap-3" x-data="{
|
||||
search: '',
|
||||
backups: @js($database->scheduledBackups->map(fn ($backup) => [
|
||||
'name' => strtolower($database->name),
|
||||
@@ -41,12 +41,12 @@
|
||||
</form>
|
||||
@else
|
||||
@if ($database->scheduledBackups->isNotEmpty())
|
||||
<div class="border-b border-neutral-200 p-3 dark:border-white/[0.06]">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="relative max-w-sm">
|
||||
<x-reicon name="search"
|
||||
class="pointer-events-none absolute top-1/2 left-2.5 z-10 size-3.5 -translate-y-1/2 text-neutral-400 dark:text-fg-faint" />
|
||||
<input type="search" x-model="search"
|
||||
class="input h-8! pl-8! text-[13px]!" placeholder="Search backup schedules…" />
|
||||
<input type="search" x-model="search" aria-label="Search backups"
|
||||
class="input h-8! w-full pl-8! text-[13px]!" placeholder="Search backups" />
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@@ -56,7 +56,7 @@
|
||||
description="Create a schedule to start protecting this database."
|
||||
icon-name="storages" />
|
||||
@else
|
||||
<div x-cloak x-show="search === '' || hasMatches()" class="data-table">
|
||||
<div x-cloak x-show="search === '' || hasMatches()" class="data-table overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]">
|
||||
<div class="data-table-header scheduled-backups-table-grid">
|
||||
<span>Schedule</span>
|
||||
<span>Latest run</span>
|
||||
@@ -83,10 +83,9 @@
|
||||
|| @js(strtolower($backup->s3?->name ?? '')).includes(search.toLowerCase())"
|
||||
class="data-table-row scheduled-backups-table-grid border-b border-neutral-200 last:border-b-0 dark:border-white/[0.06]">
|
||||
<div class="min-w-0">
|
||||
<a class="block truncate text-[12px] font-semibold text-black dark:text-fg"
|
||||
{{ wireNavigate() }} href="{{ $backupRoute }}">
|
||||
<span class="block truncate text-[12px] font-semibold text-black dark:text-fg">
|
||||
{{ $backup->frequency }}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<x-status-badge :status="$statusLabel" :type="$statusType" />
|
||||
@@ -98,7 +97,7 @@
|
||||
{{ $backup->save_s3 ? ($backup->s3?->name ?? 'Unavailable') : 'Local only' }}
|
||||
</div>
|
||||
<div class="text-[11px] text-neutral-600 dark:text-fg-dim">
|
||||
{{ $backup->executions()->count() }}
|
||||
{{ $backup->executions_count ?? $backup->executions()->count() }}
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<a class="button" {{ wireNavigate() }} href="{{ $backupRoute }}">Manage</a>
|
||||
|
||||
Reference in new issue
Block a user