feat: multi-server switcher + fleet context
- WithFleetContext concern: resolves the active server (session, default first online) + the fleet. - ServerSwitcher Livewire component in the persistent sidebar: dropdown of the fleet; selecting persists active_server_id in the session and re-navigates so panels reflect the choice. - Dashboard reacts: resource rings + panel subtitles follow the active server. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>feat/v1-foundation
parent
cd5f7c107e
commit
db0a1de92f
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Concerns;
|
||||
|
||||
use App\Models\Server;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Shared fleet context for pages: the active server (from session, default
|
||||
* first) plus the whole fleet. Real metrics come from the SSH layer later.
|
||||
*/
|
||||
trait WithFleetContext
|
||||
{
|
||||
public function fleet(): Collection
|
||||
{
|
||||
return Server::orderBy('name')->get();
|
||||
}
|
||||
|
||||
public function activeServer(): ?Server
|
||||
{
|
||||
$fleet = $this->fleet();
|
||||
|
||||
return $fleet->firstWhere('id', session('active_server_id'))
|
||||
?? $fleet->firstWhere('status', 'online')
|
||||
?? $fleet->first();
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Livewire\Concerns\WithFleetContext;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Attributes\Title;
|
||||
use Livewire\Component;
|
||||
|
|
@ -10,6 +11,8 @@ use Livewire\Component;
|
|||
#[Title('Clusev — Dashboard')]
|
||||
class Dashboard extends Component
|
||||
{
|
||||
use WithFleetContext;
|
||||
|
||||
/**
|
||||
* Mock fleet data. Replaced by the SSH layer (phpseclib) + Reverb later.
|
||||
* Shapes mirror what MetricsPoller/FleetService will return.
|
||||
|
|
@ -52,6 +55,6 @@ class Dashboard extends Component
|
|||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.dashboard');
|
||||
return view('livewire.dashboard', ['active' => $this->activeServer()]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Server;
|
||||
use Livewire\Component;
|
||||
|
||||
/**
|
||||
* Sidebar fleet switcher. Lives in the persistent layout, so it survives
|
||||
* wire:navigate. Selecting a server stores it in the session and reloads the
|
||||
* current page so every panel reflects the new active server.
|
||||
*/
|
||||
class ServerSwitcher extends Component
|
||||
{
|
||||
public function select(string $uuid): void
|
||||
{
|
||||
$server = Server::where('uuid', $uuid)->firstOrFail();
|
||||
|
||||
session(['active_server_id' => $server->id]);
|
||||
|
||||
$this->redirect(request()->header('Referer') ?: route('dashboard'), navigate: true);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$servers = Server::orderBy('name')->get();
|
||||
$active = $servers->firstWhere('id', session('active_server_id'))
|
||||
?? $servers->firstWhere('status', 'online')
|
||||
?? $servers->first();
|
||||
|
||||
return view('livewire.server-switcher', [
|
||||
'servers' => $servers,
|
||||
'active' => $active,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -16,17 +16,9 @@
|
|||
</button>
|
||||
</div>
|
||||
|
||||
{{-- Server switcher (mock) --}}
|
||||
{{-- Server switcher --}}
|
||||
<div class="shrink-0 border-b border-line p-3">
|
||||
<button type="button"
|
||||
class="flex min-h-11 w-full items-center gap-2.5 rounded-md border border-line bg-inset px-3 py-2 text-left transition-colors hover:border-line-strong">
|
||||
<x-status-dot status="online" :ping="true" />
|
||||
<span class="min-w-0 flex-1">
|
||||
<span class="block truncate text-sm text-ink">web-01</span>
|
||||
<span class="block truncate font-mono text-[11px] text-ink-3">10.10.90.136</span>
|
||||
</span>
|
||||
<x-icon name="switcher" class="h-4 w-4 shrink-0 text-ink-3" />
|
||||
</button>
|
||||
<livewire:server-switcher />
|
||||
</div>
|
||||
|
||||
{{-- Nav --}}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
|
||||
{{-- Live chart (2/3) + resource rings (1/3) --}}
|
||||
<div class="grid grid-cols-1 gap-3 lg:grid-cols-3">
|
||||
<x-panel title="Live-Auslastung" subtitle="web-01 · CPU %" class="lg:col-span-2">
|
||||
<x-panel title="Live-Auslastung" :subtitle="($active?->name ?? '—') . ' · CPU %'" class="lg:col-span-2">
|
||||
<x-slot:actions>
|
||||
<x-badge tone="cyan">Reverb</x-badge>
|
||||
</x-slot:actions>
|
||||
|
|
@ -57,11 +57,11 @@
|
|||
</div>
|
||||
</x-panel>
|
||||
|
||||
<x-panel title="Ressourcen" subtitle="web-01">
|
||||
<x-panel title="Ressourcen" :subtitle="$active?->name ?? '—'">
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<x-ring :value="$web['cpu']" label="CPU" :tone="$ringTone($web['cpu'])" />
|
||||
<x-ring :value="$web['mem']" label="RAM" :tone="$ringTone($web['mem'])" />
|
||||
<x-ring :value="$disk" label="Disk" :tone="$ringTone($disk)" />
|
||||
<x-ring :value="$active?->cpu ?? 0" label="CPU" :tone="$ringTone($active?->cpu ?? 0)" />
|
||||
<x-ring :value="$active?->mem ?? 0" label="RAM" :tone="$ringTone($active?->mem ?? 0)" />
|
||||
<x-ring :value="$active?->disk ?? 0" label="Disk" :tone="$ringTone($active?->disk ?? 0)" />
|
||||
</div>
|
||||
</x-panel>
|
||||
</div>
|
||||
|
|
@ -82,7 +82,7 @@
|
|||
</div>
|
||||
</x-panel>
|
||||
|
||||
<x-panel title="systemd-Dienste" subtitle="web-01" :padded="false">
|
||||
<x-panel title="systemd-Dienste" :subtitle="$active?->name" :padded="false">
|
||||
<div class="divide-y divide-line">
|
||||
@foreach ($services as $svc)
|
||||
<div class="flex items-center gap-3 px-4 py-2.5 sm:px-5">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
<div x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false" class="relative">
|
||||
<button type="button" @click="open = ! open"
|
||||
class="flex min-h-11 w-full items-center gap-2.5 rounded-md border border-line bg-inset px-3 py-2 text-left transition-colors hover:border-line-strong"
|
||||
:aria-expanded="open" aria-haspopup="listbox">
|
||||
@if ($active)
|
||||
<x-status-dot :status="$active->status" :ping="$active->status === 'online'" />
|
||||
<span class="min-w-0 flex-1">
|
||||
<span class="block truncate font-mono text-sm text-ink">{{ $active->name }}</span>
|
||||
<span class="block truncate font-mono text-[11px] text-ink-3">{{ $active->ip }}</span>
|
||||
</span>
|
||||
@else
|
||||
<span class="min-w-0 flex-1 text-sm text-ink-3">Kein Server</span>
|
||||
@endif
|
||||
<x-icon name="switcher" class="h-4 w-4 shrink-0 text-ink-3" />
|
||||
</button>
|
||||
|
||||
<div x-show="open" x-cloak x-transition.origin.top
|
||||
class="absolute inset-x-0 top-full z-50 mt-1 overflow-hidden rounded-md border border-line bg-raised shadow-pop"
|
||||
role="listbox">
|
||||
<div class="max-h-72 overflow-y-auto p-1">
|
||||
@foreach ($servers as $server)
|
||||
<button type="button" wire:click="select('{{ $server->uuid }}')" @click="open = false"
|
||||
@class([
|
||||
'flex min-h-11 w-full items-center gap-2.5 rounded-sm px-2.5 py-1.5 text-left transition-colors hover:bg-surface',
|
||||
'bg-accent/10' => $active && $server->id === $active->id,
|
||||
])
|
||||
role="option" :aria-selected="{{ $active && $server->id === $active->id ? 'true' : 'false' }}">
|
||||
<x-status-dot :status="$server->status" :ping="$server->status === 'online'" />
|
||||
<span class="min-w-0 flex-1">
|
||||
<span class="block truncate font-mono text-sm text-ink">{{ $server->name }}</span>
|
||||
<span class="block truncate font-mono text-[11px] text-ink-3">{{ $server->ip }} · {{ $server->os }}</span>
|
||||
</span>
|
||||
@if ($active && $server->id === $active->id)
|
||||
<x-icon name="activity" class="h-3.5 w-3.5 shrink-0 text-accent" />
|
||||
@endif
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Loading…
Reference in New Issue