refactor(hetzner): move advanced options into dropdown
This commit is contained in:
parent
d723a52d9c
commit
b939e09f12
4 changed files with 140 additions and 130 deletions
|
|
@ -74,8 +74,6 @@ class ByHetzner extends Component
|
|||
|
||||
public bool $enable_backups = false;
|
||||
|
||||
public bool $show_advanced_hetzner_options = false;
|
||||
|
||||
public bool $show_cloud_init_script = false;
|
||||
|
||||
public ?string $cloud_init_script = null;
|
||||
|
|
@ -131,7 +129,6 @@ public function resetSelection()
|
|||
$this->save_cloud_init_script = false;
|
||||
$this->cloud_init_script_name = null;
|
||||
$this->selected_cloud_init_script_id = null;
|
||||
$this->show_advanced_hetzner_options = false;
|
||||
$this->show_cloud_init_script = false;
|
||||
$this->selectedHetznerSshKeyIds = [];
|
||||
$this->selectedHetznerFirewallIds = [];
|
||||
|
|
@ -191,7 +188,6 @@ protected function rules(): array
|
|||
'enable_ipv4' => 'required|boolean',
|
||||
'enable_ipv6' => 'required|boolean',
|
||||
'enable_backups' => 'required|boolean',
|
||||
'show_advanced_hetzner_options' => 'boolean',
|
||||
'show_cloud_init_script' => 'boolean',
|
||||
'cloud_init_script' => ['nullable', 'string', new ValidCloudInitYaml],
|
||||
'save_cloud_init_script' => 'boolean',
|
||||
|
|
@ -452,22 +448,6 @@ public function getSelectedServerBackupSurchargeProperty(): ?string
|
|||
return '€'.number_format($price * 0.2, 2);
|
||||
}
|
||||
|
||||
public function getShouldShowAdvancedHetznerOptionsProperty(): bool
|
||||
{
|
||||
return $this->show_advanced_hetzner_options
|
||||
|| $this->selectedHetznerSshKeyIds !== []
|
||||
|| $this->selectedHetznerFirewallIds !== []
|
||||
|| $this->selectedHetznerNetworkIds !== []
|
||||
|| $this->enable_backups
|
||||
|| ! $this->enable_ipv4
|
||||
|| ! $this->enable_ipv6
|
||||
|| $this->show_cloud_init_script
|
||||
|| filled($this->cloud_init_script)
|
||||
|| $this->save_cloud_init_script
|
||||
|| filled($this->cloud_init_script_name)
|
||||
|| filled($this->selected_cloud_init_script_id);
|
||||
}
|
||||
|
||||
public function getAdvancedHetznerOptionsSummaryProperty(): array
|
||||
{
|
||||
$summary = [];
|
||||
|
|
@ -502,15 +482,9 @@ public function getAdvancedHetznerOptionsSummaryProperty(): array
|
|||
return $summary;
|
||||
}
|
||||
|
||||
public function toggleAdvancedHetznerOptions(): void
|
||||
{
|
||||
$this->show_advanced_hetzner_options = ! $this->show_advanced_hetzner_options;
|
||||
}
|
||||
|
||||
public function showCloudInitScript(): void
|
||||
{
|
||||
$this->show_cloud_init_script = true;
|
||||
$this->show_advanced_hetzner_options = true;
|
||||
}
|
||||
|
||||
public function updatedSelectedLocation($value)
|
||||
|
|
@ -545,7 +519,6 @@ public function updatedSelectedCloudInitScriptId($value)
|
|||
$this->cloud_init_script = $script->script;
|
||||
$this->cloud_init_script_name = $script->name;
|
||||
$this->show_cloud_init_script = true;
|
||||
$this->show_advanced_hetzner_options = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
@props([
|
||||
'inline' => false,
|
||||
'triggerClass' => '',
|
||||
'panelClass' => '',
|
||||
])
|
||||
|
||||
<div x-data="{
|
||||
dropdownOpen: false,
|
||||
panelStyles: '',
|
||||
|
|
@ -9,7 +15,7 @@
|
|||
this.dropdownOpen = false;
|
||||
},
|
||||
updatePanelPosition() {
|
||||
if (window.innerWidth >= 768) {
|
||||
if ({{ $inline ? 'true' : 'false' }} || window.innerWidth >= 768) {
|
||||
this.panelStyles = '';
|
||||
|
||||
return;
|
||||
|
|
@ -37,9 +43,13 @@
|
|||
this.panelStyles = `position: fixed; left: ${left}px; top: ${top}px;`;
|
||||
});
|
||||
}
|
||||
}" class="relative" @click.outside="close()" x-on:resize.window="if (dropdownOpen) updatePanelPosition()">
|
||||
<button x-ref="trigger" @click="dropdownOpen ? close() : open()"
|
||||
class="inline-flex items-center justify-start pr-8 transition-colors focus:outline-hidden disabled:opacity-50 disabled:pointer-events-none">
|
||||
}" @class(['relative', 'w-full' => $inline]) @click.outside="if (! {{ $inline ? 'true' : 'false' }}) close()" x-on:resize.window="if (dropdownOpen) updatePanelPosition()">
|
||||
<button type="button" x-ref="trigger" @click="dropdownOpen ? close() : open()"
|
||||
@class([
|
||||
'inline-flex items-center justify-start pr-8 transition-colors focus:outline-hidden disabled:opacity-50 disabled:pointer-events-none',
|
||||
'w-full border border-neutral-300 bg-white px-3 py-2 text-left dark:border-coolgray-300 dark:bg-coolgray-100' => $inline,
|
||||
$triggerClass,
|
||||
])>
|
||||
<span class="flex flex-col items-start h-full leading-none">
|
||||
{{ $title }}
|
||||
</span>
|
||||
|
|
@ -50,11 +60,18 @@ class="inline-flex items-center justify-start pr-8 transition-colors focus:outli
|
|||
</svg>
|
||||
</button>
|
||||
|
||||
<div x-ref="panel" x-show="dropdownOpen" @click.away="close()" x-transition:enter="ease-out duration-200"
|
||||
<div x-ref="panel" x-show="dropdownOpen" @click.away="if (! {{ $inline ? 'true' : 'false' }}) close()" x-transition:enter="ease-out duration-200"
|
||||
x-transition:enter-start="-translate-y-2" x-transition:enter-end="translate-y-0"
|
||||
:style="panelStyles" class="absolute top-full z-50 mt-1 min-w-max max-w-[calc(100vw-1rem)] md:top-0 md:mt-6" x-cloak>
|
||||
<div
|
||||
class="border border-neutral-300 bg-white p-1 shadow-sm dark:border-coolgray-300 dark:bg-coolgray-200">
|
||||
:style="panelStyles" @class([
|
||||
'mt-1 w-full' => $inline,
|
||||
'absolute top-full z-50 mt-1 min-w-max max-w-[calc(100vw-1rem)] md:top-0 md:mt-6' => ! $inline,
|
||||
]) x-cloak>
|
||||
<div @class([
|
||||
'border border-neutral-300 bg-white p-1 dark:border-coolgray-300',
|
||||
'shadow-sm dark:bg-coolgray-200' => ! $inline,
|
||||
'border-0 bg-transparent shadow-none dark:border-0 dark:bg-transparent' => $inline,
|
||||
$panelClass,
|
||||
])>
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -135,95 +135,118 @@ class="p-4 border border-warning-500 dark:border-warning-600 rounded bg-warning-
|
|||
</p>
|
||||
@endif
|
||||
</div>
|
||||
<div>
|
||||
<x-forms.datalist label="Additional SSH Keys (from Hetzner)" 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>
|
||||
<x-forms.datalist label="Firewalls (from Hetzner)" 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>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<x-forms.datalist label="Internal Networks (from Hetzner)" 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="flex flex-col gap-2">
|
||||
<label class="text-sm font-medium">Network Configuration</label>
|
||||
<div class="flex gap-4">
|
||||
<x-forms.checkbox id="enable_ipv4" label="Enable IPv4"
|
||||
helper="Enable public IPv4 address for this server" />
|
||||
<x-forms.checkbox id="enable_ipv6" label="Enable IPv6"
|
||||
helper="Enable public IPv6 address for this server" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="text-sm font-medium">Backups</label>
|
||||
<x-forms.checkbox id="enable_backups" label="Enable Hetzner Backups"
|
||||
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">
|
||||
<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>
|
||||
<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
|
||||
</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" />
|
||||
</x-slot>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<x-forms.checkbox id="save_cloud_init_script" label="Save this script for later use" />
|
||||
<div class="flex-1">
|
||||
<x-forms.input id="cloud_init_script_name" label="" placeholder="Script name..." />
|
||||
<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>
|
||||
</div>
|
||||
</x-dropdown>
|
||||
|
||||
<div class="flex gap-2 justify-between">
|
||||
<x-forms.button type="button" wire:click="previousStep">
|
||||
|
|
|
|||
|
|
@ -189,22 +189,20 @@
|
|||
expect((bool) $this->team->fresh()->show_boarding)->toBeTrue();
|
||||
});
|
||||
|
||||
test('keeps advanced Hetzner options collapsed by default in the create dialog', function () {
|
||||
test('uses the shared dropdown UI for advanced Hetzner options', function () {
|
||||
Livewire::test(ByHetzner::class)
|
||||
->set('current_step', 2)
|
||||
->assertSet('show_advanced_hetzner_options', false)
|
||||
->assertSee('Advanced Hetzner options')
|
||||
->assertDontSee('Extra SSH Keys')
|
||||
->assertDontSee('Firewalls')
|
||||
->assertDontSee('Private Networks')
|
||||
->assertDontSee('Enable Hetzner Backups')
|
||||
->assertDontSee('Cloud-Init Script');
|
||||
->assertSeeHtml('dropdownOpen')
|
||||
->assertSeeHtml('x-ref="panel"')
|
||||
->assertSeeHtml('dark:bg-coolgray-100')
|
||||
->assertSeeHtml('dark:bg-transparent')
|
||||
->assertSeeHtml('@click.outside="if (! true) close()"');
|
||||
});
|
||||
|
||||
test('shows advanced Hetzner options when expanded', function () {
|
||||
test('renders advanced Hetzner option controls inside the dropdown menu', function () {
|
||||
Livewire::test(ByHetzner::class)
|
||||
->set('current_step', 2)
|
||||
->set('show_advanced_hetzner_options', true)
|
||||
->assertSee('Extra SSH Keys')
|
||||
->assertSee('Firewalls')
|
||||
->assertSee('Private Networks')
|
||||
|
|
@ -216,7 +214,6 @@
|
|||
test('shows the cloud init script name only when saving the script', function () {
|
||||
Livewire::test(ByHetzner::class)
|
||||
->set('current_step', 2)
|
||||
->set('show_advanced_hetzner_options', true)
|
||||
->set('show_cloud_init_script', true)
|
||||
->assertSee('Cloud-Init Script')
|
||||
->assertSee('Save this script for later use')
|
||||
|
|
|
|||
Loading…
Reference in a new issue