241 lines
14 KiB
PHP
241 lines
14 KiB
PHP
<?php
|
||
|
||
use App\Models\Cluster;
|
||
use Illuminate\Support\Str;
|
||
use Livewire\Attributes\Layout;
|
||
use Livewire\Volt\Component;
|
||
|
||
new #[Layout('layouts.app')] class extends Component {
|
||
public int $step = 1;
|
||
|
||
// Step 1 — basics
|
||
public string $name = '';
|
||
public string $env = 'prod';
|
||
public string $description = '';
|
||
|
||
// Step 2 — region + size
|
||
public string $region = 'eu-central-1';
|
||
public string $size = 'medium';
|
||
public int $nodes = 2;
|
||
|
||
// Step 3 — WP options
|
||
public string $phpVersion = '8.3';
|
||
public bool $autoUpdates = true;
|
||
public bool $stagingEnv = true;
|
||
public bool $cdnEnabled = true;
|
||
public bool $hourlyBackups = true;
|
||
|
||
public function nextStep(): void
|
||
{
|
||
if ($this->step === 1) {
|
||
$this->validate([
|
||
'name' => ['required', 'string', 'min:3', 'max:64'],
|
||
'env' => ['required', 'in:prod,staging,edge,dev'],
|
||
]);
|
||
}
|
||
$this->step = min(4, $this->step + 1);
|
||
}
|
||
|
||
public function prevStep(): void
|
||
{
|
||
$this->step = max(1, $this->step - 1);
|
||
}
|
||
|
||
public function getSlugProperty(): string
|
||
{
|
||
return Str::slug($this->name) ?: 'neuer-cluster';
|
||
}
|
||
|
||
public function getMonthlyPriceProperty(): int
|
||
{
|
||
$base = match ($this->size) {
|
||
'small' => 4900,
|
||
'medium' => 9900,
|
||
'large' => 24900,
|
||
default => 9900,
|
||
};
|
||
return ($base * $this->nodes) / 100;
|
||
}
|
||
|
||
public function create(): void
|
||
{
|
||
Cluster::create([
|
||
'workspace_id' => auth()->user()?->current_workspace_id
|
||
?? \App\Models\Workspace::factory()->create()->id,
|
||
'name' => $this->name,
|
||
'slug' => $this->slug,
|
||
'region' => $this->region,
|
||
'env' => $this->env,
|
||
'monthly_price_cents' => $this->monthlyPrice * 100,
|
||
'node_count' => $this->nodes,
|
||
'backups_enabled' => $this->hourlyBackups,
|
||
]);
|
||
|
||
$this->redirect(route('servers.index'), navigate: true);
|
||
}
|
||
}; ?>
|
||
|
||
<div>
|
||
<div class="clu-topbar">
|
||
<x-clu.burger />
|
||
<div>
|
||
<h1 class="clu-page-title">Cluster anlegen</h1>
|
||
<div class="clu-page-sub">schritt {{ $step }} von 4</div>
|
||
</div>
|
||
<div class="spacer"></div>
|
||
<a href="{{ route('servers.index') }}" wire:navigate class="clu-ghost-btn">Abbrechen</a>
|
||
</div>
|
||
|
||
{{-- Stepper bar --}}
|
||
<div class="clu-stepper mb-[18px]" aria-label="Wizard schritte">
|
||
@foreach (['Basis', 'Region & Größe', 'WordPress', 'Review'] as $i => $label)
|
||
@php $stepNum = $i + 1; @endphp
|
||
<div class="clu-step @if($step === $stepNum) active @endif @if($step > $stepNum) done @endif">
|
||
<div class="clu-step-num">{{ $stepNum }}</div>
|
||
<div class="clu-step-label">{{ $label }}</div>
|
||
</div>
|
||
@if ($stepNum < 4)
|
||
<div class="clu-step-line"></div>
|
||
@endif
|
||
@endforeach
|
||
</div>
|
||
|
||
<div class="grid gap-[14px]" style="grid-template-columns:minmax(0,2fr) minmax(280px,1fr);">
|
||
<div class="clu-card">
|
||
@if ($step === 1)
|
||
<div class="clu-card-head"><h3>Basis</h3></div>
|
||
<div class="clu-card-body" style="display:flex;flex-direction:column;gap:14px;">
|
||
<div class="flex flex-col gap-[6px]">
|
||
<label class="text-[11.5px] text-(--color-muted) font-semibold">Cluster Name</label>
|
||
<div class="clu-input"><input wire:model.live="name" type="text" placeholder="z.B. Acme Cluster Prod" required /></div>
|
||
<x-input-error :messages="$errors->get('name')" class="mt-1" />
|
||
<div class="clu-slug-preview">URL-Slug: <b>{{ $this->slug }}</b></div>
|
||
</div>
|
||
<div class="flex flex-col gap-[6px]">
|
||
<label class="text-[11.5px] text-(--color-muted) font-semibold">Environment</label>
|
||
<div class="flex gap-[8px]">
|
||
@foreach (['prod' => 'Production', 'staging' => 'Staging', 'edge' => 'Edge', 'dev' => 'Development'] as $val => $lbl)
|
||
<button type="button" wire:click="$set('env', '{{ $val }}')"
|
||
class="clu-ghost-btn @if($env === $val) active @endif"
|
||
style="@if($env === $val) background:var(--color-accent-soft);border-color:var(--color-accent);color:var(--color-accent); @endif">
|
||
{{ $lbl }}
|
||
</button>
|
||
@endforeach
|
||
</div>
|
||
</div>
|
||
<div class="flex flex-col gap-[6px]">
|
||
<label class="text-[11.5px] text-(--color-muted) font-semibold">Beschreibung (optional)</label>
|
||
<div class="clu-input"><input wire:model="description" type="text" placeholder="kurz, was hier läuft" /></div>
|
||
</div>
|
||
</div>
|
||
@elseif ($step === 2)
|
||
<div class="clu-card-head"><h3>Region & Größe</h3></div>
|
||
<div class="clu-card-body" style="display:flex;flex-direction:column;gap:14px;">
|
||
<div class="flex flex-col gap-[6px]">
|
||
<label class="text-[11.5px] text-(--color-muted) font-semibold">Region</label>
|
||
<select wire:model="region" class="clu-input" style="appearance:auto;">
|
||
<option value="eu-central-1">eu-central-1 · 🇩🇪 Frankfurt</option>
|
||
<option value="eu-west-1">eu-west-1 · 🇮🇪 Ireland</option>
|
||
<option value="us-east-1">us-east-1 · 🇺🇸 Virginia</option>
|
||
<option value="ap-south-1">ap-south-1 · 🇮🇳 Mumbai</option>
|
||
</select>
|
||
</div>
|
||
<div class="flex flex-col gap-[6px]">
|
||
<label class="text-[11.5px] text-(--color-muted) font-semibold">Node-Größe</label>
|
||
<div class="grid grid-cols-3 gap-[8px]">
|
||
@foreach (['small' => ['4 vCPU · 8 GB', '€49/node'], 'medium' => ['8 vCPU · 32 GB', '€99/node'], 'large' => ['16 vCPU · 64 GB', '€249/node']] as $val => $spec)
|
||
<button type="button" wire:click="$set('size', '{{ $val }}')"
|
||
class="clu-ghost-btn @if($size === $val) active @endif"
|
||
style="flex-direction:column;align-items:flex-start;padding:14px;@if($size === $val) background:var(--color-accent-soft);border-color:var(--color-accent); @endif">
|
||
<span class="font-semibold text-[13px]">{{ ucfirst($val) }}</span>
|
||
<span class="text-[11px] text-(--color-muted) font-mono">{{ $spec[0] }}</span>
|
||
<span class="text-[11px] text-(--color-accent) font-mono mt-[4px]">{{ $spec[1] }}</span>
|
||
</button>
|
||
@endforeach
|
||
</div>
|
||
</div>
|
||
<div class="flex flex-col gap-[6px]">
|
||
<label class="text-[11.5px] text-(--color-muted) font-semibold">Anzahl Nodes: <b>{{ $nodes }}</b></label>
|
||
<input wire:model.live="nodes" type="range" min="1" max="10" class="w-full" />
|
||
<div class="text-[10.5px] text-(--color-muted) font-mono">1 – 10 nodes · später skalierbar</div>
|
||
</div>
|
||
</div>
|
||
@elseif ($step === 3)
|
||
<div class="clu-card-head"><h3>WordPress Defaults</h3></div>
|
||
<div class="clu-card-body" style="display:flex;flex-direction:column;gap:14px;">
|
||
<div class="flex flex-col gap-[6px]">
|
||
<label class="text-[11.5px] text-(--color-muted) font-semibold">PHP Version</label>
|
||
<select wire:model="phpVersion" class="clu-input" style="appearance:auto;">
|
||
<option value="8.1">PHP 8.1</option>
|
||
<option value="8.2">PHP 8.2</option>
|
||
<option value="8.3">PHP 8.3 (empfohlen)</option>
|
||
<option value="8.4">PHP 8.4 (beta)</option>
|
||
</select>
|
||
</div>
|
||
<label class="clu-checkbox" style="justify-content:space-between;width:100%;">
|
||
<span>Auto-Updates (Minor + Security)</span>
|
||
<input type="checkbox" wire:model.live="autoUpdates" />
|
||
<span class="box"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5"><path d="M5 12l5 5L20 7"/></svg></span>
|
||
</label>
|
||
<label class="clu-checkbox" style="justify-content:space-between;width:100%;">
|
||
<span>Staging Environment</span>
|
||
<input type="checkbox" wire:model.live="stagingEnv" />
|
||
<span class="box"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5"><path d="M5 12l5 5L20 7"/></svg></span>
|
||
</label>
|
||
<label class="clu-checkbox" style="justify-content:space-between;width:100%;">
|
||
<span>CDN (Cloudflare)</span>
|
||
<input type="checkbox" wire:model.live="cdnEnabled" />
|
||
<span class="box"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5"><path d="M5 12l5 5L20 7"/></svg></span>
|
||
</label>
|
||
<label class="clu-checkbox" style="justify-content:space-between;width:100%;">
|
||
<span>Stündliche Backups</span>
|
||
<input type="checkbox" wire:model.live="hourlyBackups" />
|
||
<span class="box"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5"><path d="M5 12l5 5L20 7"/></svg></span>
|
||
</label>
|
||
</div>
|
||
@elseif ($step === 4)
|
||
<div class="clu-card-head"><h3>Review</h3></div>
|
||
<div class="clu-card-body" style="display:flex;flex-direction:column;gap:10px;">
|
||
<div class="flex justify-between"><span class="text-(--color-muted)">Name</span><span class="font-semibold">{{ $name }}</span></div>
|
||
<div class="flex justify-between"><span class="text-(--color-muted)">Environment</span><span class="clu-pill accent">{{ strtoupper($env) }}</span></div>
|
||
<div class="flex justify-between"><span class="text-(--color-muted)">Region</span><span class="font-mono">{{ $region }}</span></div>
|
||
<div class="flex justify-between"><span class="text-(--color-muted)">Größe</span><span>{{ ucfirst($size) }} × {{ $nodes }}</span></div>
|
||
<div class="flex justify-between"><span class="text-(--color-muted)">PHP</span><span class="font-mono">{{ $phpVersion }}</span></div>
|
||
<div class="flex justify-between"><span class="text-(--color-muted)">Auto-Updates</span><span>{{ $autoUpdates ? 'an' : 'aus' }}</span></div>
|
||
<div class="flex justify-between"><span class="text-(--color-muted)">Staging</span><span>{{ $stagingEnv ? 'an' : 'aus' }}</span></div>
|
||
<div class="flex justify-between"><span class="text-(--color-muted)">CDN</span><span>{{ $cdnEnabled ? 'an' : 'aus' }}</span></div>
|
||
<div class="flex justify-between"><span class="text-(--color-muted)">Hourly Backups</span><span>{{ $hourlyBackups ? 'an' : 'aus' }}</span></div>
|
||
</div>
|
||
@endif
|
||
|
||
{{-- Wizard nav --}}
|
||
<div class="flex justify-between gap-[8px] p-[14px] border-t border-(--color-hairline)">
|
||
<button type="button" class="clu-ghost-btn" wire:click="prevStep" @if($step === 1) disabled @endif>← Zurück</button>
|
||
@if ($step < 4)
|
||
<button type="button" class="clu-btn-primary" wire:click="nextStep" style="height:36px;padding:0 16px;">Weiter →</button>
|
||
@else
|
||
<button type="button" class="clu-btn-primary" wire:click="create" style="height:36px;padding:0 16px;">Cluster erstellen ✓</button>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
|
||
{{-- Sticky summary --}}
|
||
<div class="clu-card" style="height:fit-content;position:sticky;top:90px;">
|
||
<div class="clu-card-head"><h3>Zusammenfassung</h3></div>
|
||
<div class="clu-card-body" style="display:flex;flex-direction:column;gap:10px;">
|
||
<div class="flex justify-between text-[12.5px]"><span class="text-(--color-muted)">Name</span><span class="font-semibold">{{ $name ?: '—' }}</span></div>
|
||
<div class="flex justify-between text-[12.5px]"><span class="text-(--color-muted)">Env</span><span class="font-mono">{{ $env }}</span></div>
|
||
<div class="flex justify-between text-[12.5px]"><span class="text-(--color-muted)">Region</span><span class="font-mono">{{ $region }}</span></div>
|
||
<div class="flex justify-between text-[12.5px]"><span class="text-(--color-muted)">Size</span><span>{{ ucfirst($size) }} × {{ $nodes }}</span></div>
|
||
<div class="flex justify-between text-[12.5px]"><span class="text-(--color-muted)">Backups</span><span>{{ $hourlyBackups ? 'hourly' : 'daily' }}</span></div>
|
||
<div class="border-t border-(--color-hairline) pt-[10px] mt-[6px]">
|
||
<div class="flex justify-between items-center">
|
||
<span class="text-[12px] text-(--color-muted)">monatlich</span>
|
||
<span class="font-mono font-semibold text-[18px]">€{{ number_format($this->monthlyPrice, 0, ',', '.') }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|