Add dedicated show/edit pages for cloud provider tokens and cloud-init scripts, including descriptions and UUID routes. Generate private keys directly from the index and surface cloud provider API loading errors in server creation flows.
294 lines
19 KiB
PHP
294 lines
19 KiB
PHP
<div class="w-full">
|
|
@if ($limit_reached)
|
|
<x-limit-reached name="servers" />
|
|
@else
|
|
@if ($current_step === 1)
|
|
<div class="flex flex-col w-full gap-4">
|
|
<div class="text-sm text-neutral-500 dark:text-neutral-400">
|
|
Don't have a Hetzner account? <a href="https://coolify.io/hetzner" target="_blank"
|
|
class="underline dark:text-white">Sign up here</a>.
|
|
<span class="text-xs">Coolify's affiliate link, only for new accounts - it supports both of us.</span>
|
|
</div>
|
|
@if ($available_tokens->count() > 0)
|
|
<div class="grid gap-3 md:grid-cols-2">
|
|
@foreach ($available_tokens as $token)
|
|
<a class="coolbox group text-left" wire:key="hetzner-token-{{ $token->id }}"
|
|
href="{{ route('server.create.token', ['type' => 'hetzner', 'token_uuid' => $token->uuid]) }}" {{ wireNavigate() }}>
|
|
<div class="flex flex-col justify-center mx-6">
|
|
<div class="box-title">
|
|
{{ $token->name ?? 'Hetzner Token' }}
|
|
</div>
|
|
<div class="box-description">
|
|
Use this token to create a Hetzner server.
|
|
</div>
|
|
</div>
|
|
</a>
|
|
@endforeach
|
|
</div>
|
|
@else
|
|
<div class="w-full max-w-2xl">
|
|
<x-modal-input title="Add Hetzner Token">
|
|
<x-slot:content>
|
|
<div class="coolbox group cursor-pointer">
|
|
<div class="flex items-center gap-4 mx-6">
|
|
<div
|
|
class="flex size-10 shrink-0 items-center justify-center rounded-full bg-coollabs/10 text-coollabs dark:bg-warning/20 dark:text-warning">
|
|
<svg class="size-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
|
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round"
|
|
d="M12 4.5v15m7.5-7.5h-15" />
|
|
</svg>
|
|
</div>
|
|
<div class="flex flex-col justify-center">
|
|
<div class="box-title">Add a new token</div>
|
|
<div class="box-description">
|
|
Add a Hetzner API token to create servers from your account.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</x-slot:content>
|
|
<livewire:security.cloud-provider-token-form :modal_mode="true" provider="hetzner"
|
|
wire:key="new-server-empty-token-hetzner" />
|
|
</x-modal-input>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@elseif ($current_step === 2)
|
|
<div wire:init="loadHetznerData">
|
|
@if ($loading_data)
|
|
<div class="flex items-center justify-center py-8">
|
|
<x-loading text="Loading Hetzner data..." />
|
|
</div>
|
|
@elseif ($provider_data_error)
|
|
<div class="flex flex-col gap-4 rounded-lg border border-error bg-error/10 p-4">
|
|
<div>
|
|
<h3>Unable to load Hetzner details</h3>
|
|
<p class="text-sm text-neutral-700 dark:text-neutral-300">
|
|
Coolify could not fetch Hetzner data with the selected token. The token may have been
|
|
deleted, revoked, or no longer has access.
|
|
</p>
|
|
</div>
|
|
<pre class="whitespace-pre-wrap break-words text-sm text-error">{{ $provider_data_error }}</pre>
|
|
<div>
|
|
<a class="button" href="{{ route('server.create.type', ['type' => 'hetzner']) }}" {{ wireNavigate() }}>
|
|
Select another token
|
|
</a>
|
|
</div>
|
|
</div>
|
|
@else
|
|
<form class="flex flex-col w-full gap-2" wire:submit='submit'>
|
|
<div>
|
|
<x-forms.input id="server_name" label="Server Name" helper="A friendly name for your server." />
|
|
</div>
|
|
|
|
<div>
|
|
<x-forms.select label="Location" id="selected_location" wire:model.live="selected_location" required>
|
|
<option value="">Select a location...</option>
|
|
@foreach ($locations as $location)
|
|
<option value="{{ $location['name'] }}">
|
|
{{ $location['city'] }} - {{ $location['country'] }}
|
|
</option>
|
|
@endforeach
|
|
</x-forms.select>
|
|
</div>
|
|
|
|
<div>
|
|
<x-forms.select label="Server Type" id="selected_server_type" wire:model.live="selected_server_type"
|
|
helper="Learn more about <a class='inline-block underline dark:text-white' href='https://www.hetzner.com/cloud/' target='_blank'>Hetzner server types</a>"
|
|
required :disabled="!$selected_location">
|
|
<option value="">
|
|
{{ $selected_location ? 'Select a server type...' : 'Select a location first' }}
|
|
</option>
|
|
@foreach ($this->availableServerTypes as $serverType)
|
|
<option value="{{ $serverType['name'] }}">
|
|
{{ $serverType['description'] }} -
|
|
{{ $serverType['cores'] }} vCPU
|
|
@if (isset($serverType['cpu_vendor_info']) && $serverType['cpu_vendor_info'])
|
|
({{ $serverType['cpu_vendor_info'] }})
|
|
@endif
|
|
, {{ $serverType['memory'] }}GB RAM,
|
|
{{ $serverType['disk'] }}GB
|
|
@if (isset($serverType['architecture']))
|
|
[{{ $serverType['architecture'] }}]
|
|
@endif
|
|
@if (isset($serverType['prices']))
|
|
-
|
|
€{{ number_format($serverType['prices'][0]['price_monthly']['gross'] ?? 0, 2) }}/mo
|
|
@endif
|
|
</option>
|
|
@endforeach
|
|
</x-forms.select>
|
|
</div>
|
|
|
|
<div>
|
|
<x-forms.select label="Image" id="selected_image" required :disabled="!$selected_server_type">
|
|
<option value="">
|
|
{{ $selected_server_type ? 'Select an image...' : 'Select a server type first' }}
|
|
</option>
|
|
@foreach ($this->availableImages as $image)
|
|
<option value="{{ $image['id'] }}">
|
|
{{ $image['description'] ?? $image['name'] }}
|
|
@if (isset($image['architecture']))
|
|
({{ $image['architecture'] }})
|
|
@endif
|
|
</option>
|
|
@endforeach
|
|
</x-forms.select>
|
|
</div>
|
|
|
|
<div>
|
|
@if ($private_keys->count() === 0)
|
|
<div class="flex flex-col gap-2">
|
|
<label class="flex gap-1 items-center mb-1 text-sm font-medium">
|
|
Private Key
|
|
<x-highlighted text="*" />
|
|
</label>
|
|
<div
|
|
class="p-4 border border-warning-500 dark:border-warning-600 rounded bg-warning-50 dark:bg-warning-900/10">
|
|
<p class="text-sm mb-3 text-neutral-700 dark:text-neutral-300">
|
|
No private keys found. You need to create a private key to continue.
|
|
</p>
|
|
<x-modal-input buttonTitle="Create New Private Key" title="New Private Key" isHighlightedButton>
|
|
<livewire:security.private-key.create :modal_mode="true" from="server" />
|
|
</x-modal-input>
|
|
</div>
|
|
</div>
|
|
@else
|
|
<x-forms.select label="Private Key" id="private_key_id" required>
|
|
<option value="">Select a private key...</option>
|
|
@foreach ($private_keys as $key)
|
|
<option value="{{ $key->id }}">
|
|
{{ $key->name }}
|
|
</option>
|
|
@endforeach
|
|
</x-forms.select>
|
|
<p class="mt-1 text-xs text-neutral-500 dark:text-neutral-400">
|
|
This SSH key will be automatically added to your Hetzner account and used to access the
|
|
server.
|
|
</p>
|
|
@endif
|
|
</div>
|
|
<x-dropdown inline panelClass="max-h-[55vh] overflow-y-auto scrollbar">
|
|
<x-slot:title>
|
|
<span class="text-sm font-medium">Advanced Hetzner options</span>
|
|
<span class="mt-1 text-xs text-neutral-500 dark:text-neutral-400">SSH keys, firewalls, private networks, backups, and cloud-init.</span>
|
|
@if (count($this->advancedHetznerOptionsSummary) > 0)
|
|
<span class="mt-1 flex flex-wrap gap-1.5">
|
|
@foreach ($this->advancedHetznerOptionsSummary as $summaryItem)
|
|
<span class="rounded bg-neutral-200 px-2 py-0.5 text-xs text-neutral-700 dark:bg-coolgray-100 dark:text-neutral-300">
|
|
{{ $summaryItem }}
|
|
</span>
|
|
@endforeach
|
|
</span>
|
|
@endif
|
|
</x-slot>
|
|
|
|
<div class="flex w-full flex-col gap-4 p-3">
|
|
<div>
|
|
<x-forms.datalist label="Extra SSH Keys" id="selectedHetznerSshKeyIds"
|
|
helper="Select existing SSH keys from your Hetzner account to add to this server. The Coolify SSH key will be automatically added."
|
|
:multiple="true" :disabled="count($hetznerSshKeys) === 0" :placeholder="count($hetznerSshKeys) > 0
|
|
? 'Search and select SSH keys...'
|
|
: 'No SSH keys found in Hetzner account'">
|
|
@foreach ($hetznerSshKeys as $sshKey)
|
|
<option value="{{ $sshKey['id'] }}">
|
|
{{ $sshKey['name'] }} - {{ substr($sshKey['fingerprint'], 0, 20) }}...
|
|
</option>
|
|
@endforeach
|
|
</x-forms.datalist>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
<x-forms.datalist label="Firewalls" id="selectedHetznerFirewallIds"
|
|
helper="Optionally apply existing Hetzner firewalls when the server is created."
|
|
:multiple="true" :disabled="count($hetznerFirewalls) === 0" :placeholder="count($hetznerFirewalls) > 0
|
|
? 'Search and select firewalls...'
|
|
: 'No firewalls found in Hetzner account'">
|
|
@foreach ($hetznerFirewalls as $firewall)
|
|
<option value="{{ $firewall['id'] }}">
|
|
{{ $firewall['name'] }}
|
|
@if (isset($firewall['rules']))
|
|
- {{ count($firewall['rules']) }} rules
|
|
@endif
|
|
</option>
|
|
@endforeach
|
|
</x-forms.datalist>
|
|
|
|
<x-forms.datalist label="Private Networks" id="selectedHetznerNetworkIds"
|
|
helper="Optionally attach one or more private networks. Networks are filtered to the selected location's network zone when possible."
|
|
:multiple="true" :disabled="count($this->availableNetworks) === 0" :placeholder="count($this->availableNetworks) > 0
|
|
? 'Search and select networks...'
|
|
: 'No compatible networks found'">
|
|
@foreach ($this->availableNetworks as $network)
|
|
<option value="{{ $network['id'] }}">
|
|
{{ $network['name'] }} - {{ $network['ip_range'] }}
|
|
</option>
|
|
@endforeach
|
|
</x-forms.datalist>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 gap-2 md:grid-cols-3">
|
|
<x-forms.checkbox id="enable_ipv4" label="Enable IPv4"
|
|
helper="Enable public IPv4 address for this server" fullWidth />
|
|
<x-forms.checkbox id="enable_ipv6" label="Enable IPv6"
|
|
helper="Enable public IPv6 address for this server" fullWidth />
|
|
<x-forms.checkbox id="enable_backups" label="Enable Hetzner Backups" fullWidth
|
|
helper="Hetzner bills backups at an additional 20% of the server monthly fee{{ $this->selectedServerBackupSurcharge ? ' (about ' . $this->selectedServerBackupSurcharge . '/mo for the selected server type)' : '' }}." />
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
@if (! $show_cloud_init_script && empty($cloud_init_script) && empty($selected_cloud_init_script_id))
|
|
<div>
|
|
<x-forms.button type="button" wire:click="showCloudInitScript">
|
|
Add cloud-init script
|
|
</x-forms.button>
|
|
</div>
|
|
@else
|
|
<div class="flex justify-between items-center gap-2">
|
|
<label class="text-sm font-medium w-32">Cloud-Init Script</label>
|
|
@if ($saved_cloud_init_scripts->count() > 0)
|
|
<div class="flex items-center gap-2 flex-1">
|
|
<x-forms.select wire:model.live="selected_cloud_init_script_id" label="" helper="">
|
|
<option value="">Load saved script...</option>
|
|
@foreach ($saved_cloud_init_scripts as $script)
|
|
<option value="{{ $script->id }}">{{ $script->name }}</option>
|
|
@endforeach
|
|
</x-forms.select>
|
|
<x-forms.button type="button" wire:click="clearCloudInitScript">
|
|
Clear
|
|
</x-forms.button>
|
|
</div>
|
|
@else
|
|
<x-forms.button type="button" wire:click="clearCloudInitScript">
|
|
Remove
|
|
</x-forms.button>
|
|
@endif
|
|
</div>
|
|
<x-forms.textarea id="cloud_init_script" label=""
|
|
helper="Add a cloud-init script to run when the server is created. See Hetzner's documentation for details."
|
|
rows="8" />
|
|
|
|
<div class="flex items-center gap-2">
|
|
<x-forms.checkbox id="save_cloud_init_script" label="Save this script for later use" />
|
|
@if ($save_cloud_init_script)
|
|
<div class="flex-1">
|
|
<x-forms.input id="cloud_init_script_name" label="" placeholder="Script name..." />
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</x-dropdown>
|
|
|
|
<x-forms.button isHighlighted canGate="create" :canResource="App\Models\Server::class" type="submit"
|
|
:disabled="!$private_key_id">
|
|
Buy & Create Server{{ $this->selectedServerPrice ? ' (' . $this->selectedServerPrice . '/mo)' : '' }}
|
|
</x-forms.button>
|
|
</form>
|
|
@endif
|
|
</div>
|
|
@endif
|
|
@endif
|
|
</div>
|