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:
Andras Bacsai committed 2026-08-05 17:56:59 +02:00
1 parent 2291c3fa1c
commit 5de6f97256
85 files changed
+2087 -981

No files matched your search

@@ -20,8 +20,7 @@
@endif
<button type="button"
class="ml-1 inline-flex items-center gap-1 font-semibold text-coollabs transition-colors hover:text-coollabs-100 dark:text-warning dark:hover:text-warning/80"
x-on:click="$wire.refreshConfigurationChanges().then(() => configurationDiffModalOpen = true)"
wire:loading.attr="disabled" wire:target="refreshConfigurationChanges">
x-on:click="configurationDiffModalOpen = true">
View changes
<x-reicon name="arrow-right" class="size-3" />
</button>
@@ -1,6 +1,6 @@
<div>
<x-slot:title>
{{ data_get_str($resource, 'name')->limit(10) }} > Console | Coolify
{{ data_get_str($resource, 'name')->limit(10) }} > Terminal | Coolify
</x-slot>
@if ($type === 'application')
@@ -11,7 +11,7 @@
<livewire:project.database.heading :database="$resource" />
@elseif ($type === 'service')
<livewire:project.shared.configuration-checker :resource="$resource" />
<livewire:project.service.heading :service="$resource" :parameters="$parameters" title="Console" />
<livewire:project.service.heading :service="$resource" :parameters="$parameters" title="Terminal" />
@else
<livewire:server.navbar :server="$servers->first()" />
@endif
@@ -20,6 +20,7 @@
$consoleUnavailable = ($type === 'server' && (! $servers->first()->isTerminalEnabled() || ! $servers->first()->isFunctional()))
|| ($type !== 'server' && $containers->isEmpty());
$consoleThemes = [
['key' => 'system', 'name' => 'System', 'background' => 'linear-gradient(135deg, #ffffff 0 50%, #121214 50% 100%)', 'accent' => '#8C8E9C'],
['key' => 'shadows-midnight', 'name' => 'Midnight', 'background' => 'linear-gradient(135deg, #2a3b4c, rgba(42, 59, 76, 0.4))', 'accent' => '#6d7a7c'],
['key' => 'shadows-golden-hour', 'name' => 'Golden Hour', 'background' => 'linear-gradient(135deg, #d58a42, rgba(213, 138, 66, 0.4))', 'accent' => '#bf8c3c'],
['key' => 'shadows-cosmic-purple', 'name' => 'Cosmic Purple', 'background' => 'linear-gradient(135deg, #5d3e66, rgba(93, 62, 102, 0.4))', 'accent' => '#A76DBE'],
@@ -40,14 +41,27 @@
])->values();
@endphp
@if (in_array($type, ['application', 'database', 'service'], true))
<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">
@if ($type === 'application')
<x-application.configuration-sidebar :application="$resource" current-route="project.application.command" />
@elseif ($type === 'database')
<x-database.configuration-sidebar :database="$resource" current-route="project.database.command" />
@else
<x-service.configuration-sidebar :service="$resource" current-route="project.service.command" />
@endif
<div class="min-w-0">
@endif
@if ($consoleUnavailable)
<section class="mt-8 w-full max-w-[1180px] xl:mt-0">
@if ($type === 'server')
<x-empty size="lg" title="Console unavailable"
<x-empty size="lg" title="Terminal unavailable"
description="This server is not functional or terminal access is disabled."
icon-name="browser-terminal" />
@else
<x-empty size="lg" title="Console unavailable"
<x-empty size="lg" title="Terminal unavailable"
description="No containers are running, or terminal access is disabled on the destination server."
icon-name="browser-terminal" />
@endif
@@ -56,14 +70,14 @@
<section class="mt-8 mb-0! h-[calc(100dvh-8rem)] min-h-[32rem] w-full max-w-[1180px] xl:mt-0"
x-data="{
themeKeys: @js($consoleThemeKeys),
consoleTheme: 'shadows-cosmic-purple',
consoleTheme: 'system',
themeOpen: false,
containerOpen: false,
selectedContainer: @entangle('selected_container').live,
containerOptions: @js($containerOptions),
init() {
const savedTheme = localStorage.getItem('coolify-console-theme');
this.consoleTheme = this.themeKeys.includes(savedTheme) ? savedTheme : 'shadows-cosmic-purple';
this.consoleTheme = this.themeKeys.includes(savedTheme) ? savedTheme : 'system';
localStorage.setItem('coolify-console-theme', this.consoleTheme);
},
setTheme(theme) {
@@ -170,10 +184,10 @@
</button>
<div x-cloak x-show="themeOpen" x-transition.origin.top.right
class="absolute top-7 right-0 z-50 max-h-80 w-56 overflow-y-auto rounded-lg border border-white/[0.1] bg-[#111113] p-1 shadow-[0_18px_50px_rgba(0,0,0,0.55)]">
class="console-theme-selector absolute top-7 right-0 z-50 max-h-80 w-56 overflow-y-auto rounded-lg border border-neutral-200 bg-white p-1 shadow-[0_18px_50px_rgba(0,0,0,0.18)] dark:border-white/[0.1] dark:bg-[#111113] dark:shadow-[0_18px_50px_rgba(0,0,0,0.55)]">
@foreach ($consoleThemes as $theme)
<button type="button"
class="flex h-8 w-full items-center gap-2 rounded-md px-2 text-left text-[11px] text-white/65 transition-colors hover:bg-white/[0.07] hover:text-white"
class="flex h-8 w-full items-center gap-2 rounded-md px-2 text-left text-[11px] text-neutral-600 transition-colors hover:bg-neutral-100 hover:text-neutral-950 dark:text-white/65 dark:hover:bg-white/[0.07] dark:hover:text-white"
@click="setTheme('{{ $theme['key'] }}')">
<span class="h-3 w-5 rounded-full border border-white/10"
style="background: {{ $theme['background'] }}"></span>
@@ -196,4 +210,9 @@
</div>
</section>
@endif
@if (in_array($type, ['application', 'database', 'service'], true))
</div>
</div>
</section>
@endif
</div>
@@ -8,8 +8,6 @@
<x-application.settings-section id="healthcheck-configuration-section" title="Healthcheck"
helper="Define how Coolify determines whether this application is ready to receive traffic.">
<x-slot:actions>
<x-status-badge :status="$healthCheckEnabled ? 'Enabled' : 'Disabled'"
:type="$healthCheckEnabled ? 'success' : 'neutral'" />
@if (!$healthCheckEnabled)
<x-modal-confirmation :disabled="!auth()->user()->can('update', $resource)"
:authDisabled="!auth()->user()->can('update', $resource)"
@@ -24,15 +24,28 @@
);
@endphp
@if (in_array($type, ['application', 'database', 'service'], true))
<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">
@if ($type === 'application')
<x-application.configuration-sidebar :application="$resource" current-route="project.application.logs" />
@elseif ($type === 'database')
<x-database.configuration-sidebar :database="$resource" current-route="project.database.logs" />
@else
<x-service.configuration-sidebar :service="$resource" current-route="project.service.logs" />
@endif
<div class="min-w-0">
@endif
<div wire:loading.flex wire:target="loadAllContainers"
class="mt-4 hidden min-h-32 w-full items-center justify-center lg:mt-3">
class="loading-state-card mt-4 hidden min-h-40 w-full items-center justify-center lg:mt-3">
<x-loading text="Loading containers" />
</div>
<div class="mt-4 w-full lg:mt-3" x-init="if (! $wire.containersLoaded) { $wire.loadAllContainers() }"
wire:loading.remove wire:target="loadAllContainers">
@if (! $containersLoaded)
<div class="flex min-h-32 items-center justify-center">
<div class="loading-state-card flex min-h-40 w-full items-center justify-center">
<x-loading text="Loading containers" />
</div>
@elseif ($logsUnavailable)
@@ -75,4 +88,9 @@
</div>
@endif
</div>
@if (in_array($type, ['application', 'database', 'service'], true))
</div>
</div>
</section>
@endif
</div>
@@ -1,10 +1,14 @@
<div x-data="{
selectedCloneServer: null,
selectedCloneDestination: null,
selectedCloneProject: null,
selectedCloneEnvironment: null,
selectedMoveProject: null,
selectedMoveEnvironment: null,
currentProjectId: {{ $resource->environment->project->id }},
currentEnvironmentId: {{ $resource->environment->id }},
currentServerId: @js($resource->destination->server->id),
currentDestinationUuid: @js($resource->destination->uuid),
servers: @js(
$servers->map(
fn ($server) => [
@@ -38,10 +42,15 @@
)->values(),
),
get availableDestinations() {
if (!this.selectedCloneServer) return [];
if (this.selectedCloneServer === null || this.selectedCloneServer === '') return [];
const server = this.servers.find(server => server.id == this.selectedCloneServer);
return server ? server.destinations : [];
},
get availableCloneEnvironments() {
if (this.selectedCloneProject === null || this.selectedCloneProject === '') return [];
const project = this.projects.find(project => project.id == this.selectedCloneProject);
return project ? project.environments : [];
},
get availableEnvironments() {
if (!this.selectedMoveProject) return [];
const project = this.projects.find(project => project.id == this.selectedMoveProject);
@@ -60,7 +69,7 @@
return [
...this.servers.map(server => ({
value: server.id,
label: `${server.name} (${server.ip})`,
label: `${server.name} (${server.ip})${server.id == this.currentServerId ? ' (current)' : ''}`,
})),
...@js(
$buildServers->map(
@@ -76,7 +85,19 @@
get cloneDestinationOptions() {
return this.availableDestinations.map(destination => ({
value: destination.uuid,
label: destination.name,
label: destination.name + (destination.uuid == this.currentDestinationUuid ? ' (current)' : ''),
}));
},
get cloneProjectOptions() {
return this.projects.map(project => ({
value: project.id,
label: project.name + (project.id == this.currentProjectId ? ' (current)' : ''),
}));
},
get cloneEnvironmentOptions() {
return this.availableCloneEnvironments.map(environment => ({
value: environment.id,
label: environment.name + (environment.id == this.currentEnvironmentId ? ' (current)' : ''),
}));
},
get moveProjectOptions() {
@@ -92,12 +113,17 @@
}));
}
}" x-init="
selectedCloneServer = null;
selectedCloneDestination = null;
selectedCloneProject = null;
selectedCloneEnvironment = null;
$watch('selectedCloneServer', () => selectedCloneDestination = null);
$watch('selectedCloneProject', () => selectedCloneEnvironment = null);
$watch('selectedMoveProject', () => selectedMoveEnvironment = null);
" class="flex flex-col gap-6">
@can('update', $resource)
<x-application.settings-section id="clone-resource-section" title="Clone resource"
helper="Duplicate this resource configuration onto another server and network destination.">
<x-application.settings-section id="clone-destination-section" title="Clone to another destination"
helper="Create the clone in the current environment on another server or network.">
<x-callout type="info" title="Configuration only">
Cloning copies settings, environment variables, and resource configuration. Stored files,
database records, and other persistent data are not copied.
@@ -110,7 +136,8 @@
<x-forms.listbox id="clone-resource-destination" label="Network destination" :wire="false"
x-model="selectedCloneDestination" x-effect="options = cloneDestinationOptions"
x-bind:disabled="!selectedCloneServer" placeholder="Choose a destination…"
x-bind:disabled="selectedCloneServer === null || selectedCloneServer === ''"
placeholder="Choose a destination…"
emptyText="No network destinations are available on this server." />
</div>
@@ -125,6 +152,32 @@
</div>
</x-application.settings-section>
<x-application.settings-section id="clone-environment-section" title="Clone to another environment"
helper="Create the clone in another project environment while keeping the current server and network.">
<div class="grid gap-4 md:grid-cols-2">
<x-forms.listbox id="clone-resource-project" label="Project" :wire="false"
x-model="selectedCloneProject" x-effect="options = cloneProjectOptions"
placeholder="Choose a project…" />
<x-forms.listbox id="clone-resource-environment" label="Environment" :wire="false"
x-model="selectedCloneEnvironment" x-effect="options = cloneEnvironmentOptions"
x-bind:disabled="!selectedCloneProject || availableCloneEnvironments.length === 0"
placeholder="Choose an environment…" />
</div>
<div x-show="selectedCloneEnvironment" x-cloak
class="mt-4 flex flex-col gap-3 border-t border-neutral-200 pt-4 sm:flex-row sm:items-center sm:justify-between dark:border-white/[0.07]">
<p class="text-[13px] text-neutral-500 dark:text-fg-dim">
Uses {{ data_get($resource, 'destination.server.name') }} ·
{{ data_get($resource, 'destination.network') }}.
</p>
<x-forms.button
@click="$wire.cloneTo(@js($resource->destination->uuid), selectedCloneEnvironment)">
Clone resource
</x-forms.button>
</div>
</x-application.settings-section>
<x-application.settings-section id="move-resource-section" title="Move resource"
helper="Transfer this resource to another project environment without changing the running deployment.">
@if ($projects->count() > 0)
@@ -13,7 +13,7 @@
@endcan
@endif
@can('update', $resource)
<x-forms.button type="button" wire:click="toggleEnabled">
<x-forms.button type="button" wire:click="toggleEnabled" :isHighlighted="!$isEnabled">
{{ $isEnabled ? 'Disable' : 'Enable' }}
</x-forms.button>
<x-modal-confirmation title="Delete scheduled task?" isErrorButton buttonTitle="Delete"
@@ -92,7 +92,7 @@
@endif
</div>
@if ($showActionsColumn)
@if ($showBackupAction)
<div
class="volumes-col-actions volumes-cell-actions flex flex-wrap items-center justify-end gap-1.5">
@if ($canUpdate)
@@ -170,7 +170,7 @@
Update
</x-forms.button>
@if ($showActionsColumn)
@if ($showBackupAction)
<x-modal-input title="Configure Volume Backup" :wireIgnore="false">
<x-slot:content>
<x-forms.button type="button" class="!px-2.5 !text-xs">Backup</x-forms.button>
@@ -185,6 +185,14 @@
wire:key="configure-service-volume-backup-{{ $id }}" />
@endif
</x-modal-input>
@elseif (method_exists($resource, 'isBackupSolutionAvailable') && $resource->isBackupSolutionAvailable())
<x-modal-input title="New Scheduled Backup" :wireIgnore="false">
<x-slot:content>
<x-forms.button type="button" class="!px-2.5 !text-xs">Backup</x-forms.button>
</x-slot:content>
<livewire:project.database.create-scheduled-backup :database="$resource"
wire:key="configure-database-backup-{{ $id }}" />
</x-modal-input>
@endif
<x-modal-confirmation title="Confirm persistent storage deletion?" isErrorButton
@@ -15,7 +15,7 @@
@if ($isApplicationConsole)
<div class="flex h-full min-h-[32rem] items-center justify-center">
<x-empty size="lg" title="Shell unavailable"
description="This container does not include Bash or sh. Install a supported shell to use the console."
description="This container does not include Bash or sh. Install a supported shell to use the terminal."
icon-name="browser-terminal" />
</div>
@else