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

+84
View File
@@ -97,6 +97,90 @@
}
}
@keyframes coolbox-border-track {
0% {
background-position: 0 0, 100% 0, 100% 100%, 0 100%;
background-size: 35% 2px, 0 0, 0 0, 0 0;
}
33% {
background-position: 100% 0, 100% 0, 100% 100%, 0 100%;
background-size: 35% 2px, 0 0, 0 0, 0 0;
}
34% {
background-position: 100% 0, 100% 0, 100% 100%, 0 100%;
background-size: 0 0, 2px 35%, 0 0, 0 0;
}
50% {
background-position: 100% 0, 100% 100%, 100% 100%, 0 100%;
background-size: 0 0, 2px 35%, 0 0, 0 0;
}
51% {
background-position: 0 0, 100% 0, 100% 100%, 0 100%;
background-size: 0 0, 0 0, 35% 2px, 0 0;
}
84% {
background-position: 0 0, 100% 0, 0 100%, 0 100%;
background-size: 0 0, 0 0, 35% 2px, 0 0;
}
85% {
background-position: 0 0, 100% 0, 100% 100%, 0 100%;
background-size: 0 0, 0 0, 0 0, 2px 35%;
}
100% {
background-position: 0 0, 100% 0, 100% 100%, 0 0;
background-size: 0 0, 0 0, 0 0, 2px 35%;
}
}
@layer components {
.coolbox-loading {
@apply overflow-hidden border-transparent;
isolation: isolate;
}
.coolbox-loading::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
background:
linear-gradient(var(--color-warning), var(--color-warning)),
linear-gradient(var(--color-warning), var(--color-warning)),
linear-gradient(var(--color-warning), var(--color-warning)),
linear-gradient(var(--color-warning), var(--color-warning));
background-repeat: no-repeat;
animation: coolbox-border-track 2400ms linear infinite;
pointer-events: none;
z-index: 0;
}
.coolbox-loading::after {
content: "";
position: absolute;
inset: 2px;
border-radius: calc(0.25rem - 2px);
background: white;
pointer-events: none;
z-index: 1;
}
.dark .coolbox-loading::after {
background: var(--color-coolgray-100);
}
.coolbox-loading > * {
position: relative;
z-index: 2;
}
}
/*
* Base styles
*/
@@ -1,4 +1,10 @@
<svg {{ $attributes->merge(['class' => 'w-6 h-6 flex-shrink-0']) }} fill="#0080FF" role="img" viewBox="0 0 24 24"
@php
$iconAttributes = $attributes->has('class')
? $attributes->class('shrink-0')
: $attributes->merge(['class' => 'size-6 shrink-0']);
@endphp
<svg {{ $iconAttributes }} fill="#0080FF" role="img" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<title>DigitalOcean</title>
<path

Before

Width:  |  Height:  |  Size: 534 B

After

Width:  |  Height:  |  Size: 658 B

+11 -16
View File
@@ -85,19 +85,14 @@
<h3>Servers</h3>
@can('create', App\Models\Server::class)
@if ($servers->count() > 0 && $privateKeys->count() > 0)
<x-modal-input buttonTitle="Add" title="New Server" :closeOutside="false">
<x-slot:content>
<button
class="flex items-center justify-center size-4 text-black dark:text-white rounded hover:bg-coolgray-400 dark:hover:bg-coolgray-300 cursor-pointer">
<svg class="size-3" xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
d="M12 4.5v15m7.5-7.5h-15" />
</svg>
</button>
</x-slot:content>
<livewire:server.create />
</x-modal-input>
<a href="{{ route('server.create') }}" {{ wireNavigate() }}
class="flex items-center justify-center size-4 text-black dark:text-white rounded hover:bg-coolgray-400 dark:hover:bg-coolgray-300 cursor-pointer">
<svg class="size-3" xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
d="M12 4.5v15m7.5-7.5h-15" />
</svg>
</a>
@endif
@endcan
</div>
@@ -154,9 +149,9 @@
<div class='font-bold dark:text-warning'>No servers found.</div>
@can('create', App\Models\Server::class)
<div class="flex items-center gap-1">
<x-modal-input buttonTitle="Add" title="New Server" :closeOutside="false">
<livewire:server.create />
</x-modal-input> your first server
<a href="{{ route('server.create') }}" {{ wireNavigate() }}>
<x-forms.button>Add</x-forms.button>
</a> your first server
or
go to the <a class="underline dark:text-white"
href="{{ route('onboarding') }}"
@@ -245,6 +245,10 @@
if (value) {
// Close search modal first
this.closeModal();
if (value === 'server') {
window.location.href = '/servers/new';
return;
}
// Open the specific resource modal after a short delay
setTimeout(() => {
this.$dispatch('open-create-modal-' + value);
@@ -705,9 +709,9 @@
</div>
@else
<div
class="flex-shrink-0 w-10 h-10 rounded-lg bg-warning-100 dark:bg-warning-900/40 flex items-center justify-center">
class="flex-shrink-0 w-10 h-10 rounded-full bg-warning/20 flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5 text-warning-600 dark:text-warning-400" fill="none"
class="h-6 w-6 text-warning" fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 4v16m8-8H4" />
@@ -821,9 +825,9 @@
</template>
<template x-if="!item.logo">
<div
class="flex-shrink-0 w-10 h-10 rounded-lg bg-warning-100 dark:bg-warning-900/40 flex items-center justify-center">
class="flex-shrink-0 w-10 h-10 rounded-full bg-warning/20 flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5 text-warning-600 dark:text-warning-400"
class="h-6 w-6 text-warning"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M12 4v16m8-8H4" />
@@ -937,46 +941,6 @@
</template>
</div>
<div x-data="{ modalOpen: false }" @open-create-modal-server.window="modalOpen = true"
@keydown.window.escape="modalOpen=false" class="relative w-auto h-auto">
<template x-teleport="body">
<div x-show="modalOpen" x-init="$watch('modalOpen', value => {
if (value) {
setTimeout(() => {
const firstInput = $el.querySelector('input, textarea, select');
if (firstInput) firstInput.focus();
}, 200);
}
})" class="fixed top-0 left-0 lg:px-0 px-4 z-99 flex items-center justify-center w-screen h-screen">
<div x-show="modalOpen" x-transition:enter="ease-out duration-100" x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100" x-transition:leave="ease-in duration-100"
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" @click="modalOpen=false"
class="absolute inset-0 w-full h-full bg-black/20 backdrop-blur-xs"></div>
<div x-show="modalOpen" x-trap.inert.noscroll="modalOpen" x-transition:enter="ease-out duration-100"
x-transition:enter-start="opacity-0 -translate-y-2 sm:scale-95"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave="ease-in duration-100"
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
class="relative w-full py-6 border rounded-sm drop-shadow-sm min-w-full lg:min-w-[36rem] max-w-fit bg-white border-neutral-200 dark:bg-base px-6 dark:border-coolgray-300">
<div class="flex items-center justify-between pb-3">
<h3 class="text-2xl font-bold">New Server</h3>
<button @click="modalOpen=false"
class="absolute top-0 right-0 flex items-center justify-center w-8 h-8 mt-5 mr-5 rounded-full dark:text-white hover:bg-neutral-100 dark:hover:bg-coolgray-300 outline-0 focus-visible:ring-2 focus-visible:ring-coollabs dark:focus-visible:ring-warning">
<svg class="w-5 h-5" 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="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="relative flex items-center justify-center w-auto">
<livewire:server.create key="create-modal-server" />
</div>
</div>
</div>
</template>
</div>
<div x-data="{ modalOpen: false }" @open-create-modal-team.window="modalOpen = true"
@keydown.window.escape="modalOpen=false" class="relative w-auto h-auto">
<template x-teleport="body">
@@ -8,28 +8,28 @@
@forelse ($private_keys as $key)
@if ($private_key_id == $key->id)
<div class="gap-2 py-4 cursor-pointer group coolbox"
wire:click="setPrivateKey('{{ $key->id }}')" wire:key="{{ $key->id }}">
wire:click="setPrivateKey('{{ $key->id }}')"
wire:loading.class="coolbox-loading"
wire:target="setPrivateKey('{{ $key->id }}')" wire:key="{{ $key->id }}">
<div class="flex flex-col mx-6">
<div class="box-title">
{{ $key->name }}
</div>
<div class="box-description">
{{ $key->description }}</div>
<span wire:target="loadRepositories" wire:loading.delay
class="loading loading-xs dark:text-warning loading-spinner"></span>
</div>
</div>
@else
<div class="gap-2 py-4 cursor-pointer group coolbox"
wire:click="setPrivateKey('{{ $key->id }}')" wire:key="{{ $key->id }}">
wire:click="setPrivateKey('{{ $key->id }}')"
wire:loading.class="coolbox-loading"
wire:target="setPrivateKey('{{ $key->id }}')" wire:key="{{ $key->id }}">
<div class="flex flex-col mx-6">
<div class="box-title">
{{ $key->name }}
</div>
<div class="box-description">
{{ $key->description }}</div>
<span wire:target="loadRepositories" wire:loading.delay
class="loading loading-xs dark:text-warning loading-spinner"></span>
</div>
</div>
@endif
@@ -25,6 +25,8 @@
<div class="flex">
<div class="w-full gap-2 py-4 group coolbox"
wire:click.prevent="loadRepositories({{ $ghapp->id }})"
wire:loading.class="coolbox-loading"
wire:target="loadRepositories({{ $ghapp->id }})"
wire:key="{{ $ghapp->id }}">
<div class="flex mr-4">
<div class="flex flex-col mx-6">
@@ -36,9 +38,6 @@
</div>
</div>
</div>
<div class="flex flex-col items-center justify-center">
<x-loading wire:loading wire:target="loadRepositories({{ $ghapp->id }})" />
</div>
</div>
@endforeach
</div>
@@ -75,7 +75,9 @@
</div>
</div>
</div>
<div x-show="loading">Loading...</div>
<div x-show="loading" class="flex items-center justify-center py-8">
<x-loading text="Loading resources..." />
</div>
<div x-show="!loading" class="flex flex-col gap-4 py-4">
<h2 x-show="filteredGitBasedApplications.length > 0">Applications</h2>
<div x-show="filteredGitBasedApplications.length > 0 || filteredDockerBasedApplications.length > 0"
@@ -24,23 +24,12 @@
target='_blank' class='underline dark:text-white'>{{ $provider === 'digitalocean' ? 'DigitalOcean' : ucfirst($provider) }} Console</a>.
@if ($provider === 'hetzner')
Choose Project → Security → API Tokens.
<br><br>
Don't have a Hetzner account? <a href='https://coolify.io/hetzner' target='_blank'
class='underline dark:text-white'>Sign up here</a>
<br>
<span class="text-xs">(Coolify's affiliate link, only new accounts - supports us (€10)
and gives you €20)</span>
@endif
@if ($provider === 'digitalocean')
Generate New Token.
@endif
@if ($provider === 'vultr')
Open Account → API Access.
<br><br>
Don't have a Vultr account? <a href='https://www.vultr.com/?ref=9911335' target='_blank'
class='underline dark:text-white'>Sign up here</a>
<br>
<span class="text-xs">Coolify's affiliate link, only new accounts - supports us</span>
@endif
</div>
@@ -68,23 +57,12 @@
target='_blank' class='underline dark:text-white'>{{ $provider === 'digitalocean' ? 'DigitalOcean' : ucfirst($provider) }} Console</a>.
@if ($provider === 'hetzner')
Choose Project → Security → API Tokens.
<br><br>
Don't have a Hetzner account? <a href='https://coolify.io/hetzner' target='_blank'
class='underline dark:text-white'>Sign up here</a>
<br>
<span class="text-xs">(Coolify's affiliate link, only new accounts - supports us (€10)
and gives you €20)</span>
@endif
@if ($provider === 'digitalocean')
Generate New Token.
@endif
@if ($provider === 'vultr')
Open Account → API Access.
<br><br>
Don't have a Vultr account? <a href='https://www.vultr.com/?ref=9911335' target='_blank'
class='underline dark:text-white'>Sign up here</a>
<br>
<span class="text-xs">Coolify's affiliate link, only new accounts - supports us</span>
@endif
</div>
</div>
@@ -0,0 +1,29 @@
<div class="flex flex-col gap-4">
<x-slot:title>
{{ $title }} | Coolify
</x-slot>
<div>
<div class="flex items-center gap-2">
<h1>{{ $title }}</h1>
@if ($type)
<a href="{{ $token_uuid ? route('server.create.type', ['type' => $type]) : route('server.create') }}" {{ wireNavigate() }}>
<x-forms.button>Back</x-forms.button>
</a>
@if (! $token_uuid && $hasProviderTokens && $tokenProvider)
@can('create', App\Models\CloudProviderToken::class)
<x-modal-input buttonTitle="+ New Token" title="Add {{ $tokenProviderName }} Token" isHighlightedButton>
<livewire:security.cloud-provider-token-form :modal_mode="true" :provider="$tokenProvider"
wire:key="new-server-header-token-{{ $tokenProvider }}" />
</x-modal-input>
@endcan
@endif
@endif
</div>
<div class="subtitle">Add a server to deploy your applications and databases.</div>
</div>
<div class="w-full">
<livewire:server.create :selected-type="$type" :selected-token-uuid="$token_uuid" />
</div>
</div>
+100 -61
View File
@@ -1,78 +1,117 @@
<div class="w-full">
<div class="flex flex-col gap-4">
@can('viewAny', App\Models\CloudProviderToken::class)
<div>
<x-modal-input title="Connect a Hetzner Server">
<x-slot:content>
<div class="relative gap-2 cursor-pointer coolbox group">
<div class="flex items-center gap-4 mx-6">
<svg class="w-10 h-10 flex-shrink-0" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
@if (!$selectedType)
<div class="flex flex-col items-center justify-center gap-3 sm:gap-6 sm:min-h-[calc(100vh-12rem)]">
<div class="mx-auto flex w-full max-w-7xl flex-col gap-3 sm:grid sm:grid-cols-2 sm:gap-6 xl:grid-cols-4">
@can('viewAny', App\Models\CloudProviderToken::class)
<a href="{{ route('server.create.type', ['type' => 'hetzner']) }}" aria-label="Choose Hetzner"
class="box-without-bg flex-col gap-3 sm:gap-6 p-3 sm:p-6 h-full cursor-pointer transition-all duration-200 hover:border-coollabs dark:hover:border-warning focus-visible:border-coollabs dark:focus-visible:border-warning focus-visible:outline-none"
{{ wireNavigate() }}>
<div class="flex flex-col gap-2 sm:gap-4 text-left h-full">
<div class="flex items-center justify-between">
<svg class="size-9 sm:size-14" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<rect width="200" height="200" fill="#D50C2D" rx="8" />
<path d="M40 40 H60 V90 H140 V40 H160 V160 H140 V110 H60 V160 H40 Z" fill="white" />
<path d="M40 40 H60 V90 H140 V40 H160 V160 H140 V110 H60 V160 H40 Z"
fill="white" />
</svg>
<div class="flex flex-col justify-center flex-1">
<div class="box-title">Connect a Hetzner Server</div>
<div class="box-description">
Deploy servers directly from your Hetzner Cloud account
</div>
</div>
<span
class="px-1.5 py-0.5 text-[10px] sm:px-2 sm:py-1 sm:text-xs font-bold uppercase tracking-wide bg-coollabs/10 dark:bg-warning/20 text-coollabs dark:text-warning rounded">
Provider
</span>
</div>
<div>
<h3 class="text-sm sm:text-xl font-bold mb-1 sm:mb-2">Hetzner</h3>
<p class="hidden sm:block text-sm dark:text-neutral-400">
Deploy servers directly from your Hetzner Cloud account.
</p>
</div>
</div>
</x-slot:content>
<livewire:server.new.by-hetzner :private_keys="$private_keys" :limit_reached="$limit_reached" />
</x-modal-input>
</div>
</a>
<div>
<x-modal-input title="Connect a Vultr Server">
<x-slot:content>
<div class="relative gap-2 cursor-pointer coolbox group">
<div class="flex items-center gap-4 mx-6">
<svg class="w-10 h-10 flex-shrink-0" viewBox="0 0 200 200"
xmlns="http://www.w3.org/2000/svg">
<a href="{{ route('server.create.type', ['type' => 'vultr']) }}" aria-label="Choose Vultr"
class="box-without-bg flex-col gap-3 sm:gap-6 p-3 sm:p-6 h-full cursor-pointer transition-all duration-200 hover:border-coollabs dark:hover:border-warning focus-visible:border-coollabs dark:focus-visible:border-warning focus-visible:outline-none"
{{ wireNavigate() }}>
<div class="flex flex-col gap-2 sm:gap-4 text-left h-full">
<div class="flex items-center justify-between">
<svg class="size-9 sm:size-14" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<rect width="200" height="200" fill="#007BFC" rx="8" />
<path d="M42 46 H73 L100 127 L127 46 H158 L114 154 H86 Z" fill="white" />
</svg>
<div class="flex flex-col justify-center flex-1">
<div class="box-title">Connect a Vultr Server</div>
<div class="box-description">
Deploy servers directly from your Vultr account
</div>
</div>
<span
class="px-1.5 py-0.5 text-[10px] sm:px-2 sm:py-1 sm:text-xs font-bold uppercase tracking-wide bg-coollabs/10 dark:bg-warning/20 text-coollabs dark:text-warning rounded">
Provider
</span>
</div>
<div>
<h3 class="text-sm sm:text-xl font-bold mb-1 sm:mb-2">Vultr</h3>
<p class="hidden sm:block text-sm dark:text-neutral-400">
Deploy servers directly from your Vultr account.
</p>
</div>
</div>
</x-slot:content>
<livewire:server.new.by-vultr :private_keys="$private_keys" :limit_reached="$limit_reached" />
</x-modal-input>
</div>
</a>
<div class="border-t dark:border-coolgray-300 my-4"></div>
<div>
<x-modal-input title="Connect a DigitalOcean Droplet">
<x-slot:content>
<div class="relative gap-2 cursor-pointer coolbox group">
<div class="flex items-center gap-4 mx-6">
<x-digital-ocean-icon class="w-10 h-10 flex-shrink-0" />
<div class="flex flex-col justify-center flex-1">
<div class="box-title">Connect a DigitalOcean Droplet</div>
<div class="box-description">
Deploy servers directly from your DigitalOcean account
</div>
</div>
<a href="{{ route('server.create.type', ['type' => 'digital-ocean']) }}"
aria-label="Choose DigitalOcean"
class="box-without-bg flex-col gap-3 sm:gap-6 p-3 sm:p-6 h-full cursor-pointer transition-all duration-200 hover:border-coollabs dark:hover:border-warning focus-visible:border-coollabs dark:focus-visible:border-warning focus-visible:outline-none"
{{ wireNavigate() }}>
<div class="flex flex-col gap-2 sm:gap-4 text-left h-full">
<div class="flex items-center justify-between">
<x-digital-ocean-icon class="size-9 sm:size-14" />
<span
class="px-1.5 py-0.5 text-[10px] sm:px-2 sm:py-1 sm:text-xs font-bold uppercase tracking-wide bg-coollabs/10 dark:bg-warning/20 text-coollabs dark:text-warning rounded">
Provider
</span>
</div>
<div>
<h3 class="text-sm sm:text-xl font-bold mb-1 sm:mb-2">DigitalOcean</h3>
<p class="hidden sm:block text-sm dark:text-neutral-400">
Deploy droplets directly from your DigitalOcean account.
</p>
</div>
</div>
</x-slot:content>
<livewire:server.new.by-digital-ocean :private_keys="$private_keys" :limit_reached="$limit_reached" />
</x-modal-input>
</a>
@endcan
<a href="{{ route('server.create.type', ['type' => 'manual']) }}" aria-label="Choose Manual"
class="box-without-bg flex-col gap-3 sm:gap-6 p-3 sm:p-6 h-full cursor-pointer transition-all duration-200 hover:border-coollabs dark:hover:border-warning focus-visible:border-coollabs dark:focus-visible:border-warning focus-visible:outline-none"
{{ wireNavigate() }}>
<div class="flex flex-col gap-2 sm:gap-4 text-left h-full">
<div class="flex items-center justify-between">
<svg class="size-9 sm:size-14" 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="M11.42 15.17L17.25 21A2.652 2.652 0 0021 17.25l-5.877-5.877M11.42 15.17l2.496-3.03c.317-.384.74-.626 1.208-.766M11.42 15.17l-4.655 5.653a2.548 2.548 0 11-3.586-3.586l6.837-5.63m5.108-.233c.55-.164 1.163-.188 1.743-.14a4.5 4.5 0 004.486-6.336l-3.276 3.277a3.004 3.004 0 01-2.25-2.25l3.276-3.276a4.5 4.5 0 00-6.336 4.486c.091 1.076-.071 2.264-.904 2.95l-.102.085m-1.745 1.437L5.909 7.5H4.5L2.25 3.75l1.5-1.5L7.5 4.5v1.409l4.26 4.26m-1.745 1.437l1.745-1.437m6.615 8.206L15.75 15.75M4.867 19.125h.008v.008h-.008v-.008z" />
</svg>
<span
class="px-1.5 py-0.5 text-[10px] sm:px-2 sm:py-1 sm:text-xs font-bold uppercase tracking-wide bg-neutral-100 dark:bg-coolgray-300 dark:text-neutral-400 rounded">
Manual
</span>
</div>
<div>
<h3 class="text-sm sm:text-xl font-bold mb-1 sm:mb-2">Manual</h3>
<p class="hidden sm:block text-sm dark:text-neutral-400">
Add any reachable server by IP address or domain.
</p>
</div>
</div>
</a>
</div>
<div class="border-t dark:border-coolgray-300 my-4"></div>
@endcan
<div>
<h3 class="pb-2">Add Server by IP Address</h3>
<livewire:server.new.by-ip :private_keys="$private_keys" :limit_reached="$limit_reached" />
</div>
</div>
@else
<div class="flex flex-col gap-4">
@if ($selectedType === 'hetzner')
<livewire:server.new.by-hetzner :private_keys="$private_keys" :limit_reached="$limit_reached"
:selected-token-uuid="$selectedTokenUuid" wire:key="new-server-hetzner-{{ $selectedTokenUuid ?? 'select' }}" />
@elseif ($selectedType === 'vultr')
<livewire:server.new.by-vultr :private_keys="$private_keys" :limit_reached="$limit_reached"
:selected-token-uuid="$selectedTokenUuid" wire:key="new-server-vultr-{{ $selectedTokenUuid ?? 'select' }}" />
@elseif ($selectedType === 'digital-ocean')
<livewire:server.new.by-digital-ocean :private_keys="$private_keys" :limit_reached="$limit_reached"
:selected-token-uuid="$selectedTokenUuid" wire:key="new-server-digital-ocean-{{ $selectedTokenUuid ?? 'select' }}" />
@else
<livewire:server.new.by-ip :private_keys="$private_keys" :limit_reached="$limit_reached"
key="new-server-manual" />
@endif
</div>
@endif
</div>
@@ -5,9 +5,9 @@
<div class="flex items-center gap-2">
<h1>Servers</h1>
@can('createAnyResource')
<x-modal-input buttonTitle="+ Add" title="New Server" :closeOutside="false">
<livewire:server.create />
</x-modal-input>
<a href="{{ route('server.create') }}" {{ wireNavigate() }}>
<x-forms.button>+ Add</x-forms.button>
</a>
@endcan
</div>
<div class="subtitle">All your servers are here.</div>
@@ -4,43 +4,61 @@
@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 DigitalOcean account? <a href="https://coolify.io/digitalocean" target="_blank"
class="underline dark:text-white">Sign up here</a>.
<span class="text-xs">Coolify's referral link - it supports both of us.</span>
</div>
@if ($available_tokens->count() > 0)
<div class="flex gap-2">
<div class="flex-1">
<x-forms.select label="Select DigitalOcean 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 }}">
<div class="grid gap-3 md:grid-cols-2">
@foreach ($available_tokens as $token)
<a class="coolbox group text-left" wire:key="digital-ocean-token-{{ $token->id }}"
href="{{ route('server.create.token', ['type' => 'digital-ocean', 'token_uuid' => $token->uuid]) }}" {{ wireNavigate() }}>
<div class="flex flex-col justify-center mx-6">
<div class="box-title">
{{ $token->name ?? 'DigitalOcean 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="box-description">
Use this token to create a DigitalOcean Droplet.
</div>
</div>
</a>
@endforeach
</div>
@else
<div class="w-full max-w-2xl">
<x-modal-input title="Add DigitalOcean 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 DigitalOcean API token to create Droplets from your account.
</div>
</div>
</div>
</div>
</x-slot:content>
<livewire:security.cloud-provider-token-form :modal_mode="true" provider="digitalocean"
wire:key="new-server-empty-token-digitalocean" />
</x-modal-input>
</div>
<div class="text-center text-sm dark:text-neutral-500">OR</div>
@endif
<x-modal-input isFullWidth
buttonTitle="{{ $available_tokens->count() > 0 ? '+ Add New Token' : 'Add DigitalOcean Token' }}"
title="Add DigitalOcean Token">
<livewire:security.cloud-provider-token-form :modal_mode="true" provider="digitalocean" />
</x-modal-input>
</div>
@elseif ($current_step === 2)
<div wire:init="loadDigitalOceanData">
@if ($loading_data)
<div class="flex items-center justify-center py-8">
<div class="text-center">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto"></div>
<p class="mt-4 text-sm dark:text-neutral-400">Loading DigitalOcean data...</p>
</div>
<x-loading text="Loading DigitalOcean data..." />
</div>
@else
<form class="flex flex-col w-full gap-2" wire:submit='submit'>
@@ -211,17 +229,13 @@
</div>
</x-dropdown>
<div class="flex gap-2 justify-between">
<x-forms.button type="button" wire:click="previousStep">
Back
</x-forms.button>
<x-forms.button isHighlighted canGate="create" :canResource="App\Models\Server::class" type="submit"
:disabled="!$private_key_id">
Buy & Create Server{{ $this->selectedDropletPrice ? ' (' . $this->selectedDropletPrice . '/mo)' : '' }}
</x-forms.button>
</div>
<x-forms.button class="w-full" isHighlighted canGate="create" :canResource="App\Models\Server::class"
type="submit" :disabled="!$private_key_id">
Buy & Create Server{{ $this->selectedDropletPrice ? ' (' . $this->selectedDropletPrice . '/mo)' : '' }}
</x-forms.button>
</form>
@endif
</div>
@endif
@endif
</div>
@@ -4,43 +4,61 @@
@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="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 }}">
<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' }}
</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="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>
<div class="text-center text-sm dark:text-neutral-500">OR</div>
@endif
<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)
<div wire:init="loadHetznerData">
@if ($loading_data)
<div class="flex items-center justify-center py-8">
<div class="text-center">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto"></div>
<p class="mt-4 text-sm dark:text-neutral-400">Loading Hetzner data...</p>
</div>
<x-loading text="Loading Hetzner data..." />
</div>
@else
<form class="flex flex-col w-full gap-2" wire:submit='submit'>
@@ -248,17 +266,13 @@
</div>
</x-dropdown>
<div class="flex gap-2 justify-between">
<x-forms.button type="button" wire:click="previousStep">
Back
</x-forms.button>
<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>
</div>
</form>
@endif
</div>
@endif
@endif
</div>
@@ -4,43 +4,61 @@
@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 Vultr account? <a href="https://coolify.io/vultr" target="_blank"
class="underline dark:text-white">Sign up here</a>.
<span class="text-xs">Coolify's affiliate link - it supports both of us.</span>
</div>
@if ($available_tokens->count() > 0)
<div class="flex gap-2">
<div class="flex-1">
<x-forms.select label="Select Vultr 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 }}">
<div class="grid gap-3 md:grid-cols-2">
@foreach ($available_tokens as $token)
<a class="coolbox group text-left" wire:key="vultr-token-{{ $token->id }}"
href="{{ route('server.create.token', ['type' => 'vultr', 'token_uuid' => $token->uuid]) }}" {{ wireNavigate() }}>
<div class="flex flex-col justify-center mx-6">
<div class="box-title">
{{ $token->name ?? 'Vultr 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="box-description">
Use this token to create a Vultr server.
</div>
</div>
</a>
@endforeach
</div>
@else
<div class="w-full max-w-2xl">
<x-modal-input title="Add Vultr 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 Vultr 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="vultr"
wire:key="new-server-empty-token-vultr" />
</x-modal-input>
</div>
<div class="text-center text-sm dark:text-neutral-500">OR</div>
@endif
<x-modal-input isFullWidth
buttonTitle="{{ $available_tokens->count() > 0 ? '+ Add New Token' : 'Add Vultr Token' }}"
title="Add Vultr Token">
<livewire:security.cloud-provider-token-form :modal_mode="true" provider="vultr" />
</x-modal-input>
</div>
@elseif ($current_step === 2)
<div wire:init="loadVultrData">
@if ($loading_data)
<div class="flex items-center justify-center py-8">
<div class="text-center">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto"></div>
<p class="mt-4 text-sm dark:text-neutral-400">Loading Vultr data...</p>
</div>
<x-loading text="Loading Vultr data..." />
</div>
@else
<form class="flex flex-col w-full gap-2" wire:submit='submit'>
@@ -125,68 +143,82 @@
@endif
</div>
<div>
<x-forms.datalist label="Additional SSH Keys (from Vultr)" id="selectedVultrSshKeyIds"
helper="Select existing SSH keys from your Vultr account to add to this server. The Coolify SSH key will be automatically added."
:multiple="true" :disabled="count($vultrSshKeys) === 0" :placeholder="count($vultrSshKeys) > 0 ? 'Search and select SSH keys...' : 'No SSH keys found in Vultr account'">
@foreach ($vultrSshKeys as $sshKey)
<option value="{{ $sshKey['id'] }}">
{{ $sshKey['name'] }} - {{ substr($sshKey['id'], 0, 20) }}
</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_ipv6" label="Enable IPv6"
helper="Enable public IPv6 address for this server" />
<x-forms.checkbox id="disable_public_ipv4" label="Disable public IPv4"
helper="Disable the default public IPv4 address" />
</div>
</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 Vultr options</span>
<span class="mt-1 text-xs text-neutral-500 dark:text-neutral-400">SSH keys, network configuration, and cloud-init.</span>
@if (count($this->advancedVultrOptionsSummary) > 0)
<span class="mt-1 flex flex-wrap gap-1.5">
@foreach ($this->advancedVultrOptionsSummary 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. Coolify sends it to Vultr as user data."
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="Additional SSH Keys (from Vultr)" id="selectedVultrSshKeyIds"
helper="Select existing SSH keys from your Vultr account to add to this server. The Coolify SSH key will be automatically added."
:multiple="true" :disabled="count($vultrSshKeys) === 0" :placeholder="count($vultrSshKeys) > 0 ? 'Search and select SSH keys...' : 'No SSH keys found in Vultr account'">
@foreach ($vultrSshKeys as $sshKey)
<option value="{{ $sshKey['id'] }}">
{{ $sshKey['name'] }} - {{ substr($sshKey['id'], 0, 20) }}
</option>
@endforeach
</x-forms.datalist>
</div>
<div class="flex flex-col gap-2">
<label class="text-sm font-medium">Network Configuration</label>
<div class="grid grid-cols-1 gap-2 md:grid-cols-2">
<x-forms.checkbox id="enable_ipv6" label="Enable IPv6"
helper="Enable public IPv6 address for this server" fullWidth />
<x-forms.checkbox id="disable_public_ipv4" label="Disable public IPv4"
helper="Disable the default public IPv4 address" fullWidth />
</div>
</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>
@endif
</div>
<x-forms.textarea id="cloud_init_script" label=""
helper="Add a cloud-init script to run when the server is created. Coolify sends it to Vultr as user data."
rows="8" />
<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>
</div>
</div>
</div>
</div>
</x-dropdown>
<div class="flex gap-2 justify-between">
<x-forms.button type="button" wire:click="previousStep">
Back
</x-forms.button>
<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>
</div>
<x-forms.button class="w-full" 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>