feat(server): add dedicated server creation flow

This commit is contained in:
Andras Bacsai committed 2026-07-08 12:20:56 +02:00
1 parent 18f0f7f6be
commit 7236cb8228
31 files changed
+1158 -352

No files matched your search

+10 -1
View File
@@ -11,12 +11,21 @@ class Create extends Component
{
public $private_keys = [];
public ?string $selectedType = null;
public ?string $selectedTokenUuid = null;
public bool $limit_reached = false;
public bool $has_hetzner_tokens = false;
public function mount()
public function mount(?string $selectedType = null, ?string $selectedTokenUuid = null): void
{
$this->selectedType = in_array($selectedType, ['hetzner', 'vultr', 'digital-ocean', 'manual'], true)
? $selectedType
: null;
$this->selectedTokenUuid = $this->selectedType && $this->selectedType !== 'manual' ? $selectedTokenUuid : null;
$this->private_keys = PrivateKey::ownedByCurrentTeamCached();
if (! isCloud()) {
$this->limit_reached = false;
+55
View File
@@ -0,0 +1,55 @@
<?php
namespace App\Livewire\Server;
use App\Models\CloudProviderToken;
use Illuminate\Contracts\View\View;
use Livewire\Component;
class CreatePage extends Component
{
public ?string $type = null;
public ?string $token_uuid = null;
public string $title = 'New Server';
public ?string $tokenProvider = null;
public ?string $tokenProviderName = null;
public bool $hasProviderTokens = false;
public function mount(?string $type = null, ?string $token_uuid = null): void
{
$this->type = $type;
$this->token_uuid = $token_uuid;
$this->tokenProvider = match ($type) {
'hetzner' => 'hetzner',
'vultr' => 'vultr',
'digital-ocean' => 'digitalocean',
default => null,
};
$this->tokenProviderName = match ($type) {
'hetzner' => 'Hetzner',
'vultr' => 'Vultr',
'digital-ocean' => 'DigitalOcean',
default => null,
};
$this->hasProviderTokens = $this->tokenProvider
? CloudProviderToken::ownedByCurrentTeam()->where('provider', $this->tokenProvider)->exists()
: false;
$this->title = match ($type) {
'hetzner' => 'Hetzner',
'vultr' => 'Vultr',
'digital-ocean' => 'DigitalOcean',
'manual' => 'Manual',
default => 'New Server',
};
}
public function render(): View
{
return view('livewire.server.create-page');
}
}
+54 -8
View File
@@ -33,6 +33,8 @@ class ByDigitalOcean extends Component
public ?int $selected_token_id = null;
public ?string $selectedTokenUuid = null;
public array $regions = [];
public array $images = [];
@@ -74,11 +76,12 @@ class ByDigitalOcean extends Component
public bool $from_onboarding = false;
public function mount()
public function mount(?string $selectedTokenUuid = null)
{
try {
$this->authorize('viewAny', CloudProviderToken::class);
$this->loadTokens();
$this->selectTokenFromUrl($selectedTokenUuid);
$this->loadSavedCloudInitScripts();
$this->server_name = generate_random_name();
$this->private_keys = PrivateKey::ownedAndOnlySShKeys()->where('id', '!=', 0)->get();
@@ -86,6 +89,11 @@ class ByDigitalOcean extends Component
if ($this->private_keys->count() > 0) {
$this->private_key_id = $this->private_keys->first()->id;
}
if ($this->selectedTokenUuid) {
$this->current_step = 2;
$this->loading_data = true;
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -174,9 +182,27 @@ class ByDigitalOcean extends Component
];
}
public function selectToken(int $tokenId): void
public function selectToken(int $tokenId): mixed
{
$this->selected_token_id = $tokenId;
return $this->nextStep();
}
private function selectTokenFromUrl(?string $selectedTokenUuid): void
{
if (! $selectedTokenUuid) {
return;
}
$token = $this->available_tokens->firstWhere('uuid', $selectedTokenUuid);
if (! $token) {
return;
}
$this->selectedTokenUuid = $selectedTokenUuid;
$this->selected_token_id = $token->id;
}
private function getDigitalOceanToken(): string
@@ -197,26 +223,46 @@ class ByDigitalOcean extends Component
]);
try {
$digitalOceanToken = $this->getDigitalOceanToken();
if (! $this->selectedTokenUuid) {
$token = $this->available_tokens->firstWhere('id', $this->selected_token_id);
if (! $digitalOceanToken) {
return $this->dispatch('error', 'Please select a valid DigitalOcean token.');
if ($token) {
return $this->redirectRoute('server.create.token', [
'type' => 'digital-ocean',
'token_uuid' => $token->uuid,
], navigate: true);
}
}
$this->loadDigitalOceanData($digitalOceanToken);
$this->current_step = 2;
$this->loading_data = true;
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function previousStep(): void
public function previousStep(): mixed
{
if ($this->selectedTokenUuid) {
return $this->redirectRoute('server.create.type', ['type' => 'digital-ocean'], navigate: true);
}
$this->current_step = 1;
return null;
}
private function loadDigitalOceanData(string $token): void
public function loadDigitalOceanData(): void
{
$token = $this->getDigitalOceanToken();
if (! $token) {
$this->loading_data = false;
$this->dispatch('error', 'Please select a valid DigitalOcean token.');
return;
}
$this->loading_data = true;
try {
+52 -10
View File
@@ -37,6 +37,8 @@ class ByHetzner extends Component
// Step 1: Token selection
public ?int $selected_token_id = null;
public ?string $selectedTokenUuid = null;
// Step 2: Server configuration
public array $locations = [];
@@ -89,11 +91,12 @@ class ByHetzner extends Component
public bool $from_onboarding = false;
public function mount()
public function mount(?string $selectedTokenUuid = null)
{
try {
$this->authorize('viewAny', CloudProviderToken::class);
$this->loadTokens();
$this->selectTokenFromUrl($selectedTokenUuid);
$this->loadSavedCloudInitScripts();
$this->server_name = generate_random_name();
$this->private_keys = PrivateKey::ownedAndOnlySShKeys()->where('id', '!=', 0)->get();
@@ -101,6 +104,11 @@ class ByHetzner extends Component
if ($this->private_keys->count() > 0) {
$this->private_key_id = $this->private_keys->first()->id;
}
if ($this->selectedTokenUuid) {
$this->current_step = 2;
$this->loading_data = true;
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -207,9 +215,27 @@ class ByHetzner extends Component
];
}
public function selectToken(int $tokenId)
public function selectToken(int $tokenId): mixed
{
$this->selected_token_id = $tokenId;
return $this->nextStep();
}
private function selectTokenFromUrl(?string $selectedTokenUuid): void
{
if (! $selectedTokenUuid) {
return;
}
$token = $this->available_tokens->firstWhere('uuid', $selectedTokenUuid);
if (! $token) {
return;
}
$this->selectedTokenUuid = $selectedTokenUuid;
$this->selected_token_id = $token->id;
}
private function validateHetznerToken(string $token): bool
@@ -244,17 +270,20 @@ class ByHetzner extends Component
]);
try {
$hetznerToken = $this->getHetznerToken();
if (! $this->selectedTokenUuid) {
$token = $this->available_tokens->firstWhere('id', $this->selected_token_id);
if (! $hetznerToken) {
return $this->dispatch('error', 'Please select a valid Hetzner token.');
if ($token) {
return $this->redirectRoute('server.create.token', [
'type' => 'hetzner',
'token_uuid' => $token->uuid,
], navigate: true);
}
}
// Load Hetzner data
$this->loadHetznerData($hetznerToken);
// Move to step 2
// Move to step 2; provider data is loaded after initial render via wire:init.
$this->current_step = 2;
$this->loading_data = true;
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -262,11 +291,24 @@ class ByHetzner extends Component
public function previousStep()
{
if ($this->selectedTokenUuid) {
return $this->redirectRoute('server.create.type', ['type' => 'hetzner'], navigate: true);
}
$this->current_step = 1;
}
private function loadHetznerData(string $token)
public function loadHetznerData(): void
{
$token = $this->getHetznerToken();
if (! $token) {
$this->loading_data = false;
$this->dispatch('error', 'Please select a valid Hetzner token.');
return;
}
$this->loading_data = true;
$this->selectedHetznerSshKeyIds = [];
$this->selectedHetznerFirewallIds = [];
+77 -8
View File
@@ -33,6 +33,8 @@ class ByVultr extends Component
public ?int $selected_token_id = null;
public ?string $selectedTokenUuid = null;
public array $regions = [];
public array $plans = [];
@@ -72,10 +74,11 @@ class ByVultr extends Component
public bool $from_onboarding = false;
public function mount(): void
public function mount(?string $selectedTokenUuid = null): void
{
$this->authorize('viewAny', CloudProviderToken::class);
$this->loadTokens();
$this->selectTokenFromUrl($selectedTokenUuid);
$this->loadSavedCloudInitScripts();
$this->server_name = generate_random_name();
$this->private_keys = PrivateKey::ownedAndOnlySShKeys()->where('id', '!=', 0)->get();
@@ -83,6 +86,11 @@ class ByVultr extends Component
if ($this->private_keys->count() > 0) {
$this->private_key_id = $this->private_keys->first()->id;
}
if ($this->selectedTokenUuid) {
$this->current_step = 2;
$this->loading_data = true;
}
}
public function getListeners(): array
@@ -165,9 +173,27 @@ class ByVultr extends Component
];
}
public function selectToken(int $tokenId): void
public function selectToken(int $tokenId): mixed
{
$this->selected_token_id = $tokenId;
return $this->nextStep();
}
private function selectTokenFromUrl(?string $selectedTokenUuid): void
{
if (! $selectedTokenUuid) {
return;
}
$token = $this->available_tokens->firstWhere('uuid', $selectedTokenUuid);
if (! $token) {
return;
}
$this->selectedTokenUuid = $selectedTokenUuid;
$this->selected_token_id = $token->id;
}
public function nextStep(): mixed
@@ -177,14 +203,19 @@ class ByVultr extends Component
]);
try {
$vultrToken = $this->getVultrToken();
if (! $this->selectedTokenUuid) {
$token = $this->available_tokens->firstWhere('id', $this->selected_token_id);
if (! $vultrToken) {
return $this->dispatch('error', 'Please select a valid Vultr token.');
if ($token) {
return $this->redirectRoute('server.create.token', [
'type' => 'vultr',
'token_uuid' => $token->uuid,
], navigate: true);
}
}
$this->loadVultrData($vultrToken);
$this->current_step = 2;
$this->loading_data = true;
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -192,9 +223,15 @@ class ByVultr extends Component
return null;
}
public function previousStep(): void
public function previousStep(): mixed
{
if ($this->selectedTokenUuid) {
return $this->redirectRoute('server.create.type', ['type' => 'vultr'], navigate: true);
}
$this->current_step = 1;
return null;
}
public function updatedSelectedRegion(): void
@@ -251,6 +288,29 @@ class ByVultr extends Component
return '$'.number_format((float) $monthlyCost, 2);
}
public function getAdvancedVultrOptionsSummaryProperty(): array
{
$summary = [];
if (count($this->selectedVultrSshKeyIds) > 0) {
$summary[] = count($this->selectedVultrSshKeyIds).' extra SSH '.str('key')->plural(count($this->selectedVultrSshKeyIds));
}
if (! $this->enable_ipv6) {
$summary[] = 'IPv6 disabled';
}
if ($this->disable_public_ipv4) {
$summary[] = 'Public IPv4 disabled';
}
if (! empty($this->cloud_init_script)) {
$summary[] = 'cloud-init';
}
return $summary;
}
private function getVultrToken(): string
{
if ($this->selected_token_id) {
@@ -262,8 +322,17 @@ class ByVultr extends Component
return '';
}
private function loadVultrData(string $token): void
public function loadVultrData(): void
{
$token = $this->getVultrToken();
if (! $token) {
$this->loading_data = false;
$this->dispatch('error', 'Please select a valid Vultr token.');
return;
}
$this->loading_data = true;
try {