mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-14 12:16:45 +00:00
feat(ui): add volume backup storage controls and grouped navigation
This commit is contained in:
1 parent
4ba6aa3500
commit
62b2fe2330
20 files changed
+373
-112
No files matched your search
@@ -52,12 +52,12 @@
|
||||
'visible' => ! $application->destination->server->isSwarm() && auth()->user()?->can('canAccessTerminal'),
|
||||
],
|
||||
[
|
||||
'label' => 'Deployment',
|
||||
'label' => 'Deployment Logs',
|
||||
'route' => 'project.application.deployment.index',
|
||||
'active' => str($currentRoute)->startsWith('project.application.deployment'),
|
||||
],
|
||||
[
|
||||
'label' => 'Runtime',
|
||||
'label' => 'Runtime Logs',
|
||||
'route' => 'project.application.logs',
|
||||
'active' => $currentRoute === 'project.application.logs',
|
||||
],
|
||||
@@ -142,8 +142,8 @@
|
||||
'Persistent Storage' => 'storages',
|
||||
'Backups' => 'database',
|
||||
'Terminal' => 'browser-terminal',
|
||||
'Deployment' => 'time-back',
|
||||
'Runtime' => 'unordered-list',
|
||||
'Deployment Logs' => 'time-back',
|
||||
'Runtime Logs' => 'unordered-list',
|
||||
'Git Source' => 'sources',
|
||||
'Servers' => 'servers',
|
||||
'Scheduled Tasks' => 'calendar',
|
||||
@@ -160,14 +160,17 @@
|
||||
|
||||
// Discord-style groups for the settings sidebar
|
||||
$menuGroups = [
|
||||
'Settings' => ['General', 'Domains', 'Advanced', 'Swarm', 'Environment Variables', 'Persistent Storage', 'Backups'],
|
||||
'Build & deploy' => ['Git Source', 'Servers', 'Healthcheck'],
|
||||
'Automation' => ['Scheduled Tasks', 'Webhooks', 'Preview Deployments'],
|
||||
'Logs' => ['Deployment', 'Runtime'],
|
||||
'Operations' => ['Terminal', 'Rollback', 'Resource Limits', 'Resource Operations', 'Metrics', 'Tags', 'Danger Zone'],
|
||||
'Settings' => ['General', 'Domains', 'Environment Variables', 'Persistent Storage', 'Advanced', 'Swarm', 'Healthcheck'],
|
||||
'Observe & troubleshoot' => ['Runtime Logs', 'Deployment Logs', 'Terminal', 'Metrics'],
|
||||
'Deploy' => ['Git Source', 'Servers', 'Preview Deployments'],
|
||||
'Automation' => ['Scheduled Tasks', 'Webhooks', 'Backups'],
|
||||
'Operations' => ['Resource Operations', 'Resource Limits', 'Rollback', 'Tags', 'Danger Zone'],
|
||||
];
|
||||
$groupedMenuItems = collect($menuGroups)
|
||||
->map(fn (array $labels) => collect($configurationMenuItems)->whereIn('label', $labels)->values())
|
||||
->map(fn (array $labels) => collect($labels)
|
||||
->map(fn (string $label) => collect($configurationMenuItems)->firstWhere('label', $label))
|
||||
->filter()
|
||||
->values())
|
||||
->filter(fn ($items) => $items->isNotEmpty());
|
||||
|
||||
// In-page sections (cards) shown as sub-items under the active page
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
['label' => 'Backups', 'route' => 'project.database.backup.index', 'icon' => 'database', 'visible' => $database->isBackupSolutionAvailable()],
|
||||
['label' => 'Import Backup', 'route' => 'project.database.import-backup', 'icon' => 'upload', 'navigate' => false, 'visible' => auth()->user()?->can('update', $database)],
|
||||
['label' => 'Servers', 'route' => 'project.database.servers', 'icon' => 'servers'],
|
||||
['label' => 'Runtime', 'route' => 'project.database.logs', 'icon' => 'unordered-list', 'navigate' => false],
|
||||
['label' => 'Runtime Logs', 'route' => 'project.database.logs', 'icon' => 'unordered-list', 'navigate' => false],
|
||||
['label' => 'Terminal', 'route' => 'project.database.command', 'icon' => 'browser-terminal', 'navigate' => false, 'visible' => auth()->user()?->can('canAccessTerminal')],
|
||||
['label' => 'Webhooks', 'route' => 'project.database.webhooks', 'icon' => 'notifications'],
|
||||
['label' => 'Healthcheck', 'route' => 'project.database.healthcheck', 'icon' => 'feedback'],
|
||||
@@ -32,14 +32,18 @@
|
||||
]);
|
||||
|
||||
$menuGroups = [
|
||||
'Settings' => ['General', 'Environment Variables', 'Persistent Storage', 'Backups', 'Import Backup', 'Servers'],
|
||||
'Automation' => ['Webhooks', 'Healthcheck'],
|
||||
'Logs' => ['Runtime'],
|
||||
'Operations' => ['Terminal', 'Resource Limits', 'Resource Operations', 'Metrics', 'Tags', 'Danger Zone'],
|
||||
'Settings' => ['General', 'Environment Variables', 'Persistent Storage', 'Healthcheck'],
|
||||
'Observe & troubleshoot' => ['Runtime Logs', 'Terminal', 'Metrics'],
|
||||
'Deploy' => ['Servers'],
|
||||
'Automation' => ['Webhooks', 'Backups', 'Import Backup'],
|
||||
'Operations' => ['Resource Operations', 'Resource Limits', 'Tags', 'Danger Zone'],
|
||||
];
|
||||
|
||||
$groupedItems = collect($menuGroups)
|
||||
->map(fn (array $labels) => $configurationItems->whereIn('label', $labels)->values())
|
||||
->map(fn (array $labels) => collect($labels)
|
||||
->map(fn (string $label) => $configurationItems->firstWhere('label', $label))
|
||||
->filter()
|
||||
->values())
|
||||
->filter(fn ($items) => $items->isNotEmpty());
|
||||
|
||||
$pageSections = $database->type() === 'standalone-postgresql'
|
||||
|
||||
@@ -13,10 +13,12 @@
|
||||
'value' => null, // initial value when wire=false
|
||||
'disabled' => false,
|
||||
'tooltip' => true,
|
||||
'portal' => false,
|
||||
])
|
||||
|
||||
@php
|
||||
$triggerId = ($htmlId ?? $id).'-trigger';
|
||||
$panelId = ($htmlId ?? $id).'-panel';
|
||||
@endphp
|
||||
|
||||
<div class="w-full min-w-0">
|
||||
@@ -41,6 +43,7 @@
|
||||
@endif
|
||||
<div class="relative min-w-0" x-data="{
|
||||
open: false,
|
||||
positioned: false,
|
||||
options: @js(array_values($options)),
|
||||
value: @if (!$wire) @js($value) @elseif ($live) @entangle($id).live @else @entangle($id) @endif,
|
||||
get current() {
|
||||
@@ -53,11 +56,42 @@
|
||||
if (String(option.value) === String(this.value)) return;
|
||||
this.value = option.value;
|
||||
@if ($onChange) this.$nextTick(() => this.$wire.{{ $onChange }}()); @endif
|
||||
},
|
||||
toggle() {
|
||||
this.open = !this.open;
|
||||
this.positioned = false;
|
||||
if (this.open && @js($portal)) {
|
||||
this.$nextTick(() => requestAnimationFrame(() => this.positionPanel()));
|
||||
}
|
||||
},
|
||||
positionPanel(panel = null) {
|
||||
const trigger = this.$refs.trigger;
|
||||
panel ??= document.getElementById(@js($panelId));
|
||||
if (!trigger || !panel) return;
|
||||
|
||||
const gap = 4;
|
||||
const edge = 12;
|
||||
const triggerRect = trigger.getBoundingClientRect();
|
||||
const panelWidth = Math.max(triggerRect.width, panel.offsetWidth);
|
||||
const panelHeight = Math.min(panel.scrollHeight, 256);
|
||||
const fitsBelow = window.innerHeight - triggerRect.bottom - gap >= panelHeight;
|
||||
const top = fitsBelow
|
||||
? triggerRect.bottom + gap
|
||||
: Math.max(edge, triggerRect.top - gap - panelHeight);
|
||||
const left = Math.min(
|
||||
Math.max(edge, triggerRect.left),
|
||||
window.innerWidth - panelWidth - edge,
|
||||
);
|
||||
|
||||
panel.style.top = `${top}px`;
|
||||
panel.style.left = `${left}px`;
|
||||
panel.style.minWidth = `${triggerRect.width}px`;
|
||||
this.positioned = true;
|
||||
}
|
||||
}" x-modelable="value" {{ $attributes->whereStartsWith('x-model') }}
|
||||
{{ $attributes->whereStartsWith('x-effect') }}
|
||||
@click.outside="open = false" @keydown.escape="open = false">
|
||||
<button id="{{ $triggerId }}" type="button" class="listbox-trigger" @click="open = !open"
|
||||
@click.outside="open = false" @keydown.escape="open = false" @resize.window="open && positionPanel()">
|
||||
<button x-ref="trigger" id="{{ $triggerId }}" type="button" class="listbox-trigger" @click="toggle()"
|
||||
@disabled($disabled) {{ $attributes->whereStartsWith('x-bind:disabled') }} aria-haspopup="listbox"
|
||||
:aria-expanded="open" @if ($tooltip) :title="current" @endif>
|
||||
<span class="listbox-trigger-label" x-text="current"></span>
|
||||
@@ -66,23 +100,48 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m8 9 4-4 4 4m0 6-4 4-4-4" />
|
||||
</svg>
|
||||
</button>
|
||||
<div class="listbox-panel" x-show="open" x-cloak role="listbox">
|
||||
<div x-show="options.length === 0"
|
||||
class="px-3 py-2 text-[13px] text-neutral-500 dark:text-fg-dim">
|
||||
{{ $emptyText }}
|
||||
</div>
|
||||
<template x-for="option in options" :key="String(option.value)">
|
||||
<button type="button" class="listbox-option" role="option"
|
||||
:class="{ 'listbox-option-disabled': option.disabled }"
|
||||
:aria-selected="String(option.value) === String(value)" @click="choose(option)">
|
||||
<span class="truncate" x-text="option.label"></span>
|
||||
<svg x-show="String(option.value) === String(value)" xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor"
|
||||
class="size-3.5 shrink-0">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
|
||||
</svg>
|
||||
</button>
|
||||
@if ($portal)
|
||||
<template x-teleport="body">
|
||||
<div id="{{ $panelId }}" class="listbox-panel" style="position: fixed; z-index: 9999" x-show="open"
|
||||
x-cloak :style="{ visibility: positioned ? 'visible' : 'hidden' }"
|
||||
x-effect="if (open) requestAnimationFrame(() => positionPanel($el))" role="listbox">
|
||||
<div x-show="options.length === 0"
|
||||
class="px-3 py-2 text-[13px] text-neutral-500 dark:text-fg-dim">
|
||||
{{ $emptyText }}
|
||||
</div>
|
||||
<template x-for="option in options" :key="String(option.value)">
|
||||
<button type="button" class="listbox-option" role="option"
|
||||
:class="{ 'listbox-option-disabled': option.disabled }"
|
||||
:aria-selected="String(option.value) === String(value)" @click="choose(option)">
|
||||
<span class="truncate" x-text="option.label"></span>
|
||||
<svg x-show="String(option.value) === String(value)" xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor"
|
||||
class="size-3.5 shrink-0">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
|
||||
</svg>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@else
|
||||
<div x-ref="panel" class="listbox-panel" x-show="open" x-cloak role="listbox">
|
||||
<div x-show="options.length === 0"
|
||||
class="px-3 py-2 text-[13px] text-neutral-500 dark:text-fg-dim">
|
||||
{{ $emptyText }}
|
||||
</div>
|
||||
<template x-for="option in options" :key="String(option.value)">
|
||||
<button type="button" class="listbox-option" role="option"
|
||||
:class="{ 'listbox-option-disabled': option.disabled }"
|
||||
:aria-selected="String(option.value) === String(value)" @click="choose(option)">
|
||||
<span class="truncate" x-text="option.label"></span>
|
||||
<svg x-show="String(option.value) === String(value)" xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor"
|
||||
class="size-3.5 shrink-0">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
|
||||
</svg>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@@ -13,7 +13,7 @@
|
||||
['label' => 'Environment Variables', 'route' => 'project.service.environment-variables', 'icon' => 'variables', 'hasWarning' => ! $service->isDeployable],
|
||||
['label' => 'Persistent Storage', 'route' => 'project.service.storages', 'icon' => 'storages'],
|
||||
['label' => 'Backups', 'route' => 'project.service.volume-backups.index', 'icon' => 'database'],
|
||||
['label' => 'Runtime', 'route' => 'project.service.logs', 'icon' => 'unordered-list', 'navigate' => false],
|
||||
['label' => 'Runtime Logs', 'route' => 'project.service.logs', 'icon' => 'unordered-list', 'navigate' => false],
|
||||
['label' => 'Terminal', 'route' => 'project.service.command', 'icon' => 'browser-terminal', 'navigate' => false, 'visible' => auth()->user()?->can('canAccessTerminal')],
|
||||
['label' => 'Scheduled Tasks', 'route' => 'project.service.scheduled-tasks.show', 'icon' => 'calendar'],
|
||||
['label' => 'Webhooks', 'route' => 'project.service.webhooks', 'icon' => 'notifications'],
|
||||
@@ -31,14 +31,17 @@
|
||||
]);
|
||||
|
||||
$menuGroups = [
|
||||
'Settings' => ['General', 'Domains', 'Environment Variables', 'Persistent Storage', 'Backups'],
|
||||
'Automation' => ['Scheduled Tasks', 'Webhooks'],
|
||||
'Logs' => ['Runtime'],
|
||||
'Operations' => ['Terminal', 'Resource Operations', 'Tags', 'Danger Zone'],
|
||||
'Settings' => ['General', 'Domains', 'Environment Variables', 'Persistent Storage'],
|
||||
'Observe & troubleshoot' => ['Runtime Logs', 'Terminal'],
|
||||
'Automation' => ['Scheduled Tasks', 'Webhooks', 'Backups'],
|
||||
'Operations' => ['Resource Operations', 'Tags', 'Danger Zone'],
|
||||
];
|
||||
|
||||
$groupedItems = collect($menuGroups)
|
||||
->map(fn (array $labels) => $configurationItems->whereIn('label', $labels)->values())
|
||||
->map(fn (array $labels) => collect($labels)
|
||||
->map(fn (string $label) => $configurationItems->firstWhere('label', $label))
|
||||
->filter()
|
||||
->values())
|
||||
->filter(fn ($items) => $items->isNotEmpty());
|
||||
@endphp
|
||||
|
||||
|
||||
@@ -14,15 +14,15 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex h-8 w-fit items-center rounded-lg border border-neutral-200 bg-white p-0.5 dark:border-white/[0.08] dark:bg-white/[0.035]">
|
||||
class="flex h-9 w-fit items-center rounded-lg border border-neutral-200 bg-white p-0.5 dark:border-white/[0.08] dark:bg-white/[0.035]">
|
||||
<button type="button" @click="viewMode = 'list'; localStorage.setItem('{{ $storageKey }}', 'list')"
|
||||
class="flex size-6.5 items-center justify-center rounded-md transition-colors"
|
||||
class="flex size-7.5 items-center justify-center rounded-md transition-colors"
|
||||
:class="viewMode === 'list' ? 'control-selected' : 'text-neutral-400 hover:bg-neutral-100 hover:text-black dark:text-fg-faint dark:hover:bg-white/[0.06] dark:hover:text-fg'"
|
||||
aria-label="List view" title="List view">
|
||||
<x-reicon name="unordered-list" class="size-3.5" />
|
||||
</button>
|
||||
<button type="button" @click="viewMode = 'grid'; localStorage.setItem('{{ $storageKey }}', 'grid')"
|
||||
class="flex size-6.5 items-center justify-center rounded-md transition-colors"
|
||||
class="flex size-7.5 items-center justify-center rounded-md transition-colors"
|
||||
:class="viewMode === 'grid' ? 'control-selected' : 'text-neutral-400 hover:bg-neutral-100 hover:text-black dark:text-fg-faint dark:hover:bg-white/[0.06] dark:hover:text-fg'"
|
||||
aria-label="Grid view" title="Grid view">
|
||||
<x-reicon name="grid" class="size-3.5" />
|
||||
|
||||
Reference in new issue
Block a user