Merge pull request #6855 from coollabsio/andrasbacsai/onboarding-redesign

feat(onboarding): redesign user onboarding flow with modern UI/UX
This commit is contained in:
Andras Bacsai 2025-10-12 18:56:55 +02:00 committed by GitHub
commit ecdeed7787
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 867 additions and 444 deletions

View file

@ -16,14 +16,18 @@ class Index extends Component
{ {
protected $listeners = ['refreshBoardingIndex' => 'validateServer']; protected $listeners = ['refreshBoardingIndex' => 'validateServer'];
#[\Livewire\Attributes\Url(as: 'step', history: true)]
public string $currentState = 'welcome'; public string $currentState = 'welcome';
#[\Livewire\Attributes\Url(keep: true)]
public ?string $selectedServerType = null; public ?string $selectedServerType = null;
public ?Collection $privateKeys = null; public ?Collection $privateKeys = null;
#[\Livewire\Attributes\Url(keep: true)]
public ?int $selectedExistingPrivateKey = null; public ?int $selectedExistingPrivateKey = null;
#[\Livewire\Attributes\Url(keep: true)]
public ?string $privateKeyType = null; public ?string $privateKeyType = null;
public ?string $privateKey = null; public ?string $privateKey = null;
@ -38,6 +42,7 @@ class Index extends Component
public ?Collection $servers = null; public ?Collection $servers = null;
#[\Livewire\Attributes\Url(keep: true)]
public ?int $selectedExistingServer = null; public ?int $selectedExistingServer = null;
public ?string $remoteServerName = null; public ?string $remoteServerName = null;
@ -58,6 +63,7 @@ class Index extends Component
public Collection $projects; public Collection $projects;
#[\Livewire\Attributes\Url(keep: true)]
public ?int $selectedProject = null; public ?int $selectedProject = null;
public ?Project $createdProject = null; public ?Project $createdProject = null;
@ -79,17 +85,68 @@ public function mount()
$this->minDockerVersion = str(config('constants.docker.minimum_required_version'))->before('.'); $this->minDockerVersion = str(config('constants.docker.minimum_required_version'))->before('.');
$this->privateKeyName = generate_random_name(); $this->privateKeyName = generate_random_name();
$this->remoteServerName = generate_random_name(); $this->remoteServerName = generate_random_name();
if (isDev()) {
$this->privateKey = '-----BEGIN OPENSSH PRIVATE KEY----- // Initialize collections to avoid null errors
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW if ($this->privateKeys === null) {
QyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevAAAAJi/QySHv0Mk $this->privateKeys = collect();
hwAAAAtzc2gtZWQyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevA }
AAAECBQw4jg1WRT2IGHMncCiZhURCts2s24HoDS0thHnnRKVuGmoeGq/pojrsyP1pszcNV if ($this->servers === null) {
uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== $this->servers = collect();
-----END OPENSSH PRIVATE KEY-----'; }
$this->privateKeyDescription = 'Created by Coolify'; if (! isset($this->projects)) {
$this->remoteServerDescription = 'Created by Coolify'; $this->projects = collect();
$this->remoteServerHost = 'coolify-testing-host'; }
// Restore state when coming from URL with query params
if ($this->selectedServerType === 'localhost' && $this->selectedExistingServer === 0) {
$this->createdServer = Server::find(0);
if ($this->createdServer) {
$this->serverPublicKey = $this->createdServer->privateKey->getPublicKey();
}
}
if ($this->selectedServerType === 'remote') {
if ($this->privateKeys->isEmpty()) {
$this->privateKeys = PrivateKey::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get();
}
if ($this->servers->isEmpty()) {
$this->servers = Server::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get();
}
if ($this->selectedExistingServer) {
$this->createdServer = Server::find($this->selectedExistingServer);
if ($this->createdServer) {
$this->serverPublicKey = $this->createdServer->privateKey->getPublicKey();
$this->updateServerDetails();
}
}
if ($this->selectedExistingPrivateKey) {
$this->createdPrivateKey = PrivateKey::where('team_id', currentTeam()->id)
->where('id', $this->selectedExistingPrivateKey)
->first();
if ($this->createdPrivateKey) {
$this->privateKey = $this->createdPrivateKey->private_key;
$this->publicKey = $this->createdPrivateKey->getPublicKey();
}
}
// Auto-regenerate key pair for "Generate with Coolify" mode on page refresh
if ($this->privateKeyType === 'create' && empty($this->privateKey)) {
$this->createNewPrivateKey();
}
}
if ($this->selectedProject) {
$this->createdProject = Project::find($this->selectedProject);
if (! $this->createdProject) {
$this->projects = Project::ownedByCurrentTeam(['name'])->get();
}
}
// Load projects when on create-project state (for page refresh)
if ($this->currentState === 'create-project' && $this->projects->isEmpty()) {
$this->projects = Project::ownedByCurrentTeam(['name'])->get();
} }
} }
@ -129,41 +186,16 @@ public function setServerType(string $type)
return $this->validateServer('localhost'); return $this->validateServer('localhost');
} elseif ($this->selectedServerType === 'remote') { } elseif ($this->selectedServerType === 'remote') {
if (isDev()) {
$this->privateKeys = PrivateKey::ownedByCurrentTeam(['name'])->get();
} else {
$this->privateKeys = PrivateKey::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get(); $this->privateKeys = PrivateKey::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get();
} // Auto-select first key if available for better UX
if ($this->privateKeys->count() > 0) { if ($this->privateKeys->count() > 0) {
$this->selectedExistingPrivateKey = $this->privateKeys->first()->id; $this->selectedExistingPrivateKey = $this->privateKeys->first()->id;
} }
$this->servers = Server::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get(); // Onboarding always creates new servers, skip existing server selection
if ($this->servers->count() > 0) {
$this->selectedExistingServer = $this->servers->first()->id;
$this->updateServerDetails();
$this->currentState = 'select-existing-server';
return;
}
$this->currentState = 'private-key'; $this->currentState = 'private-key';
} }
} }
public function selectExistingServer()
{
$this->createdServer = Server::find($this->selectedExistingServer);
if (! $this->createdServer) {
$this->dispatch('error', 'Server is not found.');
$this->currentState = 'private-key';
return;
}
$this->selectedExistingPrivateKey = $this->createdServer->privateKey->id;
$this->serverPublicKey = $this->createdServer->privateKey->getPublicKey();
$this->updateServerDetails();
$this->currentState = 'validate-server';
}
private function updateServerDetails() private function updateServerDetails()
{ {
if ($this->createdServer) { if ($this->createdServer) {
@ -181,7 +213,7 @@ public function getProxyType()
public function selectExistingPrivateKey() public function selectExistingPrivateKey()
{ {
if (is_null($this->selectedExistingPrivateKey)) { if (is_null($this->selectedExistingPrivateKey)) {
$this->restartBoarding(); $this->dispatch('error', 'Please select a private key.');
return; return;
} }
@ -202,6 +234,9 @@ public function setPrivateKey(string $type)
$this->privateKeyType = $type; $this->privateKeyType = $type;
if ($type === 'create') { if ($type === 'create') {
$this->createNewPrivateKey(); $this->createNewPrivateKey();
} else {
$this->privateKey = null;
$this->publicKey = null;
} }
$this->currentState = 'create-private-key'; $this->currentState = 'create-private-key';
} }

View file

@ -0,0 +1,47 @@
@props(['currentStep' => 1, 'totalSteps' => 3])
<div class="w-full max-w-2xl mx-auto mb-8">
<div class="flex items-center justify-between">
@for ($i = 1; $i <= $totalSteps; $i++)
<div class="flex items-center {{ $i < $totalSteps ? 'flex-1' : '' }}">
<div class="flex flex-col items-center">
<div
class="flex items-center justify-center size-10 rounded-full border-2 transition-all duration-300
{{ $i < $currentStep ? 'bg-success border-success' : 'border-neutral-200 dark:border-coolgray-300' }}
{{ $i === $currentStep ? 'bg-white dark:bg-coolgray-500' : '' }}
">
@if ($i < $currentStep)
<svg class="size-5 text-white" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
fill="currentColor">
<path fill-rule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clip-rule="evenodd" />
</svg>
@else
<span
class="text-sm font-bold text-black dark:text-white">
{{ $i }}
</span>
@endif
</div>
<span
class="mt-2 text-xs font-medium text-black dark:text-white">
@if ($i === 1)
Server
@elseif ($i === 2)
Connection
@elseif ($i === 3)
Complete
@endif
</span>
</div>
@if ($i < $totalSteps)
<div
class="flex-1 h-0.5 mx-4 transition-all duration-300
{{ $i < $currentStep ? 'bg-success' : 'bg-neutral-200 dark:bg-coolgray-300' }}">
</div>
@endif
</div>
@endfor
</div>
</div>

View file

@ -1,25 +1,29 @@
<div class="grid grid-cols-1 gap-4 md:grid-cols-3"> <div class="w-full max-w-4xl">
<div class="box-border col-span-2 lg:min-w-[24rem] lg:min-h-[21rem]"> <div class=" rounded-lg shadow-sm border border-neutral-200 dark:border-coolgray-300 overflow-hidden">
<h1 class="text-2xl font-bold lg:text-5xl">{{ $title }}</h1> <div class="p-8 lg:p-12">
<div class="py-6"> <h1 class="text-3xl font-bold lg:text-4xl mb-4">{{ $title }}</h1>
@isset($question) @isset($question)
<p class="dark:text-neutral-400"> <div class="text-base lg:text-lg dark:text-neutral-400 mb-8">
{{ $question }} {{ $question }}
</p>
@endisset
</div> </div>
@endisset
@if ($actions) @if ($actions)
<div class="flex flex-col flex-wrap gap-4 lg:items-center md:flex-row"> <div class="flex flex-col gap-4">
{{ $actions }} {{ $actions }}
</div> </div>
@endif @endif
</div> </div>
@isset($explanation) @isset($explanation)
<div class="col-span-1"> <div class=" border-t border-neutral-200 dark:border-coolgray-300 p-8 lg:p-12 ">
<h3 class="pb-8 font-bold">Explanation</h3> <h3 class="text-sm font-bold uppercase tracking-wide mb-4 dark:text-neutral-400">
<div class="space-y-4"> Technical Details
</h3>
<div class="space-y-3 text-sm dark:text-neutral-400">
{{ $explanation }} {{ $explanation }}
</div> </div>
</div> </div>
@endisset @endisset
</div>
</div> </div>

View file

@ -1,6 +1,6 @@
@extends('layouts.base') @extends('layouts.base')
@section('body') @section('body')
<main class="h-full"> <main class="min-h-screen flex items-center justify-center p-4">
{{ $slot }} {{ $slot }}
</main> </main>
@parent @parent

View file

@ -1,69 +1,196 @@
@php use App\Enums\ProxyTypes; @endphp @php use App\Enums\ProxyTypes; @endphp
<x-slot:title> <x-slot:title>
Onboarding | Coolify Onboarding | Coolify
</x-slot> </x-slot>
<section class="flex flex-col h-full lg:items-center lg:justify-center"> <section class="w-full">
<div <div class="flex flex-col items-center w-full space-y-8">
class="flex flex-col items-center justify-center p-10 mx-2 mt-10 bg-white border rounded-lg shadow-sm lg:p-20 dark:bg-transparent dark:border-none max-w-7xl border-neutral-200">
@if ($currentState === 'welcome') @if ($currentState === 'welcome')
<h1 class="text-3xl font-bold lg:text-5xl">Welcome to Coolify</h1> <div class="w-full max-w-2xl text-center space-y-8">
<div class="py-6 text-center lg:text-xl">Let me help you set up the basics.</div> <div class="space-y-4">
<div class="flex justify-center "> <h1 class="text-4xl font-bold lg:text-6xl">Welcome to Coolify</h1>
<x-forms.button class="justify-center w-64 box-boarding" <p class="text-lg lg:text-xl dark:text-neutral-400">
wire:click="$set('currentState','explanation')">Get Connect your first server and start deploying in minutes
Started </p>
</div>
<div
class="bg-white dark:bg-coolgray-100 rounded-lg shadow-sm border border-neutral-200 dark:border-coolgray-300 p-8 text-left">
<h2 class="text-sm font-bold uppercase tracking-wide dark:text-neutral-400 mb-4">
What You'll Set Up
</h2>
<div class="space-y-3">
<div class="flex items-start gap-3">
<div class="flex-shrink-0 mt-0.5">
<svg class="size-5 text-success" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
fill="currentColor">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd" />
</svg>
</div>
<div>
<div class="font-semibold text-base dark:text-white">Server Connection</div>
<div class="text-sm dark:text-neutral-400">Connect via SSH to deploy your resources
</div>
</div>
</div>
<div class="flex items-start gap-3">
<div class="flex-shrink-0 mt-0.5">
<svg class="size-5 text-success" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
fill="currentColor">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd" />
</svg>
</div>
<div>
<div class="font-semibold text-base dark:text-white">Docker Environment</div>
<div class="text-sm dark:text-neutral-400">Automated installation and configuration
</div>
</div>
</div>
<div class="flex items-start gap-3">
<div class="flex-shrink-0 mt-0.5">
<svg class="size-5 text-success" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
fill="currentColor">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd" />
</svg>
</div>
<div>
<div class="font-semibold text-base dark:text-white">Project Structure</div>
<div class="text-sm dark:text-neutral-400">Organize your applications and resources
</div>
</div>
</div>
</div>
</div>
<div class="flex justify-center pt-4">
<x-forms.button class="justify-center px-12 py-4 text-lg font-bold box-boarding"
wire:click="explanation">
Start Setup
</x-forms.button> </x-forms.button>
</div> </div>
</div>
@elseif ($currentState === 'explanation') @elseif ($currentState === 'explanation')
<x-boarding-step title="What is Coolify?"> <x-boarding-progress :currentStep="0" />
<x-boarding-step title="Platform Overview">
<x-slot:question> <x-slot:question>
Coolify is an all-in-one application to automate tasks on your servers, deploy applications with Coolify automates deployment and infrastructure management on your own servers. Deploy applications
Git from Git, manage databases, and monitor everything—without vendor lock-in.
integrations, deploy databases and services, monitor these resources with notifications and
alerts
without vendor lock-in. <br />
<a href="https://coolify.io" class="dark:text-white hover:underline">Coolify Home</a>.
<br><br>
<span class="text-xl">
<x-highlighted text="Self-hosting with superpowers!" /></span>
</x-slot:question> </x-slot:question>
<x-slot:explanation> <x-slot:explanation>
<p> <p>
<x-highlighted text="Task automation:" /> You don't need to manage your servers anymore. <x-highlighted text="Automation:" /> Coolify handles server configuration, Docker management,
Coolify does and
it for you. deployments automatically.
</p> </p>
<p> <p>
<x-highlighted text="No vendor lock-in:" /> All configurations are stored on your servers, so <x-highlighted text="Self-hosted:" /> All data and configurations live on your infrastructure.
everything works without a connection to Coolify (except integrations and automations). Works offline except for external integrations.
</p> </p>
<p> <p>
<x-highlighted text="Monitoring:" />You can get notified on your favourite platforms <x-highlighted text="Monitoring & Alerts:" /> Get real-time notifications via Discord, Telegram,
(Discord, Email, and other platforms.
Telegram, Email, etc.) when something goes wrong, or an action is needed from your side.
</p> </p>
</x-slot:explanation> </x-slot:explanation>
<x-slot:actions> <x-slot:actions>
<x-forms.button class="justify-center w-64 box-boarding" wire:click="explanation">Next <x-forms.button class="justify-center w-full lg:w-auto px-8 py-3 box-boarding"
wire:click="explanation">
Continue
</x-forms.button> </x-forms.button>
</x-slot:actions> </x-slot:actions>
</x-boarding-step> </x-boarding-step>
@elseif ($currentState === 'select-server-type') @elseif ($currentState === 'select-server-type')
<x-boarding-step title="Server"> <x-boarding-progress :currentStep="1" />
<x-boarding-step title="Choose Server Type">
<x-slot:question> <x-slot:question>
Do you want to deploy your resources to your Select where to deploy your applications and databases. You can add more servers later.
<x-highlighted text="Localhost" />
or to a
<x-highlighted text="Remote Server" />?
</x-slot:question> </x-slot:question>
<x-slot:actions> <x-slot:actions>
<x-forms.button class="justify-center w-64 box-boarding" wire:target="setServerType('localhost')" <div class="grid grid-cols-1 lg:grid-cols-3 gap-4 w-full">
wire:click="setServerType('localhost')">Localhost <button
</x-forms.button> class="group relative box-without-bg cursor-pointer hover:border-coollabs transition-all duration-200 p-6"
wire:target="setServerType('localhost')" wire:click="setServerType('localhost')">
<div class="flex flex-col gap-4 text-left">
<div class="flex items-center justify-between">
<svg class="size-10" 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="M5.25 14.25h13.5m-13.5 0a3 3 0 01-3-3m3 3a3 3 0 100 6h13.5a3 3 0 100-6m-16.5-3a3 3 0 013-3h13.5a3 3 0 013 3m-19.5 0a4.5 4.5 0 01.9-2.7L5.737 5.1a3.375 3.375 0 012.7-1.35h7.126c1.062 0 2.062.5 2.7 1.35l2.587 3.45a4.5 4.5 0 01.9 2.7m0 0a3 3 0 01-3 3m0 3h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008zm-3 6h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008z" />
</svg>
<span
class="px-2 py-1 text-xs font-bold uppercase tracking-wide bg-neutral-100 dark:bg-coolgray-300 dark:text-neutral-400 rounded">
Quick Start
</span>
</div>
<div>
<h3 class="text-xl font-bold mb-2">This Machine</h3>
<p class="text-sm dark:text-neutral-400">
Deploy on the server running Coolify. Best for testing and single-server setups.
</p>
</div>
</div>
</button>
<x-forms.button class="justify-center w-64 box-boarding " wire:target="setServerType('remote')"
wire:click="setServerType('remote')">Remote Server
</x-forms.button> <button
class="group relative box-without-bg cursor-pointer hover:border-coollabs transition-all duration-200 p-6"
wire:target="setServerType('remote')" wire:click="setServerType('remote')">
<div class="flex flex-col gap-4 text-left">
<div class="flex items-center justify-between">
<svg class="size-10 " 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="M2.25 15a4.5 4.5 0 004.5 4.5H18a3.75 3.75 0 001.332-7.257 3 3 0 00-3.758-3.848 5.25 5.25 0 00-10.233 2.33A4.502 4.502 0 002.25 15z" />
</svg>
<span
class="px-2 py-1 text-xs font-bold uppercase tracking-wide bg-coollabs/10 dark:bg-warning/20 text-coollabs dark:text-warning rounded">
Recommended
</span>
</div>
<div>
<h3 class="text-xl font-bold mb-2">Remote Server</h3>
<p class="text-sm dark:text-neutral-400">
Connect via SSH to any server—cloud VPS, bare metal, or home infrastructure.
</p>
</div>
</div>
</button>
@can('viewAny', App\Models\CloudProviderToken::class)
<x-modal-input title="Connect a Hetzner Server" isFullWidth>
<x-slot:content>
<div
class="group relative box-without-bg cursor-pointer hover:border-coollabs transition-all duration-200 p-6 h-full min-h-[210px]">
<div class="flex flex-col gap-4 text-left">
<div class="flex items-center justify-between">
<svg class="size-10" 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" />
</svg>
<span
class="px-2 py-1 text-xs font-bold uppercase tracking-wide bg-coollabs/10 dark:bg-warning/20 text-coollabs dark:text-warning rounded">
Recommended
</span>
</div>
<div>
<h3 class="text-xl font-bold mb-2">Hetzner Cloud</h3>
<p class="text-sm dark:text-neutral-400">
Deploy servers directly from your Hetzner Cloud account.
</p>
</div>
</div>
</div>
</x-slot:content>
<livewire:server.new.by-hetzner :private_keys="$this->privateKeys" :limit_reached="false" />
</x-modal-input>
@endcan
</div>
@if (!$serverReachable) @if (!$serverReachable)
<div class="mt-6 p-4 border border-error rounded-lg text-gray-800 dark:text-gray-200"> <div class="mt-6 p-4 border border-error rounded-lg text-gray-800 dark:text-gray-200">
@ -111,290 +238,500 @@ class="bg-red-200 dark:bg-red-900 px-1 rounded-sm">~/.ssh/authorized_keys</code>
@endif @endif
</x-slot:actions> </x-slot:actions>
<x-slot:explanation> <x-slot:explanation>
<p>Servers are the main building blocks, as they will host your applications, databases,
services, called resources. Any CPU intensive process will use the server's CPU where you
are deploying your resources.</p>
<p> <p>
<x-highlighted text="Localhost" /> is the server where Coolify is running on. It is not <x-highlighted text="Servers" /> host your applications, databases, and services (collectively
recommended to use one server called resources). All CPU-intensive operations run on the target server.
for everything.
</p> </p>
<p> <p>
<x-highlighted text="A remote server" /> is a server reachable through SSH. It can be hosted <x-highlighted text="Localhost:" /> The machine running Coolify. Not recommended for production
at home, or from any cloud workloads due to resource contention.
provider. </p>
<p>
<x-highlighted text="Remote Server:" /> Any SSH-accessible server—cloud providers (AWS, Hetzner,
DigitalOcean), bare metal, or self-hosted infrastructure.
</p> </p>
</x-slot:explanation> </x-slot:explanation>
</x-boarding-step> </x-boarding-step>
@elseif ($currentState === 'private-key') @elseif ($currentState === 'private-key')
<x-boarding-step title="SSH Key"> <x-boarding-progress :currentStep="2" />
<x-boarding-step title="SSH Authentication">
<x-slot:question> <x-slot:question>
Do you have your own SSH Private Key? Configure SSH key-based authentication for secure server access.
</x-slot:question> </x-slot:question>
<x-slot:actions> <x-slot:actions>
<x-forms.button class="justify-center lg:w-64 box-boarding" wire:target="setPrivateKey('own')" @if ($privateKeys && $privateKeys->count() > 0)
wire:click="setPrivateKey('own')">Yes <div class="w-full space-y-4">
</x-forms.button> <div class="p-4 rounded-lg border border-neutral-200 dark:border-coolgray-400">
<x-forms.button class="justify-center lg:w-64 box-boarding" wire:target="setPrivateKey('create')" <form wire:submit='selectExistingPrivateKey' class="flex flex-col gap-4">
wire:click="setPrivateKey('create')">No (create one for me)
</x-forms.button>
@if (count($privateKeys) > 0)
<form wire:submit='selectExistingPrivateKey' class="flex flex-col w-full gap-4 lg:pr-10">
<x-forms.select label="Existing SSH Keys" id='selectedExistingPrivateKey'> <x-forms.select label="Existing SSH Keys" id='selectedExistingPrivateKey'>
@foreach ($privateKeys as $privateKey) @foreach ($privateKeys as $privateKey)
<option wire:key="{{ $loop->index }}" value="{{ $privateKey->id }}"> <option wire:key="{{ $loop->index }}" value="{{ $privateKey->id }}">
{{ $privateKey->name }}</option> {{ $privateKey->name }}
</option>
@endforeach @endforeach
</x-forms.select> </x-forms.select>
<x-forms.button type="submit">Use this SSH Key</x-forms.button> <x-forms.button type="submit" class="w-full lg:w-auto">Use Selected Key</x-forms.button>
</form>
@endif
</x-slot:actions>
<x-slot:explanation>
<p>SSH Keys are used to connect to a remote server through a secure shell, called SSH.</p>
<p>You can use your own ssh private key, or you can let Coolify to create one for you.</p>
<p>In both ways, you need to add the public version of your ssh private key to the remote
server's
<code class="dark:text-warning">~/.ssh/authorized_keys</code> file.
</p>
</x-slot:explanation>
</x-boarding-step>
@elseif ($currentState === 'select-existing-server')
<x-boarding-step title="Select a server">
<x-slot:question>
There are already servers available for your Team. Do you want to use one of them?
</x-slot:question>
<x-slot:actions>
<div class="flex flex-col gap-4">
<div>
<x-forms.button class="justify-center w-64 box-boarding" wire:click="createNewServer">No
(create one for me)
</x-forms.button>
</div>
<div>
<form wire:submit='selectExistingServer' class="flex flex-col w-full gap-4 lg:w-96">
<x-forms.select label="Existing servers" class="w-96" id='selectedExistingServer'>
@foreach ($servers as $server)
<option wire:key="{{ $loop->index }}" value="{{ $server->id }}">
{{ $server->name }}</option>
@endforeach
</x-forms.select>
<x-forms.button type="submit">Use this Server</x-forms.button>
</form> </form>
</div> </div>
<div class="relative">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-neutral-300 dark:border-coolgray-400"></div>
</div> </div>
@if (!$serverReachable) <div class="relative flex justify-center text-sm">
<div class="mt-6 p-4 bg-red-100 dark:bg-red-950 rounded-lg text-gray-800 dark:text-gray-200"> <div
<h2 class="text-lg font-bold mb-2">Server is not reachable</h2> class="px-2 py-1 bg-white dark:bg-coolgray-100 border border-neutral-300 dark:border-coolgray-300 rounded text-xs font-bold text-neutral-500 dark:text-neutral-400">
<p class="mb-4">Please check the connection details below and correct them if they are OR
incorrect.</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<x-forms.input placeholder="Default is 22" label="Port" id="remoteServerPort"
wire:model="remoteServerPort" :value="$remoteServerPort" />
<div>
<x-forms.input placeholder="Default is root" label="User" id="remoteServerUser"
wire:model="remoteServerUser" :value="$remoteServerUser" />
<p class="text-xs mt-1">
Non-root user is experimental:
<a class="font-bold underline" target="_blank"
href="https://coolify.io/docs/knowledge-base/server/non-root-user">docs</a>
</p>
</div> </div>
</div> </div>
<div class="mb-4">
<p class="mb-2">If the connection details are correct, please ensure:</p>
<ul class="list-disc list-inside">
<li>The correct public key is in your <code
class="bg-red-200 dark:bg-red-900 px-1 rounded-sm">~/.ssh/authorized_keys</code>
file for the specified user</li>
<li>Or skip the boarding process and manually add a new private key to Coolify and
the server</li>
</ul>
</div> </div>
<p class="mb-4">
For more help, check this <a target="_blank" class="underline font-semibold"
href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a>.
</p>
<x-forms.input readonly id="serverPublicKey" class="mb-4"
label="Current Public Key"></x-forms.input>
<x-forms.button class="w-full md:w-auto box-boarding" wire:click="saveAndValidateServer">
Check again
</x-forms.button>
</div> </div>
@endif @endif
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 w-full">
<x-forms.button
class="justify-center h-auto py-6 box-without-bg hover:border-coollabs transition-all duration-200"
wire:target="setPrivateKey('own')" wire:click="setPrivateKey('own')">
<div class="flex flex-col items-center gap-2">
<svg class="size-8" 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="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z" />
</svg>
<div class="text-center">
<h3 class="text-xl font-bold mb-2">Use Existing Key</h3>
<p class="text-sm dark:text-neutral-400">I have my own SSH key</p>
</div>
</div>
</x-forms.button>
<x-forms.button
class="justify-center h-auto py-6 box-without-bg hover:border-coollabs transition-all duration-200"
wire:target="setPrivateKey('create')" wire:click="setPrivateKey('create')">
<div class="flex flex-col items-center gap-2">
<svg class="size-8" 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="M19 7.5v3m0 0v3m0-3h3m-3 0h-3m-2.25-4.125a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zM4 19.235v-.11a6.375 6.375 0 0112.75 0v.109A12.318 12.318 0 0110.374 21c-2.331 0-4.512-.645-6.374-1.766z" />
</svg>
<div class="text-center">
<h3 class="text-xl font-bold mb-2">Generate New Key</h3>
<p class="text-sm dark:text-neutral-400">Create ED25519 key pair</p>
</div>
</div>
</x-forms.button>
</div>
</x-slot:actions> </x-slot:actions>
<x-slot:explanation> <x-slot:explanation>
<p>Private Keys are used to connect to a remote server through a secure shell, called SSH.</p> <p>
<p>You can use your own private key, or you can let Coolify to create one for you.</p> <x-highlighted text="SSH Key Authentication:" /> Uses public-key cryptography for secure,
<p>In both ways, you need to add the public version of your private key to the remote server's password-less server access.
<code>~/.ssh/authorized_keys</code> file. </p>
<p>
<x-highlighted text="Public Key Deployment:" /> Add the public key to your server's
<code
class="text-xs bg-coolgray-300 dark:bg-coolgray-400 px-1 py-0.5 rounded">~/.ssh/authorized_keys</code>
file.
</p>
<p>
<x-highlighted text="Key Generation:" /> Coolify generates ED25519 keys by default for optimal
security and performance.
</p> </p>
</x-slot:explanation> </x-slot:explanation>
</x-boarding-step> </x-boarding-step>
@elseif ($currentState === 'create-private-key') @elseif ($currentState === 'create-private-key')
<x-boarding-step title="Create Private Key"> <x-boarding-progress :currentStep="2" />
<x-boarding-step title="SSH Key Configuration">
<x-slot:question> <x-slot:question>
Please let me know your key details. Configure your SSH key for server authentication.
</x-slot:question> </x-slot:question>
<x-slot:actions> <x-slot:actions>
<form wire:submit='savePrivateKey' class="flex flex-col w-full gap-4 lg:pr-10"> <form wire:submit='savePrivateKey' class="flex flex-col w-full gap-4">
<x-forms.input required placeholder="Choose a name for your Private Key. Could be anything." <x-forms.input required placeholder="e.g., production-server-key" label="Key Name"
label="Name" id="privateKeyName" /> id="privateKeyName" />
<x-forms.input placeholder="Description, so others will know more about this." <x-forms.input placeholder="Optional: Note what this key is used for" label="Description"
label="Description" id="privateKeyDescription" /> id="privateKeyDescription" />
<x-forms.textarea required placeholder="-----BEGIN OPENSSH PRIVATE KEY-----"
label="Private Key" id="privateKey" />
@if ($privateKeyType === 'create') @if ($privateKeyType === 'create')
<x-forms.textarea required readonly label="Private Key" id="privateKey" rows="8" />
<x-forms.textarea rows="7" readonly label="Public Key" id="publicKey" /> <x-forms.textarea rows="7" readonly label="Public Key" id="publicKey" />
<span class="font-bold dark:text-warning">ACTION REQUIRED: Copy the 'Public Key' to your @else
server's <x-forms.textarea required placeholder="-----BEGIN OPENSSH PRIVATE KEY-----" label="Private Key"
~/.ssh/authorized_keys id="privateKey" rows="8" />
file.</span>
@endif @endif
<x-forms.button type="submit">Save</x-forms.button> @if ($privateKeyType === 'create')
<div class="p-4 bg-warning/10 border border-warning rounded-lg">
<div class="flex gap-3">
<svg class="size-5 text-warning flex-shrink-0 mt-0.5" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z"
clip-rule="evenodd" />
</svg>
<div>
<p class="font-bold text-warning mb-1">Action Required</p>
<p class="text-sm dark:text-white text-black">
Copy the public key above and add it to your server's
<code
class="text-xs bg-coolgray-300 dark:bg-coolgray-400 px-1 py-0.5 rounded">~/.ssh/authorized_keys</code>
file.
</p>
</div>
</div>
</div>
@endif
<x-forms.button type="submit" class="w-full lg:w-auto">Save SSH Key</x-forms.button>
</form> </form>
</x-slot:actions> </x-slot:actions>
<x-slot:explanation> <x-slot:explanation>
<p>Private Keys are used to connect to a remote server through a secure shell, called SSH.</p> <p>
<p>You can use your own private key, or you can let Coolify to create one for you.</p> <x-highlighted text="Key Storage:" /> Private keys are encrypted at rest in Coolify's database.
<p>In both ways, you need to add the public version of your private key to the remote server's </p>
<code>~/.ssh/authorized_keys</code> file. <p>
<x-highlighted text="Public Key Distribution:" /> Deploy the public key to
<code
class="text-xs bg-coolgray-300 dark:bg-coolgray-400 px-1 py-0.5 rounded">~/.ssh/authorized_keys</code>
on your target server for the specified user.
</p>
<p>
<x-highlighted text="Key Format:" /> Supports RSA, ED25519, ECDSA, and DSA key types in OpenSSH
format.
</p> </p>
</x-slot:explanation> </x-slot:explanation>
</x-boarding-step> </x-boarding-step>
@elseif ($currentState === 'create-server') @elseif ($currentState === 'create-server')
<x-boarding-step title="Create Server"> <x-boarding-progress :currentStep="2" />
<x-boarding-step title="Server Configuration">
<x-slot:question> <x-slot:question>
Please let me know your server details. Provide connection details for your remote server.
</x-slot:question> </x-slot:question>
<x-slot:actions> <x-slot:actions>
<form wire:submit='saveServer' class="flex flex-col w-full gap-4 lg:w-96"> <form wire:submit='saveServer' class="flex flex-col w-full gap-4">
<x-forms.input required placeholder="Choose a name for your Server. Could be anything." <div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
label="Name" id="remoteServerName" wire:model="remoteServerName" /> <x-forms.input required placeholder="e.g., production-app-server" label="Server Name"
<x-forms.input placeholder="Description, so others will know more about this." id="remoteServerName" wire:model="remoteServerName" />
label="Description" id="remoteServerDescription" wire:model="remoteServerDescription" /> <x-forms.input required placeholder="IP address or hostname" label="IP Address/Hostname"
<x-forms.input required placeholder="127.0.0.1" label="IP Address" id="remoteServerHost" id="remoteServerHost" wire:model="remoteServerHost" />
wire:model="remoteServerHost" /> </div>
<div x-data="{ showAdvanced: false }" class="flex flex-col gap-2"> <x-forms.input placeholder="Optional: Note what this server hosts" label="Description"
id="remoteServerDescription" wire:model="remoteServerDescription" />
<div x-data="{ showAdvanced: false }" class="flex flex-col gap-4">
<button @click="showAdvanced = !showAdvanced" type="button" <button @click="showAdvanced = !showAdvanced" type="button"
class="text-left text-sm text-gray-600 dark:text-gray-300 hover:underline"> class="flex items-center gap-2 text-left text-sm font-medium hover:underline">
Advanced Settings <svg x-show="!showAdvanced" class="size-4" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z"
clip-rule="evenodd" />
</svg>
<svg x-show="showAdvanced" class="size-4" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z"
clip-rule="evenodd" />
</svg>
Advanced Connection Settings
</button> </button>
<div x-show="showAdvanced" class="flex flex-col gap-2"> <div x-show="showAdvanced" x-cloak
<x-forms.input placeholder="Port number of your server. Default is 22." label="Port" class="grid grid-cols-1 lg:grid-cols-2 gap-4 p-4 rounded-lg border border-neutral-200 dark:border-coolgray-400">
<x-forms.input placeholder="Default: 22" label="SSH Port" type="number"
id="remoteServerPort" wire:model="remoteServerPort" /> id="remoteServerPort" wire:model="remoteServerPort" />
<div> <div>
<x-forms.input placeholder="Default is root." label="User" <x-forms.input placeholder="Default: root" label="SSH User" id="remoteServerUser"
id="remoteServerUser" wire:model="remoteServerUser" /> wire:model="remoteServerUser" />
<div class="text-xs text-gray-600 dark:text-gray-300">Non-root user is <p class="mt-1 text-xs dark:text-white text-black">
experimental: <a class="font-bold underline" target="_blank" Non-root user support is experimental.
href="https://coolify.io/docs/knowledge-base/server/non-root-user">docs</a>. <a class="font-bold underline hover:text-coollabs" target="_blank"
href="https://coolify.io/docs/knowledge-base/server/non-root-user">Learn
more</a>
</p>
</div> </div>
</div> </div>
</div> </div>
</div> <x-forms.button type="submit" class="w-full lg:w-auto">Validate Connection</x-forms.button>
<x-forms.button type="submit">Continue</x-forms.button>
</form> </form>
</x-slot:actions> </x-slot:actions>
<x-slot:explanation>
<p>
<x-highlighted text="Connection Requirements:" /> Server must be accessible via SSH on the
specified port (default 22).
</p>
<p>
<x-highlighted text="Hostname Resolution:" /> Use IP addresses for direct connections or ensure
DNS resolution is configured.
</p>
<p>
<x-highlighted text="User Permissions:" /> Root or sudo-enabled users recommended for full
Docker
management capabilities.
</p>
</x-slot:explanation>
</x-boarding-step> </x-boarding-step>
@elseif ($currentState === 'validate-server') @elseif ($currentState === 'validate-server')
<x-boarding-step title="Validate & Configure Server"> <x-boarding-progress :currentStep="2" />
<x-boarding-step title="Server Validation">
<x-slot:question> <x-slot:question>
I need to validate your server (connection, Docker Engine, etc) and configure if something is Coolify will automatically install Docker {{ $minDockerVersion }}+ if not present.
missing for me. Are you okay with this?
</x-slot:question> </x-slot:question>
<x-slot:actions> <x-slot:actions>
<div class="w-full space-y-6">
<div
class="p-6 bg-neutral-50 dark:bg-coolgray-200 rounded-lg border border-neutral-200 dark:border-coolgray-400">
<h3 class="font-bold text-black dark:text-white mb-4">Validation Steps</h3>
<div class="space-y-3">
<div class="flex items-start gap-3">
<div class="flex-shrink-0 mt-0.5">
<svg class="size-5 text-success" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd" />
</svg>
</div>
<div>
<div class="font-semibold text-base dark:text-white">Test SSH Connection</div>
<div class="text-sm dark:text-neutral-400">Verify key-based authentication</div>
</div>
</div>
<div class="flex items-start gap-3">
<div class="flex-shrink-0 mt-0.5">
<svg class="size-5 text-success" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd" />
</svg>
</div>
<div>
<div class="font-semibold text-base dark:text-white">Check OS Compatibility
</div>
<div class="text-sm dark:text-neutral-400">Verify supported Linux distribution
</div>
</div>
</div>
<div class="flex items-start gap-3">
<div class="flex-shrink-0 mt-0.5">
<svg class="size-5 text-success" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd" />
</svg>
</div>
<div>
<div class="font-semibold text-base dark:text-white">Install Docker Engine</div>
<div class="text-sm dark:text-neutral-400">Auto-install if version
{{ $minDockerVersion }}+ not
found
</div>
</div>
</div>
<div class="flex items-start gap-3">
<div class="flex-shrink-0 mt-0.5">
<svg class="size-5 text-success" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd" />
</svg>
</div>
<div>
<div class="font-semibold text-base dark:text-white">Configure Network</div>
<div class="text-sm dark:text-neutral-400">Set up Docker networks and proxy
</div>
</div>
</div>
</div>
</div>
<x-slide-over closeWithX fullScreen> <x-slide-over closeWithX fullScreen>
<x-slot:title>Validate & configure</x-slot:title> <x-slot:title>Server Validation</x-slot:title>
<x-slot:content> <x-slot:content>
<livewire:server.validate-and-install :server="$this->createdServer" /> <livewire:server.validate-and-install :server="$this->createdServer" />
</x-slot:content> </x-slot:content>
<x-forms.button @click="slideOverOpen=true" class="w-full font-bold box-boarding lg:w-96" <x-forms.button @click="slideOverOpen=true" class="w-full font-bold py-4 box-boarding"
wire:click.prevent='installServer' isHighlighted> wire:click.prevent='installServer' isHighlighted>
Let's do it! Start Validation
</x-forms.button> </x-forms.button>
</x-slide-over> </x-slide-over>
</div>
</x-slot:actions> </x-slot:actions>
<x-slot:explanation> <x-slot:explanation>
<p>This will install the latest Docker Engine on your server, configure a few things to be able <p>
to run optimal.<br><br>Minimum Docker Engine version is: {{ $minDockerVersion }}<br><br>To <x-highlighted text="Automated Setup:" /> Coolify installs Docker Engine, Docker Compose, and
manually install configures system requirements automatically.
Docker </p>
Engine, check <a target="_blank" class="underline dark:text-warning" <p>
href="https://docs.docker.com/engine/install/#server">this <x-highlighted text="Version Requirements:" /> Minimum Docker Engine {{ $minDockerVersion }}.x
documentation</a>.</p> required.
<a target="_blank" class="underline hover:text-coollabs"
href="https://docs.docker.com/engine/install/#server">Manual installation guide</a>
</p>
<p>
<x-highlighted text="System Configuration:" /> Sets up Docker networks, proxy configuration, and
resource monitoring.
</p>
</x-slot:explanation> </x-slot:explanation>
</x-boarding-step> </x-boarding-step>
@elseif ($currentState === 'create-project') @elseif ($currentState === 'create-project')
<x-boarding-step title="Project"> <x-boarding-progress :currentStep="3" />
<x-boarding-step title="Project Setup">
<x-slot:question> <x-slot:question>
@if (count($projects) > 0) @if ($projects && $projects->count() > 0)
You already have some projects. Do you want to use one of them or should I create a new one You have existing projects. Select one or create a new project to organize your resources.
for
you?
@else @else
Let's create an initial project for you. You can change all the details later on. Create your first project to organize applications, databases, and services.
@endif @endif
</x-slot:question> </x-slot:question>
<x-slot:actions> <x-slot:actions>
<x-forms.button class="justify-center w-64 box-boarding" wire:click="createNewProject">Create new <div class="w-full space-y-4">
project!</x-forms.button> <x-forms.button class="justify-center w-full py-4 font-bold box-boarding"
<div> wire:click="createNewProject" isHighlighted>
@if (count($projects) > 0) Create "My First Project"
<form wire:submit='selectExistingProject' class="flex flex-col w-full gap-4 lg:w-96"> </x-forms.button>
<x-forms.select label="Existing projects" class="w-96" id='selectedProject'>
@if ($projects && $projects->count() > 0)
<div class="relative">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-neutral-300 dark:border-coolgray-400"></div>
</div>
<div class="relative flex justify-center text-sm">
<span class="px-2 text-neutral-500 dark:text-neutral-400">Or use existing</span>
</div>
</div>
<form wire:submit='selectExistingProject' class="flex flex-col gap-4">
<x-forms.select label="Existing Projects" id='selectedProject'>
@foreach ($projects as $project) @foreach ($projects as $project)
<option wire:key="{{ $loop->index }}" value="{{ $project->id }}"> <option wire:key="{{ $loop->index }}" value="{{ $project->id }}">
{{ $project->name }}</option> {{ $project->name }}
</option>
@endforeach @endforeach
</x-forms.select> </x-forms.select>
<x-forms.button type="submit">Use this Project</x-forms.button> <x-forms.button type="submit" class="w-full lg:w-auto">Use Selected Project</x-forms.button>
</form> </form>
@endif @endif
</div> </div>
</x-slot:actions> </x-slot:actions>
<x-slot:explanation> <x-slot:explanation>
<p>Projects contain several resources combined into one virtual group. There are no <p>
limitations on the number of projects you can add.</p> <x-highlighted text="Project Organization:" /> Group related resources (apps, databases,
<p>Each project should have at least one environment, this allows you to create a production & services)
staging version of the same application, but grouped separately.</p> into logical projects.
</p>
<p>
<x-highlighted text="Environments:" /> Each project includes a production environment by
default.
Add staging, development, or custom environments as needed.
</p>
<p>
<x-highlighted text="Team Access:" /> Projects inherit team permissions and can be managed
collaboratively.
</p>
</x-slot:explanation> </x-slot:explanation>
</x-boarding-step> </x-boarding-step>
@elseif ($currentState === 'create-resource') @elseif ($currentState === 'create-resource')
<x-boarding-step title="Resources"> <x-boarding-progress :currentStep="3" />
<x-slot:question> <div class="w-full max-w-2xl text-center space-y-8">
Let's go to the new resource page, where you can create your first resource. <div class="space-y-4">
</x-slot:question> <div class="flex justify-center">
<x-slot:actions> <svg class="size-16 text-success" xmlns="http://www.w3.org/2000/svg" fill="none"
<div class="items-center justify-center w-64 box-boarding" wire:click="showNewResource">Let's do viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
it!</div> <path stroke-linecap="round" stroke-linejoin="round"
</x-slot:actions> d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
<x-slot:explanation> </svg>
<p>A resource could be an application, a database or a service (like WordPress).</p> </div>
</x-slot:explanation> <h1 class="text-4xl font-bold lg:text-5xl">Setup Complete!</h1>
</x-boarding-step> <p class="text-lg dark:text-neutral-400">
Your server is connected and ready. Start deploying your first resource.
</p>
</div>
<div
class="bg-white dark:bg-coolgray-100 rounded-lg shadow-sm border border-neutral-200 dark:border-coolgray-300 p-8 text-left">
<h2 class="text-sm font-bold uppercase tracking-wide dark:text-neutral-400 mb-4">
What's Configured
</h2>
<div class="space-y-3">
<div class="flex items-start gap-3">
<div class="flex-shrink-0 mt-0.5">
<svg class="size-5 text-success" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
fill="currentColor">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd" />
</svg>
</div>
<div>
<div class="font-semibold text-base dark:text-white">Server: {{ $createdServer->name }}
</div>
<div class="text-sm dark:text-neutral-400">{{ $createdServer->ip }}</div>
</div>
</div>
<div class="flex items-start gap-3">
<div class="flex-shrink-0 mt-0.5">
<svg class="size-5 text-success" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
fill="currentColor">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd" />
</svg>
</div>
<div>
<div class="font-semibold text-base dark:text-white">Project:
{{ $createdProject->name }}
</div>
<div class="text-sm dark:text-neutral-400">Production environment ready</div>
</div>
</div>
<div class="flex items-start gap-3">
<div class="flex-shrink-0 mt-0.5">
<svg class="size-5 text-success" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
fill="currentColor">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd" />
</svg>
</div>
<div>
<div class="font-semibold text-base dark:text-white">Docker Engine</div>
<div class="text-sm dark:text-neutral-400">Installed and running</div>
</div>
</div>
</div>
</div>
<div class="flex flex-col gap-3">
<x-forms.button class="justify-center w-full py-4 text-lg font-bold box-boarding"
wire:click="showNewResource" isHighlighted>
Deploy Your First Resource
</x-forms.button>
<button wire:click="skipBoarding"
class="text-sm dark:text-neutral-400 hover:text-coollabs dark:hover:text-warning hover:underline transition-colors">
Go to Dashboard
</button>
</div>
</div>
@endif @endif
</div> </div>
<div class="flex flex-col justify-center gap-4 pt-4 lg:gap-2 lg:flex">
<div class="flex justify-center w-full gap-2"> @if ($currentState !== 'welcome' && $currentState !== 'create-resource')
<div class="cursor-pointer hover:underline dark:hover:text-white" wire:click='skipBoarding'>Skip <div class="flex flex-col items-center gap-4 pt-8 mt-8 border-t border-neutral-200 dark:border-coolgray-400">
onboarding</div> <div class="flex justify-center gap-6 text-sm">
<div class="cursor-pointer hover:underline dark:hover:text-white" wire:click='restartBoarding'>Restart <button wire:click='skipBoarding'
onboarding</div> class="dark:text-neutral-400 hover:text-coollabs dark:hover:text-warning hover:underline transition-colors">
Skip Setup
</button>
<button wire:click='restartBoarding'
class="dark:text-neutral-400 hover:text-coollabs dark:hover:text-warning hover:underline transition-colors">
Restart
</button>
</div> </div>
<x-modal-input title="How can we help?"> <x-modal-input title="Need Help?">
<x-slot:content> <x-slot:content>
<div class="w-full text-center cursor-pointer hover:underline dark:hover:text-white" <button
title="Send us feedback or get help!"> class="text-sm dark:text-neutral-400 hover:text-coollabs dark:hover:text-warning hover:underline transition-colors">
Feedback Contact Support
</div> </button>
</x-slot:content> </x-slot:content>
<livewire:help /> <livewire:help />
</x-modal-input> </x-modal-input>
</div> </div>
</section> @endif
</section>