mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-18 04:14:18 +00:00
feat(server): auto-resolve Sentinel URL and polish setup UI
Ensure a Sentinel endpoint from instance FQDN/IP when enabling, use process dialogs for validation with install state, and tidy server create, boarding, metrics empty state, and log timestamps.
This commit is contained in:
1 parent
a6f71909fd
commit
8f7eb2d790
22 files changed
+306
-97
No files matched your search
@@ -23,13 +23,10 @@ class StartSentinel
|
|||||||
$refreshRate = data_get($server, 'settings.sentinel_metrics_refresh_rate_seconds');
|
$refreshRate = data_get($server, 'settings.sentinel_metrics_refresh_rate_seconds');
|
||||||
$pushInterval = data_get($server, 'settings.sentinel_push_interval_seconds');
|
$pushInterval = data_get($server, 'settings.sentinel_push_interval_seconds');
|
||||||
$token = $server->settings->ensureValidSentinelToken();
|
$token = $server->settings->ensureValidSentinelToken();
|
||||||
$endpoint = data_get($server, 'settings.sentinel_custom_url');
|
$endpoint = $server->settings->ensureSentinelUrl();
|
||||||
$debug = data_get($server, 'settings.is_sentinel_debug_enabled');
|
$debug = data_get($server, 'settings.is_sentinel_debug_enabled');
|
||||||
$mountDir = '/data/coolify/sentinel';
|
$mountDir = '/data/coolify/sentinel';
|
||||||
$image = coolifyRegistryUrl().'/coollabsio/sentinel:'.$version;
|
$image = coolifyRegistryUrl().'/coollabsio/sentinel:'.$version;
|
||||||
if (! $endpoint) {
|
|
||||||
throw new \RuntimeException('You should set FQDN in Instance Settings.');
|
|
||||||
}
|
|
||||||
$environments = [
|
$environments = [
|
||||||
'TOKEN' => $token,
|
'TOKEN' => $token,
|
||||||
'DEBUG' => $debug ? 'true' : 'false',
|
'DEBUG' => $debug ? 'true' : 'false',
|
||||||
|
|||||||
@@ -114,9 +114,10 @@ class Sentinel extends Component
|
|||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$this->isSentinelEnabled = true;
|
|
||||||
$customImage = isDev() ? $this->sentinelCustomDockerImage : null;
|
$customImage = isDev() ? $this->sentinelCustomDockerImage : null;
|
||||||
StartSentinel::run($this->server, true, null, $customImage);
|
StartSentinel::run($this->server, true, null, $customImage);
|
||||||
|
$this->sentinelCustomUrl = $this->server->settings->sentinel_custom_url;
|
||||||
|
$this->isSentinelEnabled = true;
|
||||||
} else {
|
} else {
|
||||||
$this->isSentinelEnabled = false;
|
$this->isSentinelEnabled = false;
|
||||||
$this->isMetricsEnabled = false;
|
$this->isMetricsEnabled = false;
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ class ValidateAndInstall extends Component
|
|||||||
|
|
||||||
public bool $ask = false;
|
public bool $ask = false;
|
||||||
|
|
||||||
|
public bool $isInstalling = false;
|
||||||
|
|
||||||
protected $listeners = [
|
protected $listeners = [
|
||||||
'init',
|
'init',
|
||||||
'validateConnection',
|
'validateConnection',
|
||||||
@@ -51,6 +53,7 @@ class ValidateAndInstall extends Component
|
|||||||
|
|
||||||
public function init(int $data = 0)
|
public function init(int $data = 0)
|
||||||
{
|
{
|
||||||
|
$this->isInstalling = false;
|
||||||
$this->uptime = null;
|
$this->uptime = null;
|
||||||
$this->supported_os_type = null;
|
$this->supported_os_type = null;
|
||||||
$this->prerequisites_installed = null;
|
$this->prerequisites_installed = null;
|
||||||
@@ -172,6 +175,7 @@ class ValidateAndInstall extends Component
|
|||||||
if ($this->number_of_tries <= $this->max_tries) {
|
if ($this->number_of_tries <= $this->max_tries) {
|
||||||
$this->installationStep = 'Prerequisites';
|
$this->installationStep = 'Prerequisites';
|
||||||
$activity = $this->server->installPrerequisites();
|
$activity = $this->server->installPrerequisites();
|
||||||
|
$this->isInstalling = true;
|
||||||
$this->number_of_tries++;
|
$this->number_of_tries++;
|
||||||
$this->dispatch('activityMonitor', $activity->id, 'init', $this->number_of_tries, "{$this->installationStep} Installation Logs");
|
$this->dispatch('activityMonitor', $activity->id, 'init', $this->number_of_tries, "{$this->installationStep} Installation Logs");
|
||||||
}
|
}
|
||||||
@@ -208,6 +212,7 @@ class ValidateAndInstall extends Component
|
|||||||
if ($this->number_of_tries <= $this->max_tries) {
|
if ($this->number_of_tries <= $this->max_tries) {
|
||||||
$this->installationStep = 'Docker';
|
$this->installationStep = 'Docker';
|
||||||
$activity = $this->server->installDocker();
|
$activity = $this->server->installDocker();
|
||||||
|
$this->isInstalling = true;
|
||||||
$this->number_of_tries++;
|
$this->number_of_tries++;
|
||||||
$this->dispatch('activityMonitor', $activity->id, 'init', $this->number_of_tries, "{$this->installationStep} Installation Logs");
|
$this->dispatch('activityMonitor', $activity->id, 'init', $this->number_of_tries, "{$this->installationStep} Installation Logs");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -219,7 +219,22 @@ class ServerSetting extends Model
|
|||||||
return $token;
|
return $token;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function generateSentinelUrl(bool $save = true, bool $ignoreEvent = false)
|
public function ensureSentinelUrl(): string
|
||||||
|
{
|
||||||
|
$url = $this->sentinel_custom_url;
|
||||||
|
|
||||||
|
if (blank($url)) {
|
||||||
|
$url = $this->generateSentinelUrl(ignoreEvent: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blank($url)) {
|
||||||
|
throw new \RuntimeException('Set an instance FQDN or public IP before enabling Sentinel.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function generateSentinelUrl(bool $save = true, bool $ignoreEvent = false): ?string
|
||||||
{
|
{
|
||||||
$domain = null;
|
$domain = null;
|
||||||
$settings = InstanceSettings::get();
|
$settings = InstanceSettings::get();
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d
|
|||||||
} catch (Exception) {
|
} catch (Exception) {
|
||||||
$timestamp->setTimezone('UTC');
|
$timestamp->setTimezone('UTC');
|
||||||
}
|
}
|
||||||
data_set($i, 'timestamp', $timestamp->format('Y-M-d H:i:s.u'));
|
data_set($i, 'timestamp', $timestamp->format('Y-M-d H:i:s'));
|
||||||
|
|
||||||
return $i;
|
return $i;
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
@php use App\Actions\CoolifyTask\RunRemoteProcess; @endphp
|
@php use App\Actions\CoolifyTask\RunRemoteProcess; @endphp
|
||||||
<div @class([
|
<div @class([
|
||||||
'h-full flex flex-col overflow-hidden' => $fullHeight,
|
'h-full flex flex-col overflow-hidden' => $fullHeight,
|
||||||
'h-full overflow-hidden' => !$fullHeight,
|
'overflow-hidden' => !$fullHeight,
|
||||||
])>
|
])>
|
||||||
@if ($activity)
|
@if ($activity)
|
||||||
@if (isset($header))
|
@if (isset($header))
|
||||||
|
|||||||
@@ -507,16 +507,16 @@
|
|||||||
</section>
|
</section>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<x-slide-over closeWithX fullScreen>
|
<x-process-dialog closeWithX size="xl">
|
||||||
<x-slot:title>Server validation</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 justify-center"
|
<x-forms.button @click="processDialogOpen = true" class="w-full justify-center"
|
||||||
wire:click.prevent="installServer" isHighlighted>
|
wire:click.prevent="installServer" isHighlighted>
|
||||||
Start validation
|
Start validation
|
||||||
</x-forms.button>
|
</x-forms.button>
|
||||||
</x-slide-over>
|
</x-process-dialog>
|
||||||
</div>
|
</div>
|
||||||
</x-slot:actions>
|
</x-slot:actions>
|
||||||
<x-slot:explanation>
|
<x-slot:explanation>
|
||||||
@@ -659,7 +659,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if ($currentState !== 'welcome' && $currentState !== 'create-resource')
|
@if ($currentState !== 'welcome' && $currentState !== 'create-resource')
|
||||||
<div class="mt-6 flex w-full max-w-3xl flex-col items-center gap-3">
|
<div class="mx-auto mt-6 flex w-full max-w-3xl flex-col items-center gap-3">
|
||||||
<div
|
<div
|
||||||
class="inline-flex flex-wrap items-center justify-center gap-0.5 rounded-lg border border-neutral-200 bg-neutral-50 p-0.5 dark:border-white/[0.08] dark:bg-white/[0.025]">
|
class="inline-flex flex-wrap items-center justify-center gap-0.5 rounded-lg border border-neutral-200 bg-neutral-50 p-0.5 dark:border-white/[0.08] dark:bg-white/[0.025]">
|
||||||
<button type="button" wire:click="skipBoarding"
|
<button type="button" wire:click="skipBoarding"
|
||||||
|
|||||||
@@ -280,10 +280,6 @@
|
|||||||
@else
|
@else
|
||||||
<x-application.settings-section id="server-metrics-overview-section" title="Metrics"
|
<x-application.settings-section id="server-metrics-overview-section" title="Metrics"
|
||||||
helper="Inspect recent CPU and memory usage reported by Sentinel.">
|
helper="Inspect recent CPU and memory usage reported by Sentinel.">
|
||||||
<x-slot:actions>
|
|
||||||
<x-status-badge status="Unavailable" type="warning" />
|
|
||||||
</x-slot:actions>
|
|
||||||
|
|
||||||
<x-empty size="sm" title="Sentinel is required"
|
<x-empty size="sm" title="Sentinel is required"
|
||||||
description="Enable Sentinel before collecting CPU and memory metrics for this server."
|
description="Enable Sentinel before collecting CPU and memory metrics for this server."
|
||||||
icon-name="dashboard">
|
icon-name="dashboard">
|
||||||
|
|||||||
@@ -3,38 +3,42 @@
|
|||||||
New Server | Coolify
|
New Server | Coolify
|
||||||
</x-slot>
|
</x-slot>
|
||||||
|
|
||||||
<div class="mb-4 flex flex-wrap items-center justify-end gap-2">
|
<div class="mb-5 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<a href="{{ $selectedType ? route('server.create') : route('server.index') }}" class="button"
|
<h1 class="min-w-0 text-[24px]! leading-7! font-semibold! tracking-tight!">New server</h1>
|
||||||
{{ wireNavigate() }}>
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
{{ $selectedType ? 'Change method' : 'Back to servers' }}
|
@if ($selectedType)
|
||||||
</a>
|
<a href="{{ route('server.create') }}" class="button" {{ wireNavigate() }}>
|
||||||
@if ($selectedType && $selectedType !== 'manual' && ! $selectedTokenUuid)
|
Change method
|
||||||
@php
|
</a>
|
||||||
$tokenProvider = $selectedType === 'digital-ocean' ? 'digitalocean' : $selectedType;
|
@endif
|
||||||
$tokenProviderName = $selectedType === 'digital-ocean'
|
@if ($selectedType && $selectedType !== 'manual' && ! $selectedTokenUuid)
|
||||||
? 'DigitalOcean'
|
@php
|
||||||
: str($selectedType)->headline();
|
$tokenProvider = $selectedType === 'digital-ocean' ? 'digitalocean' : $selectedType;
|
||||||
@endphp
|
$tokenProviderName = $selectedType === 'digital-ocean'
|
||||||
@can('create', App\Models\CloudProviderToken::class)
|
? 'DigitalOcean'
|
||||||
<x-modal-input title="New {{ $tokenProviderName }} token">
|
: str($selectedType)->headline();
|
||||||
<x-slot:content>
|
@endphp
|
||||||
<button type="button"
|
@can('create', App\Models\CloudProviderToken::class)
|
||||||
class="button button-highlighted">
|
<x-modal-input title="New {{ $tokenProviderName }} token">
|
||||||
<x-reicon name="plus" class="size-3.5" />
|
<x-slot:content>
|
||||||
New token
|
<button type="button"
|
||||||
</button>
|
class="button button-highlighted">
|
||||||
</x-slot:content>
|
<x-reicon name="plus" class="size-3.5" />
|
||||||
<livewire:security.cloud-provider-token-form :modal_mode="true" :provider="$tokenProvider"
|
New token
|
||||||
wire:key="new-server-token-{{ $tokenProvider }}" />
|
</button>
|
||||||
</x-modal-input>
|
</x-slot:content>
|
||||||
@endcan
|
<livewire:security.cloud-provider-token-form :modal_mode="true" :provider="$tokenProvider"
|
||||||
@endif
|
wire:key="new-server-token-{{ $tokenProvider }}" />
|
||||||
|
</x-modal-input>
|
||||||
|
@endcan
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (!$selectedType)
|
@if (!$selectedType)
|
||||||
<div class="application-settings-form">
|
<div class="application-settings-form">
|
||||||
<x-application.settings-section title="Add a server"
|
<section class="application-settings-section">
|
||||||
description="Provision with a cloud provider or connect any reachable Linux server." flush>
|
<div class="application-settings-section-body is-flush">
|
||||||
<div class="grid grid-cols-1 gap-3 p-3 sm:grid-cols-2 lg:grid-cols-4">
|
<div class="grid grid-cols-1 gap-3 p-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
@can('viewAny', App\Models\CloudProviderToken::class)
|
@can('viewAny', App\Models\CloudProviderToken::class)
|
||||||
<a href="{{ route('server.create.type', ['type' => 'hetzner']) }}"
|
<a href="{{ route('server.create.type', ['type' => 'hetzner']) }}"
|
||||||
@@ -115,7 +119,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</x-application.settings-section>
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
<div class="application-settings-form">
|
<div class="application-settings-form">
|
||||||
|
|||||||
@@ -20,28 +20,17 @@
|
|||||||
</button>
|
</button>
|
||||||
</x-slot:actions>
|
</x-slot:actions>
|
||||||
|
|
||||||
<div class="grid gap-4 lg:grid-cols-2">
|
<div class="mb-5">
|
||||||
<x-forms.input id="name" label="Name" required />
|
|
||||||
<x-forms.input id="description" label="Description" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-5 grid gap-4 border-t border-neutral-200 pt-4 lg:grid-cols-3 dark:border-white/[0.08]">
|
|
||||||
<x-forms.input id="ip" label="IP address or domain" required
|
<x-forms.input id="ip" label="IP address or domain" required
|
||||||
helper="For example 127.0.0.1 or server.example.com." />
|
helper="For example 127.0.0.1 or server.example.com." />
|
||||||
<x-forms.input id="user" label="User" required
|
|
||||||
helper="Non-root SSH users are experimental." />
|
|
||||||
<x-forms.input type="number" id="port" label="Port" required />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-5 grid items-end gap-4 border-t border-neutral-200 pt-4 lg:grid-cols-2 dark:border-white/[0.08]">
|
<div class="mb-5">
|
||||||
<x-forms.listbox id="private_key_id" label="Private key"
|
<div class="flex items-end gap-3">
|
||||||
placeholder="Select a private key" :options="$privateKeyOptions" />
|
<div class="min-w-0 flex-1">
|
||||||
|
<x-forms.listbox id="private_key_id" label="Private key"
|
||||||
<div class="flex items-center justify-between gap-3">
|
placeholder="Select a private key" :options="$privateKeyOptions" />
|
||||||
<x-forms.checkbox id="is_build_server"
|
</div>
|
||||||
helper="Build servers compile applications but do not host deployments."
|
|
||||||
label="Use as a build server" />
|
|
||||||
|
|
||||||
@can('create', App\Models\PrivateKey::class)
|
@can('create', App\Models\PrivateKey::class)
|
||||||
<div x-data="{ dropdownOpen: false }" class="relative shrink-0"
|
<div x-data="{ dropdownOpen: false }" class="relative shrink-0"
|
||||||
@click.outside="dropdownOpen = false"
|
@click.outside="dropdownOpen = false"
|
||||||
@@ -81,6 +70,33 @@
|
|||||||
@endcan
|
@endcan
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-4 border-t border-neutral-200 pt-4 lg:grid-cols-2 dark:border-white/[0.08]">
|
||||||
|
<x-forms.input id="name" label="Name" required />
|
||||||
|
<x-forms.input id="description" label="Description" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div x-data="{ advancedOpen: false }"
|
||||||
|
class="mt-5 border-t border-neutral-200 pt-4 dark:border-white/[0.08]">
|
||||||
|
<button type="button" @click="advancedOpen = !advancedOpen"
|
||||||
|
class="flex w-full items-center justify-between gap-3 text-left text-[13px] font-medium text-neutral-600 dark:text-fg-dim"
|
||||||
|
:aria-expanded="advancedOpen">
|
||||||
|
Advanced settings
|
||||||
|
<x-reicon name="chevron-down" class="size-3.5 transition-transform"
|
||||||
|
x-bind:class="advancedOpen && 'rotate-180'" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div x-show="advancedOpen" x-cloak class="mt-4 flex flex-col gap-4">
|
||||||
|
<div class="grid gap-4 lg:grid-cols-2">
|
||||||
|
<x-forms.input id="user" label="User" required
|
||||||
|
helper="Non-root SSH users are experimental." />
|
||||||
|
<x-forms.input type="number" id="port" label="Port" required />
|
||||||
|
</div>
|
||||||
|
<x-forms.checkbox id="is_build_server"
|
||||||
|
helper="Build servers compile applications but do not host deployments. Enabling this makes the server build-only."
|
||||||
|
label="Use as a dedicated build server" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</x-application.settings-section>
|
</x-application.settings-section>
|
||||||
</form>
|
</form>
|
||||||
@endif
|
@endif
|
||||||
|
|||||||
@@ -13,19 +13,14 @@
|
|||||||
helper="Monitor server and container health while collecting historical metrics.">
|
helper="Monitor server and container health while collecting historical metrics.">
|
||||||
<x-slot:actions>
|
<x-slot:actions>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<x-status-badge
|
|
||||||
:status="!$isSentinelEnabled
|
|
||||||
? 'Disabled'
|
|
||||||
: ($server->isSentinelLive() ? 'In sync' : 'Out of sync')"
|
|
||||||
:type="!$isSentinelEnabled
|
|
||||||
? 'neutral'
|
|
||||||
: ($server->isSentinelLive() ? 'success' : 'warning')" />
|
|
||||||
@if (!$isSentinelEnabled)
|
@if (!$isSentinelEnabled)
|
||||||
<x-forms.button canGate="update" :canResource="$server" isHighlighted
|
<x-forms.button canGate="update" :canResource="$server" isHighlighted
|
||||||
wire:click="toggleSentinel">
|
wire:click="toggleSentinel">
|
||||||
Enable Sentinel
|
Enable Sentinel
|
||||||
</x-forms.button>
|
</x-forms.button>
|
||||||
@else
|
@else
|
||||||
|
<x-status-badge :status="$server->isSentinelLive() ? 'In sync' : 'Out of sync'"
|
||||||
|
:type="$server->isSentinelLive() ? 'success' : 'warning'" />
|
||||||
<x-forms.button wire:click="restartSentinel" canGate="update"
|
<x-forms.button wire:click="restartSentinel" canGate="update"
|
||||||
:canResource="$server">
|
:canResource="$server">
|
||||||
<x-reicon name="refresh" class="size-3.5" />
|
<x-reicon name="refresh" class="size-3.5" />
|
||||||
|
|||||||
@@ -192,17 +192,17 @@
|
|||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<x-slide-over closeWithX fullScreen>
|
<x-process-dialog closeWithX size="xl">
|
||||||
<x-slot:title>Validate and configure</x-slot:title>
|
<x-slot:title>Validate and configure</x-slot:title>
|
||||||
<x-slot:content>
|
<x-slot:content>
|
||||||
<livewire:server.validate-and-install :server="$server" :ask="$server->isFunctional()" />
|
<livewire:server.validate-and-install :server="$server" :ask="$server->isFunctional()" />
|
||||||
</x-slot:content>
|
</x-slot:content>
|
||||||
<x-forms.button type="button"
|
<x-forms.button type="button" :isHighlighted="! $server->isFunctional()"
|
||||||
@click="slideOverOpen=true" wire:click.prevent="validateServer">
|
@click="processDialogOpen = true" wire:click.prevent="validateServer">
|
||||||
<x-reicon name="refresh" class="size-3.5" />
|
<x-reicon :name="$server->isFunctional() ? 'refresh' : 'alert-circle'" class="size-3.5" />
|
||||||
{{ $server->isFunctional() ? 'Revalidate connection' : 'Validate connection' }}
|
{{ $server->isFunctional() ? 'Revalidate connection' : 'Validate connection' }}
|
||||||
</x-forms.button>
|
</x-forms.button>
|
||||||
</x-slide-over>
|
</x-process-dialog>
|
||||||
</x-slot:actions>
|
</x-slot:actions>
|
||||||
|
|
||||||
@if ($this->limaStartCommand)
|
@if ($this->limaStartCommand)
|
||||||
@@ -261,10 +261,11 @@
|
|||||||
@if ($isBuildServerLocked)
|
@if ($isBuildServerLocked)
|
||||||
<x-forms.checkbox disabled id="isBuildServer"
|
<x-forms.checkbox disabled id="isBuildServer"
|
||||||
helper="This server already hosts resources and cannot become build-only."
|
helper="This server already hosts resources and cannot become build-only."
|
||||||
label="Use as a build server" />
|
label="Use as a dedicated build server" />
|
||||||
@else
|
@else
|
||||||
<x-forms.checkbox canGate="update" :canResource="$server" instantSave
|
<x-forms.checkbox canGate="update" :canResource="$server" instantSave
|
||||||
id="isBuildServer" label="Use as a build server"
|
id="isBuildServer" label="Use as a dedicated build server"
|
||||||
|
helper="Build servers compile applications but do not host deployments. Enabling this makes the server build-only."
|
||||||
:disabled="$isValidating" />
|
:disabled="$isValidating" />
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,6 +21,12 @@
|
|||||||
$showDocker = (bool) ($uptime && $supported_os_type && $prerequisites_installed);
|
$showDocker = (bool) ($uptime && $supported_os_type && $prerequisites_installed);
|
||||||
$showCompose = $showDocker;
|
$showCompose = $showDocker;
|
||||||
$showVersion = (bool) ($showDocker && $docker_compose_installed);
|
$showVersion = (bool) ($showDocker && $docker_compose_installed);
|
||||||
|
$validationComplete = (bool) ($uptime
|
||||||
|
&& $supported_os_type
|
||||||
|
&& $prerequisites_installed
|
||||||
|
&& $docker_installed
|
||||||
|
&& $docker_compose_installed
|
||||||
|
&& $docker_version);
|
||||||
|
|
||||||
$checkpoints = [
|
$checkpoints = [
|
||||||
[
|
[
|
||||||
@@ -66,7 +72,7 @@
|
|||||||
];
|
];
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex h-full min-h-0 flex-col gap-4 overflow-y-auto scrollbar">
|
||||||
@if ($ask)
|
@if ($ask)
|
||||||
<div
|
<div
|
||||||
class="rounded-[10px] border border-neutral-200 bg-neutral-50 px-4 py-3 text-[13px] leading-5 text-neutral-600 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-dim">
|
class="rounded-[10px] border border-neutral-200 bg-neutral-50 px-4 py-3 text-[13px] leading-5 text-neutral-600 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-dim">
|
||||||
@@ -77,28 +83,36 @@
|
|||||||
Continue
|
Continue
|
||||||
</x-forms.button>
|
</x-forms.button>
|
||||||
@else
|
@else
|
||||||
<section class="application-settings-section">
|
<div data-validation-checkpoints
|
||||||
<header>
|
class="overflow-hidden rounded-[10px] border border-neutral-200 dark:border-white/[0.08]">
|
||||||
<div class="flex items-center gap-2">
|
<div class="border-b border-neutral-200 px-4 py-2.5 dark:border-white/[0.08]">
|
||||||
<h3>Validation checkpoints</h3>
|
<h3 class="text-[13px] font-medium text-neutral-600 dark:text-fg-dim">Validation checkpoints</h3>
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<div class="application-settings-section-body is-flush">
|
|
||||||
<div class="divide-y divide-neutral-200 dark:divide-white/[0.07]">
|
|
||||||
@foreach ($checkpoints as $checkpoint)
|
|
||||||
@continue(! $checkpoint['visible'])
|
|
||||||
<x-checkpoint-item :title="$checkpoint['title']" :description="$checkpoint['description']"
|
|
||||||
:status="$checkpoint['status']" />
|
|
||||||
@endforeach
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
<div class="divide-y divide-neutral-200 dark:divide-white/[0.07]">
|
||||||
|
@foreach ($checkpoints as $checkpoint)
|
||||||
|
<x-checkpoint-item :title="$checkpoint['title']" :description="$checkpoint['description']"
|
||||||
|
:status="$checkpoint['status']" />
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<section class="application-settings-section">
|
@if ($validationComplete)
|
||||||
<div class="application-settings-section-body">
|
<div class="mt-auto flex shrink-0 items-center justify-between gap-3 rounded-[10px] border border-emerald-500/20 bg-emerald-500/[0.06] px-4 py-3">
|
||||||
<livewire:activity-monitor :header="$installationStep.' installation logs'" :showWaiting="false" />
|
<div class="flex items-center gap-2 text-[13px] font-medium text-emerald-700 dark:text-emerald-300">
|
||||||
|
<x-reicon name="check-circle" class="size-4 shrink-0" />
|
||||||
|
Validation complete
|
||||||
|
</div>
|
||||||
|
<x-forms.button type="button" @click="processDialogOpen = false">
|
||||||
|
Close
|
||||||
|
</x-forms.button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
@elseif ($isInstalling)
|
||||||
|
<section class="application-settings-section">
|
||||||
|
<div class="application-settings-section-body">
|
||||||
|
<livewire:activity-monitor :header="$installationStep.' installation logs'" :showWaiting="false" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
@endif
|
||||||
|
|
||||||
@isset($error)
|
@isset($error)
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
test('boarding utility actions are centered', function () {
|
||||||
|
$view = file_get_contents(resource_path('views/livewire/boarding/index.blade.php'));
|
||||||
|
|
||||||
|
expect($view)->toContain('class="mx-auto mt-6 flex w-full max-w-3xl flex-col items-center gap-3"');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('server validation opens in the centered process dialog', function () {
|
||||||
|
$view = file_get_contents(resource_path('views/livewire/boarding/index.blade.php'));
|
||||||
|
|
||||||
|
expect($view)
|
||||||
|
->toContain('<x-process-dialog closeWithX size="xl">')
|
||||||
|
->toContain('@click="processDialogOpen = true"')
|
||||||
|
->not->toContain('<x-slide-over closeWithX fullScreen>');
|
||||||
|
});
|
||||||
@@ -19,3 +19,20 @@ it('dispatches a server navbar refresh after toggling sentinel', function () {
|
|||||||
expect($matches['body'] ?? '')
|
expect($matches['body'] ?? '')
|
||||||
->toContain("\$this->dispatch('refreshServerShow');");
|
->toContain("\$this->dispatch('refreshServerShow');");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('only marks sentinel enabled after startup succeeds', function () {
|
||||||
|
$componentSource = file_get_contents(app_path('Livewire/Server/Sentinel.php'));
|
||||||
|
|
||||||
|
preg_match('/public function toggleSentinel\([^)]*\).*?\{(?<body>.*?)\n \}/s', $componentSource, $matches);
|
||||||
|
$toggleBody = $matches['body'] ?? '';
|
||||||
|
|
||||||
|
expect(strpos($toggleBody, 'StartSentinel::run'))->toBeLessThan(
|
||||||
|
strpos($toggleBody, '$this->isSentinelEnabled = true;')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not repeat a disabled status badge in the sentinel empty state', function () {
|
||||||
|
$view = file_get_contents(resource_path('views/livewire/server/sentinel.blade.php'));
|
||||||
|
|
||||||
|
expect($view)->not->toContain("? 'Disabled'");
|
||||||
|
});
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
test('server settings explain dedicated build server mode', function () {
|
||||||
|
$view = file_get_contents(resource_path('views/livewire/server/show.blade.php'));
|
||||||
|
|
||||||
|
expect($view)
|
||||||
|
->toContain('label="Use as a dedicated build server"')
|
||||||
|
->toContain('helper="Build servers compile applications but do not host deployments. Enabling this makes the server build-only."');
|
||||||
|
});
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
test('server creation uses the standard page title without redundant navigation', function () {
|
||||||
|
$view = file_get_contents(resource_path('views/livewire/server/create.blade.php'));
|
||||||
|
|
||||||
|
expect($view)
|
||||||
|
->toContain('<h1 class="min-w-0 text-[24px]! leading-7! font-semibold! tracking-tight!">New server</h1>')
|
||||||
|
->not->toContain('Back to servers')
|
||||||
|
->not->toContain('title="Add a server"');
|
||||||
|
});
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
test('server creation keeps private key actions together and advanced options collapsed', function () {
|
||||||
|
$view = file_get_contents(resource_path('views/livewire/server/new/by-ip.blade.php'));
|
||||||
|
|
||||||
|
expect($view)
|
||||||
|
->toContain('class="flex items-end gap-3"')
|
||||||
|
->toContain('x-data="{ advancedOpen: false }"')
|
||||||
|
->toContain('x-show="advancedOpen" x-cloak')
|
||||||
|
->toContain('Advanced settings')
|
||||||
|
->toContain('label="Use as a dedicated build server"')
|
||||||
|
->toContain('helper="Build servers compile applications but do not host deployments. Enabling this makes the server build-only."');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('server creation places the IP address and private key before optional details', function () {
|
||||||
|
$view = file_get_contents(resource_path('views/livewire/server/new/by-ip.blade.php'));
|
||||||
|
|
||||||
|
expect($view)
|
||||||
|
->toContain('class="mb-5"')
|
||||||
|
->and(strpos($view, 'id="ip"'))->toBeLessThan(strpos($view, 'id="private_key_id"'))
|
||||||
|
->and(strpos($view, 'id="private_key_id"'))->toBeLessThan(strpos($view, 'id="name"'));
|
||||||
|
});
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
test('sentinel-required metrics state does not repeat an unavailable badge', function () {
|
||||||
|
$view = file_get_contents(resource_path('views/livewire/server/charts.blade.php'));
|
||||||
|
$sentinelRequiredState = str($view)->after('@else')->before('@endif')->toString();
|
||||||
|
|
||||||
|
expect($sentinelRequiredState)
|
||||||
|
->toContain('title="Sentinel is required"')
|
||||||
|
->not->toContain('status="Unavailable"');
|
||||||
|
});
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
test('server revalidation opens in the centered process dialog', function () {
|
||||||
|
$view = file_get_contents(resource_path('views/livewire/server/show.blade.php'));
|
||||||
|
|
||||||
|
expect($view)
|
||||||
|
->toContain('<x-process-dialog closeWithX size="xl">')
|
||||||
|
->toContain(':isHighlighted="! $server->isFunctional()"')
|
||||||
|
->toContain('@click="processDialogOpen = true" wire:click.prevent="validateServer"');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('completed server validation shows a close action instead of empty logs', function () {
|
||||||
|
$view = file_get_contents(resource_path('views/livewire/server/validate-and-install.blade.php'));
|
||||||
|
|
||||||
|
expect($view)
|
||||||
|
->toContain('$validationComplete')
|
||||||
|
->toContain('mt-auto')
|
||||||
|
->toContain('<x-forms.button type="button" @click="processDialogOpen = false">')
|
||||||
|
->toContain('@click="processDialogOpen = false"')
|
||||||
|
->toContain('Validation complete')
|
||||||
|
->toContain('Close');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('installation logs are only shown after an installation starts', function () {
|
||||||
|
$view = file_get_contents(resource_path('views/livewire/server/validate-and-install.blade.php'));
|
||||||
|
$component = file_get_contents(app_path('Livewire/Server/ValidateAndInstall.php'));
|
||||||
|
|
||||||
|
expect($view)->toContain('@elseif ($isInstalling)')
|
||||||
|
->and($component)
|
||||||
|
->toContain('public bool $isInstalling = false;')
|
||||||
|
->toContain('$this->isInstalling = true;');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('server validation content scrolls within the dialog', function () {
|
||||||
|
$view = file_get_contents(resource_path('views/livewire/server/validate-and-install.blade.php'));
|
||||||
|
$activityMonitor = file_get_contents(resource_path('views/livewire/activity-monitor.blade.php'));
|
||||||
|
|
||||||
|
expect($view)->toContain('class="flex h-full min-h-0 flex-col gap-4 overflow-y-auto scrollbar"')
|
||||||
|
->and($activityMonitor)->toContain("'overflow-hidden' => !\$fullHeight")
|
||||||
|
->and($activityMonitor)->not->toContain("'h-full overflow-hidden' => !\$fullHeight");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('validation checkpoints use the standard bordered list treatment', function () {
|
||||||
|
$view = file_get_contents(resource_path('views/livewire/server/validate-and-install.blade.php'));
|
||||||
|
|
||||||
|
expect($view)
|
||||||
|
->toContain('data-validation-checkpoints')
|
||||||
|
->toContain('overflow-hidden rounded-[10px] border border-neutral-200 dark:border-white/[0.08]');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('all validation checkpoints remain visible while only the current phase runs', function () {
|
||||||
|
$view = file_get_contents(resource_path('views/livewire/server/validate-and-install.blade.php'));
|
||||||
|
|
||||||
|
expect($view)->not->toContain("@continue(! \$checkpoint['visible'])");
|
||||||
|
});
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
test('deployment log timestamps omit microseconds', function () {
|
||||||
|
$helper = file_get_contents(dirname(__DIR__, 2).'/bootstrap/helpers/remoteProcess.php');
|
||||||
|
|
||||||
|
expect($helper)
|
||||||
|
->toContain("->format('Y-M-d H:i:s')")
|
||||||
|
->not->toContain("->format('Y-M-d H:i:s.u')");
|
||||||
|
});
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
test('sentinel startup regenerates an empty endpoint from instance settings', function () {
|
||||||
|
$action = file_get_contents(dirname(__DIR__, 2).'/app/Actions/Server/StartSentinel.php');
|
||||||
|
$component = file_get_contents(dirname(__DIR__, 2).'/app/Livewire/Server/Sentinel.php');
|
||||||
|
|
||||||
|
expect($action)
|
||||||
|
->toContain('ensureSentinelUrl()')
|
||||||
|
->and($component)
|
||||||
|
->toContain('$this->sentinelCustomUrl = $this->server->settings->sentinel_custom_url;')
|
||||||
|
->and(file_get_contents(dirname(__DIR__, 2).'/app/Models/ServerSetting.php'))
|
||||||
|
->toContain('generateSentinelUrl(ignoreEvent: true)')
|
||||||
|
->toContain('Set an instance FQDN or public IP before enabling Sentinel.')
|
||||||
|
->and($component)
|
||||||
|
->toContain('$this->sentinelCustomUrl = $this->server->settings->sentinel_custom_url;');
|
||||||
|
});
|
||||||
Reference in new issue
Block a user