167 lines
10 KiB
PHP
167 lines
10 KiB
PHP
<div class="space-y-6">
|
||
<div class="animate-rise">
|
||
<h1 class="text-2xl font-bold tracking-tight text-ink">{{ __('capacity.title') }}</h1>
|
||
<p class="mt-1 max-w-[70ch] text-sm text-muted">{{ __('capacity.subtitle') }}</p>
|
||
</div>
|
||
|
||
{{-- ── The queue ────────────────────────────────────────────────────── --}}
|
||
<x-ui.card class="animate-rise [animation-delay:60ms]">
|
||
<div class="flex flex-wrap items-center gap-x-4 gap-y-2">
|
||
<h2 class="font-semibold text-ink">{{ __('capacity.queue_title') }}</h2>
|
||
@if ($demand['count'] > 0)
|
||
<x-ui.badge status="warning">{{ trans_choice('capacity.queue_count', $demand['count'], ['count' => $demand['count']]) }}</x-ui.badge>
|
||
@else
|
||
<x-ui.badge status="active">{{ __('capacity.queue_empty_badge') }}</x-ui.badge>
|
||
@endif
|
||
<x-ui.button wire:click="recheck" variant="secondary" size="sm" class="ml-auto"
|
||
wire:loading.attr="disabled" wire:target="recheck">
|
||
<x-ui.icon name="refresh" class="size-4" />{{ __('capacity.recheck') }}
|
||
</x-ui.button>
|
||
</div>
|
||
|
||
@if ($demand['count'] === 0)
|
||
<p class="mt-4 text-sm text-muted">{{ __('capacity.queue_empty') }}</p>
|
||
@else
|
||
<table class="mt-5 w-full text-left text-sm">
|
||
<thead>
|
||
<tr class="border-b border-line">
|
||
<th class="lbl pb-2 font-normal">{{ __('capacity.col_customer') }}</th>
|
||
<th class="lbl pb-2 font-normal">{{ __('capacity.col_plan') }}</th>
|
||
<th class="lbl pb-2 text-right font-normal">{{ __('capacity.col_needs') }}</th>
|
||
<th class="lbl pb-2 font-normal">{{ __('capacity.col_datacenter') }}</th>
|
||
<th class="lbl pb-2 text-right font-normal">{{ __('capacity.col_waiting') }}</th>
|
||
<th class="lbl pb-2 font-normal">{{ __('capacity.col_target') }}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@foreach ($parked as $entry)
|
||
@php
|
||
// Only hosts in the order's own datacenter: placement
|
||
// is a per-datacenter question, and the step refuses a
|
||
// pin from anywhere else. Offering one here would be
|
||
// offering a choice that silently does nothing.
|
||
$choices = $hosts->where('datacenter', $entry['datacenter']);
|
||
$pinnedHost = $entry['pinned_host_id'] === null
|
||
? null
|
||
: $choices->firstWhere('id', $entry['pinned_host_id']);
|
||
@endphp
|
||
<tr wire:key="parked-{{ $entry['run']->id }}" class="border-b border-line last:border-0">
|
||
<td class="py-3 font-medium text-ink">{{ $entry['order']->customer?->name ?? '—' }}</td>
|
||
<td class="py-3 text-body">{{ $entry['name'] }}</td>
|
||
<td class="py-3 text-right font-mono tabular-nums text-ink">{{ $entry['needs'] }} GB</td>
|
||
<td class="py-3 font-mono text-xs text-muted">{{ $entry['datacenter'] }}</td>
|
||
<td class="py-3 text-right text-xs text-muted">{{ $entry['waiting_since']->local()->diffForHumans() }}</td>
|
||
{{-- One select, one value, no height change: the row
|
||
stays a row (R20's exception), and the decision
|
||
is the operator's — the placement rule only
|
||
takes over again when nothing is chosen. --}}
|
||
<td class="py-3">
|
||
<select wire:change="pin({{ $entry['run']->id }}, $event.target.value)"
|
||
class="rounded-md border border-line-strong bg-surface px-2 py-1 text-xs text-body">
|
||
<option value="">{{ __('capacity.target_auto') }}</option>
|
||
@foreach ($choices as $host)
|
||
<option value="{{ $host->id }}" @selected($entry['pinned_host_id'] === $host->id)>
|
||
{{ $host->name }} · {{ $host->availableGb() }} GB
|
||
</option>
|
||
@endforeach
|
||
</select>
|
||
@if ($pinnedHost !== null && ! $pinnedHost->canTake($entry['needs']))
|
||
{{-- A pin is honoured or it waits — never
|
||
quietly redirected. So say so, or the row
|
||
sits there looking like every other one
|
||
while it is the choice, not the estate,
|
||
that is holding it up. --}}
|
||
<p class="mt-1 flex items-center gap-1.5 text-xs text-warning">
|
||
<x-ui.icon name="alert-triangle" class="size-3.5 shrink-0" />{{ __('capacity.target_too_small') }}
|
||
</p>
|
||
@endif
|
||
</td>
|
||
</tr>
|
||
@endforeach
|
||
</tbody>
|
||
</table>
|
||
|
||
{{-- What to buy, in numbers. The largest single package decides the
|
||
MINIMUM machine — an instance lives on one host, so a server
|
||
that cannot take the biggest one leaves it parked however much
|
||
total space it has. --}}
|
||
<div class="mt-6 rounded-lg border border-accent-border bg-accent-subtle p-5">
|
||
<p class="lbl !text-accent-text">{{ __('capacity.needed_title') }}</p>
|
||
<p class="mt-3 text-sm leading-relaxed text-body">
|
||
{{ __('capacity.needed_body', [
|
||
'mix' => collect($demand['by_plan'])->map(fn ($n, $plan) => $n.'× '.$plan)->join(', '),
|
||
'largest' => $demand['largest'],
|
||
'total' => $demand['total'],
|
||
]) }}
|
||
</p>
|
||
</div>
|
||
@endif
|
||
</x-ui.card>
|
||
|
||
{{-- ── What the estate can still take ───────────────────────────────── --}}
|
||
<x-ui.card :title="__('capacity.hosts_title')" class="animate-rise [animation-delay:120ms]">
|
||
@if ($hosts->isEmpty())
|
||
<p class="text-sm text-muted">{{ __('capacity.hosts_empty') }}</p>
|
||
@else
|
||
<table class="w-full text-left text-sm">
|
||
<tbody>
|
||
@foreach ($hosts as $host)
|
||
<tr class="border-b border-line last:border-0">
|
||
<td class="py-3 font-medium text-ink">{{ $host->name }}</td>
|
||
<td class="py-3 font-mono text-xs text-muted">{{ $host->datacenter }}</td>
|
||
<td class="py-3 text-right font-mono tabular-nums text-body">
|
||
{{ $host->availableGb() }} / {{ $host->freeGb() }} GB {{ __('capacity.free') }}
|
||
</td>
|
||
</tr>
|
||
@endforeach
|
||
</tbody>
|
||
</table>
|
||
@endif
|
||
|
||
@if ($unplaceable !== [])
|
||
<x-ui.alert variant="warning" class="mt-5">
|
||
{{ __('capacity.unsellable', [
|
||
'plans' => collect($unplaceable)->pluck('name')->join(', '),
|
||
]) }}
|
||
</x-ui.alert>
|
||
@endif
|
||
</x-ui.card>
|
||
|
||
{{-- ── What such a machine costs today ──────────────────────────────── --}}
|
||
@if ($demand['count'] > 0)
|
||
<x-ui.card :title="__('capacity.market_title')" class="animate-rise [animation-delay:180ms]">
|
||
<p class="text-sm leading-relaxed text-muted">{{ __('capacity.market_body', ['gb' => $demand['largest']]) }}</p>
|
||
|
||
@if ($offers === [])
|
||
<p class="mt-4 text-sm text-muted">{{ __('capacity.market_none') }}</p>
|
||
@else
|
||
<table class="mt-4 w-full text-left text-sm">
|
||
<thead>
|
||
<tr class="border-b border-line">
|
||
<th class="lbl pb-2 font-normal">CPU</th>
|
||
<th class="lbl pb-2 text-right font-normal">RAM</th>
|
||
<th class="lbl pb-2 font-normal">{{ __('capacity.col_disks') }}</th>
|
||
<th class="lbl pb-2 text-right font-normal">{{ __('capacity.col_usable') }}</th>
|
||
<th class="lbl pb-2 font-normal">{{ __('capacity.col_location') }}</th>
|
||
<th class="lbl pb-2 text-right font-normal">{{ __('capacity.col_price') }}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@foreach ($offers as $offer)
|
||
<tr class="border-b border-line last:border-0">
|
||
<td class="py-3 text-body">{{ $offer['cpu'] }}</td>
|
||
<td class="py-3 text-right font-mono tabular-nums text-body">{{ $offer['ram'] }} GB</td>
|
||
<td class="py-3 font-mono text-xs text-muted">{{ $offer['disks'] }}</td>
|
||
<td class="py-3 text-right font-mono tabular-nums text-ink">{{ number_format($offer['flash_gb'], 0, ',', '.') }} GB</td>
|
||
<td class="py-3 font-mono text-xs text-muted">{{ $offer['datacenter'] }}</td>
|
||
<td class="py-3 text-right font-mono tabular-nums font-semibold text-ink">{{ number_format($offer['price'], 2, ',', '.') }} €</td>
|
||
</tr>
|
||
@endforeach
|
||
</tbody>
|
||
</table>
|
||
<p class="mt-3 text-xs text-muted">{{ __('capacity.market_note') }}</p>
|
||
@endif
|
||
</x-ui.card>
|
||
@endif
|
||
</div>
|