mirror of
https://github.com/tiennm99/coolify.git
synced 2026-08-20 06:23:23 +00:00
Add keyboard-aware terminal controls, refine terminal layouts and navigation, surface proxy and Sentinel health warnings, and make avatar uploads update only after successful persistence.
194 lines
11 KiB
PHP
194 lines
11 KiB
PHP
<div class="application-settings-form w-full">
|
|
<x-slot:title>
|
|
Servers | Coolify
|
|
</x-slot>
|
|
|
|
<div class="mb-5 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
<h1 class="min-w-0 text-[24px]! leading-7! font-semibold! tracking-tight!">Servers</h1>
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
@if (isDev())
|
|
@can('create', App\Models\Server::class)
|
|
<a href="{{ route('server.transfer.import') }}" {{ wireNavigate() }}
|
|
class="button w-fit shrink-0 whitespace-nowrap">
|
|
<x-reicon name="upload" class="size-3.5" />
|
|
Import transfer
|
|
<x-status-badge label="Dev" />
|
|
</a>
|
|
@endcan
|
|
@endif
|
|
@can('createAnyResource')
|
|
<a href="{{ route('server.create') }}" {{ wireNavigate() }}
|
|
class="button w-fit shrink-0 whitespace-nowrap button-highlighted">
|
|
<x-reicon name="plus" class="size-3.5" />
|
|
New server
|
|
</a>
|
|
@endcan
|
|
</div>
|
|
</div>
|
|
|
|
@php
|
|
$serverRows = $servers->map(function ($server) {
|
|
$isTransferredAway = $server->isTransferredAway();
|
|
$isReady = $server->settings->is_reachable
|
|
&& $server->settings->is_usable
|
|
&& ! $server->settings->force_disabled
|
|
&& ! $isTransferredAway;
|
|
$proxyNeedsAttention = $isReady && $server->proxySet() && $server->proxy->status !== 'running';
|
|
$sentinelNeedsAttention = $isReady && $server->isSentinelEnabled() && ! $server->isSentinelLive();
|
|
|
|
$status = match (true) {
|
|
$isTransferredAway => 'Transferred away',
|
|
$server->settings->force_disabled => 'Disabled',
|
|
$proxyNeedsAttention || $sentinelNeedsAttention => 'Attention required',
|
|
$isReady => 'Ready',
|
|
default => 'Validation required',
|
|
};
|
|
|
|
$statusType = match (true) {
|
|
$proxyNeedsAttention || $sentinelNeedsAttention => 'warning',
|
|
$isReady => 'success',
|
|
$isTransferredAway || $server->settings->force_disabled => 'error',
|
|
default => 'error',
|
|
};
|
|
|
|
return [
|
|
'uuid' => $server->uuid,
|
|
'name' => $server->name,
|
|
'description' => $server->description ?: 'No description',
|
|
'href' => route('server.show', ['server_uuid' => $server->uuid]),
|
|
'status' => $status,
|
|
'statusType' => $statusType,
|
|
];
|
|
})->values();
|
|
@endphp
|
|
|
|
<div x-data="{
|
|
search: '',
|
|
viewMode: localStorage.getItem('coolify-servers-view') || 'table',
|
|
servers: @js($serverRows),
|
|
get filteredServers() {
|
|
const query = this.search.trim().toLowerCase();
|
|
if (!query) return this.servers;
|
|
return this.servers.filter(server =>
|
|
[server.name, server.description, server.status]
|
|
.some(value => String(value || '').toLowerCase().includes(query))
|
|
);
|
|
},
|
|
setViewMode(mode) {
|
|
this.viewMode = mode;
|
|
localStorage.setItem('coolify-servers-view', mode);
|
|
}
|
|
}">
|
|
@if ($servers->isEmpty())
|
|
<x-empty title="No servers yet"
|
|
description="Add a server to deploy applications, databases, and services."
|
|
icon-name="servers" />
|
|
@else
|
|
<div class="mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
<div class="relative w-full sm: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 x-model.debounce.150ms="search" type="search" placeholder="Search servers"
|
|
class="h-8! w-full rounded-lg! border-neutral-200! bg-white! py-0! pr-8! pl-8! text-[12px]! shadow-none! placeholder:text-neutral-400 focus:border-accent! focus:ring-0! dark:border-white/[0.08]! dark:bg-white/[0.035]! dark:text-fg! dark:placeholder:text-fg-faint">
|
|
<button x-cloak x-show="search" x-on:click="search = ''" type="button"
|
|
class="absolute top-1/2 right-2 flex size-5 -translate-y-1/2 items-center justify-center rounded text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-black dark:text-fg-faint dark:hover:bg-white/[0.07] dark:hover:text-fg"
|
|
aria-label="Clear search">
|
|
<x-reicon name="x" class="size-3" />
|
|
</button>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-3">
|
|
<span class="text-[11px] text-neutral-500 dark:text-fg-faint">
|
|
<span x-text="filteredServers.length"></span>
|
|
<span x-text="filteredServers.length === 1 ? 'server' : 'servers'"></span>
|
|
</span>
|
|
<div
|
|
class="flex h-9 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" x-on:click="setViewMode('table')"
|
|
class="flex size-7.5 items-center justify-center rounded-md transition-colors"
|
|
:class="viewMode === 'table'
|
|
? '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="Table view">
|
|
<x-reicon name="unordered-list" class="size-3.5" />
|
|
</button>
|
|
<button type="button" x-on:click="setViewMode('grid')"
|
|
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">
|
|
<x-reicon name="grid" class="size-3.5" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div x-cloak x-show="viewMode === 'grid'"
|
|
class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
|
<template x-for="server in filteredServers" :key="server.uuid">
|
|
<a :href="server.href" {{ wireNavigate() }}
|
|
class="group relative flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]">
|
|
<div class="flex items-start gap-3">
|
|
<div :title="server.status" :aria-label="`Server status: ${server.status}`"
|
|
class="flex size-8 shrink-0 items-center justify-center rounded-lg border bg-neutral-50 text-neutral-500 dark:bg-white/[0.04] dark:text-fg-dim"
|
|
:class="server.statusType === 'success' ? 'border-emerald-500/70' : server.statusType === 'warning' ? 'border-amber-500/70' : 'border-red-500/70'">
|
|
<x-reicon name="servers" class="size-4" />
|
|
</div>
|
|
<div class="min-w-0 flex-1">
|
|
<h2 class="truncate text-[13px]! leading-4! font-semibold! text-black dark:text-fg"
|
|
x-text="server.name"></h2>
|
|
<p class="mt-0.5 truncate text-[11px] text-neutral-500 dark:text-fg-faint"
|
|
x-text="server.description"></p>
|
|
</div>
|
|
</div>
|
|
</a>
|
|
</template>
|
|
</div>
|
|
|
|
<div x-show="viewMode === 'table'"
|
|
class="overflow-x-auto rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]">
|
|
<div
|
|
class="grid min-w-[480px] grid-cols-[minmax(0,1fr)_9.5rem] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
|
|
<div>Server</div>
|
|
<div>Status</div>
|
|
</div>
|
|
<template x-for="server in filteredServers" :key="server.uuid">
|
|
<a :href="server.href" {{ wireNavigate() }}
|
|
class="grid min-h-14 min-w-[480px] grid-cols-[minmax(0,1fr)_9.5rem] items-center border-b border-neutral-200 px-4 py-2.5 text-[12px] transition-colors last:border-b-0 hover:bg-neutral-50 hover:no-underline dark:border-white/[0.07] dark:hover:bg-white/[0.025]">
|
|
<div class="flex min-w-0 items-center gap-3">
|
|
<div :title="server.status" :aria-label="`Server status: ${server.status}`"
|
|
class="flex size-8 shrink-0 items-center justify-center rounded-lg border bg-neutral-50 text-neutral-500 dark:bg-white/[0.035] dark:text-fg-dim"
|
|
:class="server.statusType === 'success' ? 'border-emerald-500/70' : server.statusType === 'warning' ? 'border-amber-500/70' : 'border-red-500/70'">
|
|
<x-reicon name="servers" class="size-4" />
|
|
</div>
|
|
<div class="min-w-0">
|
|
<p class="truncate text-[13px] font-semibold text-black dark:text-fg"
|
|
x-text="server.name"></p>
|
|
<p class="truncate text-[11px] text-neutral-500 dark:text-fg-faint"
|
|
x-text="server.description"></p>
|
|
</div>
|
|
</div>
|
|
<div class="text-[11px] font-medium text-neutral-600 dark:text-fg-dim">
|
|
<span x-text="server.status"></span>
|
|
</div>
|
|
</a>
|
|
</template>
|
|
</div>
|
|
|
|
<div x-show="filteredServers.length === 0"
|
|
class="flex min-h-52 flex-col items-center justify-center rounded-xl border border-neutral-200 bg-white px-6 text-center dark:border-white/[0.08] dark:bg-white/[0.025]">
|
|
<x-reicon name="search" class="mb-3 size-6 text-neutral-300 dark:text-fg-faint" />
|
|
<p class="text-[13px] font-medium">No matching servers</p>
|
|
<p class="mt-1 text-[12px] text-neutral-500 dark:text-fg-dim">Try a different search.</p>
|
|
</div>
|
|
@endif
|
|
|
|
@isset($error)
|
|
<div class="mt-4 rounded-lg border border-red-200 bg-red-50 p-3 text-[12px] text-red-700 dark:border-red-500/20 dark:bg-red-500/[0.06] dark:text-red-300">
|
|
{{ $error }}
|
|
</div>
|
|
@endisset
|
|
</div>
|
|
</div>
|