improved hetzner features

This commit is contained in:
Andras Bacsai committed 2025-10-09 12:53:57 +02:00
1 parent c9e6418542
commit 704ddf2968
17 files changed
+514 -199

No files matched your search

@@ -8,8 +8,11 @@
'content' => null,
'closeOutside' => true,
'minWidth' => '36rem',
'isFullWidth' => false,
])
<div x-data="{ modalOpen: false }" :class="{ 'z-40': modalOpen }" @keydown.window.escape="modalOpen=false"
<div x-data="{ modalOpen: false }"
x-init="$watch('modalOpen', value => { if (!value) { $wire.dispatch('modalClosed') } })"
:class="{ 'z-40': modalOpen }" @keydown.window.escape="modalOpen=false"
class="relative w-auto h-auto" wire:ignore>
@if ($content)
<div @click="modalOpen=true">
@@ -17,13 +20,13 @@
</div>
@else
@if ($disabled)
<x-forms.button isError disabled>{{ $buttonTitle }}</x-forms.button>
<x-forms.button isError disabled @class(['w-full' => $isFullWidth])>{{ $buttonTitle }}</x-forms.button>
@elseif ($isErrorButton)
<x-forms.button isError @click="modalOpen=true">{{ $buttonTitle }}</x-forms.button>
<x-forms.button isError @click="modalOpen=true" @class(['w-full' => $isFullWidth])>{{ $buttonTitle }}</x-forms.button>
@elseif ($isHighlightedButton)
<x-forms.button isHighlighted @click="modalOpen=true">{{ $buttonTitle }}</x-forms.button>
<x-forms.button isHighlighted @click="modalOpen=true" @class(['w-full' => $isFullWidth])>{{ $buttonTitle }}</x-forms.button>
@else
<x-forms.button @click="modalOpen=true">{{ $buttonTitle }}</x-forms.button>
<x-forms.button @click="modalOpen=true" @class(['w-full' => $isFullWidth])>{{ $buttonTitle }}</x-forms.button>
@endif
@endif
<template x-teleport="body">
@@ -9,6 +9,11 @@
<a class="menu-item {{ $activeMenu === 'private-key' ? 'menu-item-active' : '' }}"
href="{{ route('server.private-key', ['server_uuid' => $server->uuid]) }}">Private Key
</a>
@if ($server->hetzner_server_id)
<a class="menu-item {{ $activeMenu === 'cloud-provider-token' ? 'menu-item-active' : '' }}"
href="{{ route('server.cloud-provider-token', ['server_uuid' => $server->uuid]) }}">Hetzner Token
</a>
@endif
<a class="menu-item {{ $activeMenu === 'ca-certificate' ? 'menu-item-active' : '' }}"
href="{{ route('server.ca-certificate', ['server_uuid' => $server->uuid]) }}">CA Certificate
</a>
@@ -0,0 +1,43 @@
<div class="w-full">
<form class="flex flex-col gap-2 {{ $modal_mode ? 'w-full' : '' }}" wire:submit='addToken'>
@if ($modal_mode)
{{-- Modal layout: vertical, compact --}}
@if (!isset($provider) || empty($provider) || $provider === '')
<x-forms.select required id="provider" label="Provider">
<option value="hetzner">Hetzner</option>
<option value="digitalocean">DigitalOcean</option>
</x-forms.select>
@else
<input type="hidden" wire:model="provider" />
@endif
<x-forms.input required id="name" label="Token Name"
placeholder="e.g., Production Hetzner. tip: add Hetzner project name in it" />
<x-forms.input required type="password" id="token" label="API Token" placeholder="Enter your API token"
helper="Your {{ ucfirst($provider) }} Cloud API token. You can create one in your <a href='{{ $provider === 'hetzner' ? 'https://console.hetzner.cloud/' : '#' }}' target='_blank' class='underline dark:text-white'>{{ ucfirst($provider) }} Console</a>." />
<x-forms.button type="submit">Add Token</x-forms.button>
@else
{{-- Full page layout: horizontal, spacious --}}
<div class="flex gap-2 items-end flex-wrap">
<div class="w-64">
<x-forms.select required id="provider" label="Provider">
<option value="hetzner">Hetzner</option>
<option value="digitalocean">DigitalOcean</option>
</x-forms.select>
</div>
<div class="flex-1 min-w-64">
<x-forms.input required id="name" label="Token Name" placeholder="e.g., Production Hetzner" />
</div>
</div>
<div class="flex gap-2 items-end flex-wrap">
<div class="flex-1 min-w-64">
<x-forms.input required type="password" id="token" label="API Token"
placeholder="Enter your API token" />
</div>
<x-forms.button type="submit">Add Token</x-forms.button>
</div>
@endif
</form>
</div>
@@ -1,28 +1,10 @@
<div>
<h2>Cloud Provider Tokens</h2>
<div class="pb-4">Manage API tokens for cloud providers (Hetzner, DigitalOcean, etc.). Tokens are saved encrypted and shared with your team.</div>
<div class="pb-4">Manage API tokens for cloud providers (Hetzner, DigitalOcean, etc.).</div>
<h3>New Token</h3>
@can('create', App\Models\CloudProviderToken::class)
<form class="flex flex-col gap-2" wire:submit='addNewToken'>
<div class="flex gap-2 items-end flex-wrap">
<div class="w-64">
<x-forms.select required id="provider" label="Provider">
<option value="hetzner">Hetzner</option>
<option value="digitalocean">DigitalOcean</option>
</x-forms.select>
</div>
<div class="flex-1 min-w-64">
<x-forms.input required id="name" label="Token Name" placeholder="e.g., Production Hetzner" />
</div>
</div>
<div class="flex gap-2 items-end flex-wrap">
<div class="flex-1 min-w-64">
<x-forms.input required type="password" id="token" label="API Token" placeholder="Enter your API token" />
</div>
<x-forms.button type="submit">Add Token</x-forms.button>
</div>
</form>
<livewire:security.cloud-provider-token-form :modal_mode="false" />
@endcan
<h3 class="py-4">Saved Tokens</h3>
@@ -36,25 +18,17 @@
</span>
<span class="font-bold dark:text-white">{{ $savedToken->name }}</span>
</div>
<div class="text-sm">Token: ***{{ substr($savedToken->token, -4) }}</div>
<div class="text-sm">Created: {{ $savedToken->created_at->diffForHumans() }}</div>
@can('delete', $savedToken)
<x-modal-confirmation
title="Confirm Token Deletion?"
isErrorButton
buttonTitle="Delete Token"
submitAction="deleteToken({{ $savedToken->id }})"
:actions="[
<x-modal-confirmation title="Confirm Token Deletion?" isErrorButton buttonTitle="Delete Token"
submitAction="deleteToken({{ $savedToken->id }})" :actions="[
'This cloud provider token will be permanently deleted.',
'Any servers using this token will need to be reconfigured.',
]"
confirmationText="{{ $savedToken->name }}"
confirmationLabel="Please confirm the deletion by entering the token name below"
shortConfirmationLabel="Token Name"
:confirmWithPassword="false"
step2ButtonText="Delete Token"
/>
shortConfirmationLabel="Token Name" :confirmWithPassword="false" step2ButtonText="Delete Token" />
@endcan
</div>
@empty
@@ -0,0 +1,61 @@
<div>
<x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > Hetzner Token | Coolify
</x-slot>
<livewire:server.navbar :server="$server" />
<div class="flex flex-col h-full gap-8 sm:flex-row">
<x-server.sidebar :server="$server" activeMenu="cloud-provider-token" />
<div class="w-full">
@if ($server->hetzner_server_id)
<div class="flex items-end gap-2">
<h2>Hetzner Token</h2>
@can('create', App\Models\CloudProviderToken::class)
<x-modal-input buttonTitle="+ Add" title="Add Hetzner Token">
<livewire:security.cloud-provider-token-form :modal_mode="true" provider="hetzner" />
</x-modal-input>
@endcan
<x-forms.button canGate="update" :canResource="$server" isHighlighted
wire:click.prevent='validateToken'>
Validate token
</x-forms.button>
</div>
<div class="pb-4">Change your server's Hetzner token.</div>
<div class="grid xl:grid-cols-2 grid-cols-1 gap-2">
@forelse ($cloudProviderTokens as $token)
<div
class="box-without-bg justify-between dark:bg-coolgray-100 bg-white items-center flex flex-col gap-2">
<div class="flex flex-col w-full">
<div class="box-title">{{ $token->name }}</div>
<div class="box-description">
Created {{ $token->created_at->diffForHumans() }}
</div>
</div>
@if (data_get($server, 'cloudProviderToken.id') !== $token->id)
<x-forms.button canGate="update" :canResource="$server" class="w-full"
wire:click='setCloudProviderToken({{ $token->id }})'>
Use this token
</x-forms.button>
@else
<x-forms.button class="w-full" disabled>
Currently used
</x-forms.button>
@endif
</div>
@empty
<div>No Hetzner tokens found. </div>
@endforelse
</div>
@else
<div class="flex items-end gap-2">
<h2>Hetzner Token</h2>
</div>
<div class="pb-4">This server was not created through Hetzner Cloud integration.</div>
<div class="p-4 border rounded-md dark:border-coolgray-300 dark:bg-coolgray-100">
<p class="dark:text-neutral-400">
Only servers created through Hetzner Cloud can have their tokens managed here.
</p>
</div>
@endif
</div>
</div>
</div>
@@ -3,7 +3,7 @@
@can('viewAny', App\Models\CloudProviderToken::class)
<div>
<div class="flex gap-2 flex-wrap">
<x-modal-input title="Connect to Hetzner">
<x-modal-input title="Connect a Hetzner Server">
<x-slot:button-title>
<div class="flex items-center gap-2">
<svg class="w-5 h-5" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
@@ -3,40 +3,37 @@
<x-limit-reached name="servers" />
@else
@if ($current_step === 1)
<form class="flex flex-col w-full gap-2" wire:submit.prevent="nextStep">
<div class="flex flex-col w-full gap-4">
@if ($available_tokens->count() > 0)
<div>
<x-forms.select label="Use Saved Token" id="selected_token_id"
wire:change="selectToken($event.target.value)">
<option value="">Select a saved token...</option>
@foreach ($available_tokens as $token)
<option value="{{ $token->id }}">
{{ $token->name ?? 'Hetzner Token' }} (***{{ substr($token->token, -4) }})
</option>
@endforeach
</x-forms.select>
<div class="flex gap-2">
<div class="flex-1">
<x-forms.select label="Select Hetzner Token" id="selected_token_id"
wire:change="selectToken($event.target.value)" required>
<option value="">Select a saved token...</option>
@foreach ($available_tokens as $token)
<option value="{{ $token->id }}">
{{ $token->name ?? 'Hetzner Token' }}
</option>
@endforeach
</x-forms.select>
</div>
<div class="flex items-end">
<x-forms.button canGate="create" :canResource="App\Models\Server::class"
wire:click="nextStep" :disabled="!$selected_token_id">
Continue
</x-forms.button>
</div>
</div>
<div class="text-center text-sm dark:text-neutral-500 py-2">OR</div>
<div class="text-center text-sm dark:text-neutral-500">OR</div>
@endif
<div>
<x-forms.input type="password" id="hetzner_token" label="New Hetzner API Token"
helper="Your Hetzner Cloud API token. You can create one in your <a href='https://console.hetzner.cloud/' target='_blank' class='underline dark:text-white'>Hetzner Cloud Console</a>." />
</div>
<div>
<x-forms.checkbox id="save_token" label="Save this token for my team" />
</div>
<div>
<x-forms.input id="token_name" label="Token Name" placeholder="e.g., Production Hetzner"
helper="Give this token a friendly name to identify it later." />
</div>
<x-forms.button canGate="create" :canResource="App\Models\Server::class" type="submit">
Continue
</x-forms.button>
</form>
<x-modal-input isFullWidth
buttonTitle="{{ $available_tokens->count() > 0 ? '+ Add New Token' : 'Add Hetzner Token' }}"
title="Add Hetzner Token">
<livewire:security.cloud-provider-token-form :modal_mode="true" provider="hetzner" />
</x-modal-input>
</div>
@elseif ($current_step === 2)
@if ($loading_data)
<div class="flex items-center justify-center py-8">
@@ -66,7 +63,9 @@
<div>
<x-forms.select label="Server Type" id="selected_server_type"
wire:model.live="selected_server_type" required :disabled="!$selected_location">
<option value="">{{ $selected_location ? 'Select a server type...' : 'Select a location first' }}</option>
<option value="">
{{ $selected_location ? 'Select a server type...' : 'Select a location first' }}
</option>
@foreach ($this->availableServerTypes as $serverType)
<option value="{{ $serverType['name'] }}">
{{ $serverType['description'] }} -
@@ -87,7 +86,9 @@
<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>
<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'] }}