Merge pull request #3937 from coollabsio/ui

UI
This commit is contained in:
Andras Bacsai
2024-10-17 15:45:12 +02:00
committed by GitHub
29 changed files with 615 additions and 392 deletions

View File

@@ -5,13 +5,13 @@ namespace App\Actions\Server;
use App\Models\Server; use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
class RemoveServer class DeleteServer
{ {
use AsAction; use AsAction;
public function handle(Server $server) public function handle(Server $server)
{ {
StopSentinel::run($server); StopSentinel::run($server);
$server->delete(); $server->forceDelete();
} }
} }

View File

@@ -34,9 +34,9 @@ class StartSentinel
'COLLECTOR_RETENTION_PERIOD_DAYS' => $metrics_history, 'COLLECTOR_RETENTION_PERIOD_DAYS' => $metrics_history,
]; ];
if (isDev()) { if (isDev()) {
data_set($environments, 'DEBUG', 'true'); // data_set($environments, 'DEBUG', 'true');
$mount_dir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/sentinel'; $mount_dir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/sentinel';
$image = 'sentinel'; // $image = 'sentinel';
} }
$docker_environments = '-e "' . implode('" -e "', array_map(fn($key, $value) => "$key=$value", array_keys($environments), $environments)) . '"'; $docker_environments = '-e "' . implode('" -e "', array_map(fn($key, $value) => "$key=$value", array_keys($environments), $environments)) . '"';

View File

@@ -2,7 +2,7 @@
namespace App\Http\Controllers\Api; namespace App\Http\Controllers\Api;
use App\Actions\Server\RemoveServer; use App\Actions\Server\DeleteServer;
use App\Actions\Server\ValidateServer; use App\Actions\Server\ValidateServer;
use App\Enums\ProxyStatus; use App\Enums\ProxyStatus;
use App\Enums\ProxyTypes; use App\Enums\ProxyTypes;
@@ -726,7 +726,8 @@ class ServersController extends Controller
if ($server->definedResources()->count() > 0) { if ($server->definedResources()->count() > 0) {
return response()->json(['message' => 'Server has resources, so you need to delete them before.'], 400); return response()->json(['message' => 'Server has resources, so you need to delete them before.'], 400);
} }
RemoveServer::dispatch($server); $server->delete();
DeleteServer::dispatch($server);
return response()->json(['message' => 'Server deleted.']); return response()->json(['message' => 'Server deleted.']);
} }

View File

@@ -66,7 +66,7 @@ class Show extends Component
return ! $alreadyAddedNetworks->contains('network', $network['Name']); return ! $alreadyAddedNetworks->contains('network', $network['Name']);
}); });
if ($this->networks->count() === 0) { if ($this->networks->count() === 0) {
$this->dispatch('success', 'No new networks found.'); $this->dispatch('success', 'No new destinations found on this server.');
return; return;
} }

View File

@@ -0,0 +1,61 @@
<?php
namespace App\Livewire\Server;
use App\Models\Server;
use Livewire\Component;
class Advanced extends Component
{
public Server $server;
protected $rules = [
'server.settings.concurrent_builds' => 'required|integer|min:1',
'server.settings.dynamic_timeout' => 'required|integer|min:1',
'server.settings.force_docker_cleanup' => 'required|boolean',
'server.settings.docker_cleanup_frequency' => 'required_if:server.settings.force_docker_cleanup,true|string',
'server.settings.docker_cleanup_threshold' => 'required_if:server.settings.force_docker_cleanup,false|integer|min:1|max:100',
'server.settings.delete_unused_volumes' => 'boolean',
'server.settings.delete_unused_networks' => 'boolean',
];
protected $validationAttributes = [
'server.settings.concurrent_builds' => 'Concurrent Builds',
'server.settings.dynamic_timeout' => 'Dynamic Timeout',
'server.settings.force_docker_cleanup' => 'Force Docker Cleanup',
'server.settings.docker_cleanup_frequency' => 'Docker Cleanup Frequency',
'server.settings.docker_cleanup_threshold' => 'Docker Cleanup Threshold',
'server.settings.delete_unused_volumes' => 'Delete Unused Volumes',
'server.settings.delete_unused_networks' => 'Delete Unused Networks',
];
public function instantSave()
{
try {
$this->validate();
$this->server->settings->save();
$this->dispatch('success', 'Server updated.');
$this->dispatch('refreshServerShow');
} catch (\Throwable $e) {
$this->server->settings->refresh();
return handleError($e, $this);
}
}
public function submit()
{
try {
$frequency = $this->server->settings->docker_cleanup_frequency;
if (empty($frequency) || ! validate_cron_expression($frequency)) {
$this->server->settings->docker_cleanup_frequency = '*/10 * * * *';
throw new \Exception('Invalid Cron / Human expression for Docker Cleanup Frequency. Resetting to default 10 minutes.');
}
$this->server->settings->save();
$this->dispatch('success', 'Server updated.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.server.advanced');
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Livewire\Server;
use App\Models\Server;
use Livewire\Component;
class CloudflareTunnels extends Component
{
public Server $server;
protected $rules = [
'server.settings.is_cloudflare_tunnel' => 'required|boolean',
];
protected $validationAttributes = [
'server.settings.is_cloudflare_tunnel' => 'Cloudflare Tunnel',
];
public function instantSave()
{
try {
$this->validate();
$this->server->settings->save();
$this->dispatch('success', 'Server updated.');
$this->dispatch('refreshServerShow');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function manualCloudflareConfig()
{
$this->server->settings->is_cloudflare_tunnel = true;
$this->server->settings->save();
$this->server->refresh();
$this->dispatch('success', 'Cloudflare Tunnels enabled.');
}
public function render()
{
return view('livewire.server.cloudflare-tunnels');
}
}

View File

@@ -2,7 +2,7 @@
namespace App\Livewire\Server; namespace App\Livewire\Server;
use App\Actions\Server\RemoveServer; use App\Actions\Server\DeleteServer;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
@@ -28,8 +28,8 @@ class Delete extends Component
return; return;
} }
RemoveServer::run($this->server); $this->server->delete();
DeleteServer::dispatch($this->server);
return redirect()->route('server.index'); return redirect()->route('server.index');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);

View File

@@ -7,7 +7,6 @@ use App\Actions\Server\StopSentinel;
use App\Jobs\DockerCleanupJob; use App\Jobs\DockerCleanupJob;
use App\Jobs\PullSentinelImageJob; use App\Jobs\PullSentinelImageJob;
use App\Models\Server; use App\Models\Server;
use Illuminate\Support\Facades\Http;
use Livewire\Component; use Livewire\Component;
class Form extends Component class Form extends Component
@@ -47,27 +46,19 @@ class Form extends Component
'server.ip' => 'required', 'server.ip' => 'required',
'server.user' => 'required', 'server.user' => 'required',
'server.port' => 'required', 'server.port' => 'required',
'server.settings.is_cloudflare_tunnel' => 'required|boolean', 'wildcard_domain' => 'nullable|url',
'server.settings.is_reachable' => 'required', 'server.settings.is_reachable' => 'required',
'server.settings.is_swarm_manager' => 'required|boolean', 'server.settings.is_swarm_manager' => 'required|boolean',
'server.settings.is_swarm_worker' => 'required|boolean', 'server.settings.is_swarm_worker' => 'required|boolean',
'server.settings.is_build_server' => 'required|boolean', 'server.settings.is_build_server' => 'required|boolean',
'server.settings.concurrent_builds' => 'required|integer|min:1',
'server.settings.dynamic_timeout' => 'required|integer|min:1',
'server.settings.is_metrics_enabled' => 'required|boolean', 'server.settings.is_metrics_enabled' => 'required|boolean',
'server.settings.sentinel_token' => 'required', 'server.settings.sentinel_token' => 'required',
'server.settings.sentinel_metrics_refresh_rate_seconds' => 'required|integer|min:1', 'server.settings.sentinel_metrics_refresh_rate_seconds' => 'required|integer|min:1',
'server.settings.sentinel_metrics_history_days' => 'required|integer|min:1', 'server.settings.sentinel_metrics_history_days' => 'required|integer|min:1',
'server.settings.sentinel_push_interval_seconds' => 'required|integer|min:10', 'server.settings.sentinel_push_interval_seconds' => 'required|integer|min:10',
'wildcard_domain' => 'nullable|url',
'server.settings.sentinel_custom_url' => 'nullable|url', 'server.settings.sentinel_custom_url' => 'nullable|url',
'server.settings.is_sentinel_enabled' => 'required|boolean', 'server.settings.is_sentinel_enabled' => 'required|boolean',
'server.settings.server_timezone' => 'required|string|timezone', 'server.settings.server_timezone' => 'required|string|timezone',
'server.settings.force_docker_cleanup' => 'required|boolean',
'server.settings.docker_cleanup_frequency' => 'required_if:server.settings.force_docker_cleanup,true|string',
'server.settings.docker_cleanup_threshold' => 'required_if:server.settings.force_docker_cleanup,false|integer|min:1|max:100',
'server.settings.delete_unused_volumes' => 'boolean',
'server.settings.delete_unused_networks' => 'boolean',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
@@ -76,23 +67,18 @@ class Form extends Component
'server.ip' => 'IP address/Domain', 'server.ip' => 'IP address/Domain',
'server.user' => 'User', 'server.user' => 'User',
'server.port' => 'Port', 'server.port' => 'Port',
'server.settings.is_cloudflare_tunnel' => 'Cloudflare Tunnel',
'server.settings.is_reachable' => 'Is reachable', 'server.settings.is_reachable' => 'Is reachable',
'server.settings.is_swarm_manager' => 'Swarm Manager', 'server.settings.is_swarm_manager' => 'Swarm Manager',
'server.settings.is_swarm_worker' => 'Swarm Worker', 'server.settings.is_swarm_worker' => 'Swarm Worker',
'server.settings.is_build_server' => 'Build Server', 'server.settings.is_build_server' => 'Build Server',
'server.settings.concurrent_builds' => 'Concurrent Builds',
'server.settings.dynamic_timeout' => 'Dynamic Timeout',
'server.settings.is_metrics_enabled' => 'Metrics', 'server.settings.is_metrics_enabled' => 'Metrics',
'server.settings.sentinel_token' => 'Metrics Token', 'server.settings.sentinel_token' => 'Metrics Token',
'server.settings.sentinel_metrics_refresh_rate_seconds' => 'Metrics Interval', 'server.settings.sentinel_metrics_refresh_rate_seconds' => 'Metrics Interval',
'server.settings.sentinel_metrics_history_days' => 'Metrics History', 'server.settings.sentinel_metrics_history_days' => 'Metrics History',
'server.settings.sentinel_push_interval_seconds' => 'Push Interval', 'server.settings.sentinel_push_interval_seconds' => 'Push Interval',
'server.settings.is_sentinel_enabled' => 'Server API', 'server.settings.is_sentinel_enabled' => 'Server API',
'server.settings.sentinel_custom_url' => 'Sentinel URL', 'server.settings.sentinel_custom_url' => 'Coolify URL',
'server.settings.server_timezone' => 'Server Timezone', 'server.settings.server_timezone' => 'Server Timezone',
'server.settings.delete_unused_volumes' => 'Delete Unused Volumes',
'server.settings.delete_unused_networks' => 'Delete Unused Networks',
]; ];
public function mount(Server $server) public function mount(Server $server)
@@ -100,13 +86,10 @@ class Form extends Component
$this->server = $server; $this->server = $server;
$this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray(); $this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray();
$this->wildcard_domain = $this->server->settings->wildcard_domain; $this->wildcard_domain = $this->server->settings->wildcard_domain;
$this->server->settings->docker_cleanup_threshold = $this->server->settings->docker_cleanup_threshold;
$this->server->settings->docker_cleanup_frequency = $this->server->settings->docker_cleanup_frequency;
$this->server->settings->delete_unused_volumes = $server->settings->delete_unused_volumes;
$this->server->settings->delete_unused_networks = $server->settings->delete_unused_networks;
} }
public function checkSyncStatus(){ public function checkSyncStatus()
{
$this->server->refresh(); $this->server->refresh();
$this->server->settings->refresh(); $this->server->settings->refresh();
} }
@@ -114,9 +97,10 @@ class Form extends Component
public function regenerateSentinelToken() public function regenerateSentinelToken()
{ {
try { try {
$this->server->generateSentinelToken(); $this->server->settings->generateSentinelToken();
$this->server->settings->refresh(); $this->server->settings->refresh();
$this->dispatch('success', 'Sentinel token regenerated. Please restart your Sentinel.'); $this->restartSentinel(notification: false);
$this->dispatch('success', 'Token regenerated & Sentinel restarted.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }
@@ -152,25 +136,35 @@ class Form extends Component
$this->dispatch('proxyStatusUpdated'); $this->dispatch('proxyStatusUpdated');
} }
public function updatedServerSettingsIsSentinelEnabled($value){ public function updatedServerSettingsIsSentinelEnabled($value)
{
$this->validate();
$this->validate([
'server.settings.sentinel_custom_url' => 'required|url',
]);
if ($value === false) { if ($value === false) {
StopSentinel::dispatch($this->server); StopSentinel::dispatch($this->server);
$this->server->settings->is_metrics_enabled = false; $this->server->settings->is_metrics_enabled = false;
$this->server->settings->save(); $this->server->settings->save();
$this->server->sentinelHeartbeat(isReset: true); $this->server->sentinelHeartbeat(isReset: true);
} else { } else {
try {
StartSentinel::run($this->server); StartSentinel::run($this->server);
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
} }
public function updatedServerSettingsIsMetricsEnabled(){ public function updatedServerSettingsIsMetricsEnabled()
{
$this->restartSentinel(); $this->restartSentinel();
} }
public function instantSave() public function instantSave()
{ {
try { try {
$this->validate();
refresh_server_connection($this->server->privateKey); refresh_server_connection($this->server->privateKey);
$this->validateServer(false); $this->validateServer(false);
@@ -179,6 +173,14 @@ class Form extends Component
$this->dispatch('success', 'Server updated.'); $this->dispatch('success', 'Server updated.');
$this->dispatch('refreshServerShow'); $this->dispatch('refreshServerShow');
// if ($this->server->isSentinelEnabled()) {
// StartSentinel::run($this->server);
// } else {
// StopSentinel::run($this->server);
// $this->server->settings->is_metrics_enabled = false;
// $this->server->settings->save();
// $this->server->sentinelHeartbeat(isReset: true);
// }
// if ($this->server->isSentinelEnabled()) { // if ($this->server->isSentinelEnabled()) {
// PullSentinelImageJob::dispatchSync($this->server); // PullSentinelImageJob::dispatchSync($this->server);
// ray('Sentinel is enabled'); // ray('Sentinel is enabled');
@@ -196,16 +198,24 @@ class Form extends Component
// $this->checkPortForServerApi(); // $this->checkPortForServerApi();
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->server->settings->refresh();
return handleError($e, $this); return handleError($e, $this);
} }
} }
public function restartSentinel() public function restartSentinel($notification = true)
{ {
try { try {
$this->validate();
$this->validate([
'server.settings.sentinel_custom_url' => 'required|url',
]);
$version = get_latest_sentinel_version(); $version = get_latest_sentinel_version();
StartSentinel::run($this->server, $version, true); StartSentinel::run($this->server, $version, true);
if ($notification) {
$this->dispatch('success', 'Sentinel started.'); $this->dispatch('success', 'Sentinel started.');
}
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -1,16 +0,0 @@
<?php
namespace App\Livewire\Server\Proxy;
use App\Models\Server;
use Livewire\Component;
class Modal extends Component
{
public Server $server;
public function proxyStatusUpdated()
{
$this->dispatch('proxyStatusUpdated');
}
}

View File

@@ -15,7 +15,9 @@ class Resources extends Component
public $parameters = []; public $parameters = [];
public Collection $unmanagedContainers; public Collection $containers;
public $activeTab = 'managed';
public function getListeners() public function getListeners()
{ {
@@ -50,14 +52,29 @@ class Resources extends Component
public function refreshStatus() public function refreshStatus()
{ {
$this->server->refresh(); $this->server->refresh();
if ($this->activeTab === 'managed') {
$this->loadManagedContainers();
} else {
$this->loadUnmanagedContainers(); $this->loadUnmanagedContainers();
}
$this->dispatch('success', 'Resource statuses refreshed.'); $this->dispatch('success', 'Resource statuses refreshed.');
} }
public function loadManagedContainers()
{
try {
$this->activeTab = 'managed';
$this->containers = $this->server->refresh()->definedResources();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function loadUnmanagedContainers() public function loadUnmanagedContainers()
{ {
$this->activeTab = 'unmanaged';
try { try {
$this->unmanagedContainers = $this->server->loadUnmanagedContainers(); $this->containers = $this->server->loadUnmanagedContainers();
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }
@@ -65,13 +82,14 @@ class Resources extends Component
public function mount() public function mount()
{ {
$this->unmanagedContainers = collect(); $this->containers = collect();
$this->parameters = get_route_parameters(); $this->parameters = get_route_parameters();
try { try {
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first(); $this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first();
if (is_null($this->server)) { if (is_null($this->server)) {
return redirect()->route('server.index'); return redirect()->route('server.index');
} }
$this->loadManagedContainers();
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -2,7 +2,6 @@
namespace App\Livewire\Server; namespace App\Livewire\Server;
use App\Models\PrivateKey;
use App\Models\Server; use App\Models\Server;
use Livewire\Component; use Livewire\Component;
@@ -14,15 +13,29 @@ class ShowPrivateKey extends Component
public $parameters; public $parameters;
public function mount()
{
$this->parameters = get_route_parameters();
}
public function setPrivateKey($privateKeyId) public function setPrivateKey($privateKeyId)
{ {
$originalPrivateKeyId = $this->server->getOriginal('private_key_id');
try { try {
$privateKey = PrivateKey::findOrFail($privateKeyId); $this->server->update(['private_key_id' => $privateKeyId]);
$this->server->update(['private_key_id' => $privateKey->id]); ['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection();
$this->server->refresh(); if ($uptime) {
$this->dispatch('success', 'Private key updated successfully.'); $this->dispatch('success', 'Private key updated successfully.');
} else {
throw new \Exception('Server is not reachable.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help.<br><br>Error: '.$error);
}
} catch (\Exception $e) { } catch (\Exception $e) {
$this->server->update(['private_key_id' => $originalPrivateKeyId]);
$this->server->validateConnection();
$this->dispatch('error', 'Failed to update private key: '.$e->getMessage()); $this->dispatch('error', 'Failed to update private key: '.$e->getMessage());
} finally {
$this->dispatch('refreshServerShow');
$this->server->refresh();
} }
} }
@@ -33,18 +46,16 @@ class ShowPrivateKey extends Component
if ($uptime) { if ($uptime) {
$this->dispatch('success', 'Server is reachable.'); $this->dispatch('success', 'Server is reachable.');
} else { } else {
ray($error);
$this->dispatch('error', 'Server is not reachable.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help.<br><br>Error: '.$error); $this->dispatch('error', 'Server is not reachable.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help.<br><br>Error: '.$error);
return; return;
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} finally {
$this->dispatch('refreshServerShow');
$this->server->refresh();
} }
} }
public function mount()
{
$this->parameters = get_route_parameters();
}
} }

View File

@@ -7,6 +7,7 @@ use App\Enums\ProxyTypes;
use App\Jobs\PullSentinelImageJob; use App\Jobs\PullSentinelImageJob;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
@@ -17,7 +18,6 @@ use OpenApi\Attributes as OA;
use Spatie\SchemalessAttributes\Casts\SchemalessAttributes; use Spatie\SchemalessAttributes\Casts\SchemalessAttributes;
use Spatie\SchemalessAttributes\SchemalessAttributesTrait; use Spatie\SchemalessAttributes\SchemalessAttributesTrait;
use Spatie\Url\Url; use Spatie\Url\Url;
use Illuminate\Support\Str;
use Symfony\Component\Yaml\Yaml; use Symfony\Component\Yaml\Yaml;
#[OA\Schema( #[OA\Schema(
@@ -45,7 +45,7 @@ use Symfony\Component\Yaml\Yaml;
class Server extends BaseModel class Server extends BaseModel
{ {
use SchemalessAttributesTrait; use SchemalessAttributesTrait,SoftDeletes;
public static $batch_counter = 0; public static $batch_counter = 0;
@@ -97,7 +97,8 @@ class Server extends BaseModel
} }
} }
}); });
static::deleting(function ($server) {
static::forceDeleting(function ($server) {
$server->destinations()->each(function ($destination) { $server->destinations()->each(function ($destination) {
$destination->delete(); $destination->delete();
}); });
@@ -527,34 +528,6 @@ $schema://$host {
Storage::disk('ssh-mux')->delete($this->muxFilename()); Storage::disk('ssh-mux')->delete($this->muxFilename());
} }
public function generateSentinelUrl() {
if ($this->isLocalhost()) {
return 'http://host.docker.internal:8000';
}
$settings = InstanceSettings::get();
if ($settings->fqdn) {
return $settings->fqdn;
}
if ($settings->ipv4) {
return $settings->ipv4 . ':8000';
}
if ($settings->ipv6) {
return $settings->ipv6 . ':8000';
}
return null;
}
public function generateSentinelToken()
{
$data = [
'server_uuid' => $this->uuid,
];
$token = json_encode($data);
$encrypted = encrypt($token);
$this->settings->sentinel_token = $encrypted;
$this->settings->save();
return $encrypted;
}
public function sentinelHeartbeat(bool $isReset = false) public function sentinelHeartbeat(bool $isReset = false)
{ {
@@ -568,7 +541,7 @@ $schema://$host {
public function isSentinelEnabled() public function isSentinelEnabled()
{ {
return $this->isMetricsEnabled() || $this->isServerApiEnabled() || !$this->isBuildServer(); return ($this->isMetricsEnabled() || $this->isServerApiEnabled()) && !$this->isBuildServer();
} }
public function isMetricsEnabled() public function isMetricsEnabled()

View File

@@ -59,10 +59,59 @@ class ServerSetting extends Model
protected static function booted() protected static function booted()
{ {
static::creating(function ($setting) { static::creating(function ($setting) {
try {
if (str($setting->sentinel_token)->isEmpty()) {
$setting->generateSentinelToken(save: false);
}
if (str($setting->sentinel_custom_url)->isEmpty()) {
$url = $setting->generateSentinelUrl(save: false);
if (str($url)->isEmpty()) {
$setting->is_sentinel_enabled = false;
} else {
$setting->is_sentinel_enabled = true; $setting->is_sentinel_enabled = true;
}
}
} catch (\Throwable $e) {
loggy('Error creating server setting: ' . $e->getMessage());
}
}); });
} }
public function generateSentinelToken(bool $save = true)
{
$data = [
'server_uuid' => $this->server->uuid,
];
$token = json_encode($data);
$encrypted = encrypt($token);
$this->sentinel_token = $encrypted;
if ($save) {
$this->save();
}
return $encrypted;
}
public function generateSentinelUrl(bool $save = true)
{
$domain = null;
$settings = InstanceSettings::get();
if ($this->server->isLocalhost()) {
$domain = 'http://host.docker.internal:8000';
} else if ($settings->fqdn) {
$domain = $settings->fqdn;
} else if ($settings->ipv4) {
$domain = $settings->ipv4 . ':8000';
} else if ($settings->ipv6) {
$domain = $settings->ipv6 . ':8000';
}
$this->sentinel_custom_url = $domain;
if ($save) {
$this->save();
}
return $domain;
}
public function server() public function server()
{ {
return $this->belongsTo(Server::class); return $this->belongsTo(Server::class);

View File

@@ -126,7 +126,7 @@ function refreshSession(?Team $team = null): void
} }
function handleError(?Throwable $error = null, ?Livewire\Component $livewire = null, ?string $customErrorMessage = null) function handleError(?Throwable $error = null, ?Livewire\Component $livewire = null, ?string $customErrorMessage = null)
{ {
ray($error); loggy($error);
if ($error instanceof TooManyRequestsException) { if ($error instanceof TooManyRequestsException) {
if (isset($livewire)) { if (isset($livewire)) {
return $livewire->dispatch('error', "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds."); return $livewire->dispatch('error', "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds.");

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('servers', function (Blueprint $table) {
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('servers', function (Blueprint $table) {
$table->dropSoftDeletes();
});
}
};

View File

@@ -9,22 +9,23 @@ class SentinelSeeder extends Seeder
{ {
public function run() public function run()
{ {
try {
Server::chunk(100, function ($servers) { Server::chunk(100, function ($servers) {
foreach ($servers as $server) { foreach ($servers as $server) {
try {
if (str($server->settings->sentinel_token)->isEmpty()) { if (str($server->settings->sentinel_token)->isEmpty()) {
$server->generateSentinelToken(); $server->settings->generateSentinelToken();
} }
if (str($server->settings->sentinel_custom_url)->isEmpty()) { if (str($server->settings->sentinel_custom_url)->isEmpty()) {
$url = $server->generateSentinelUrl(); $url = $server->settings->generateSentinelUrl();
$server->settings->sentinel_custom_url = $url; if (str($url)->isEmpty()) {
$server->settings->is_sentinel_enabled = false;
$server->settings->save(); $server->settings->save();
} }
} }
});
} catch (\Throwable $e) { } catch (\Throwable $e) {
echo "Error: {$e->getMessage()}\n"; loggy("Error: {$e->getMessage()}\n");
ray($e->getMessage());
} }
} }
});
}
} }

View File

@@ -1,5 +1,14 @@
<div class="pb-6"> <div class="pb-6">
<livewire:server.proxy.modal :server="$server" /> <x-modal modalId="startProxy">
<x-slot:modalBody>
<livewire:activity-monitor header="Proxy Startup Logs" />
</x-slot:modalBody>
<x-slot:modalSubmit>
<x-forms.button onclick="startProxy.close()" type="submit">
Close
</x-forms.button>
</x-slot:modalSubmit>
</x-modal>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h1>Server</h1> <h1>Server</h1>
@if ($server->proxySet()) @if ($server->proxySet())
@@ -13,20 +22,9 @@
href="{{ route('server.show', [ href="{{ route('server.show', [
'server_uuid' => data_get($parameters, 'server_uuid'), 'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}"> ]) }}">
<button>General</button> <button>Configuration</button>
</a>
<a class="{{ request()->routeIs('server.private-key') ? 'dark:text-white' : '' }}"
href="{{ route('server.private-key', [
'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}">
<button>Private Key</button>
</a>
<a class="{{ request()->routeIs('server.resources') ? 'dark:text-white' : '' }}"
href="{{ route('server.resources', [
'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}">
<button>Resources</button>
</a> </a>
@if (!$server->isSwarmWorker() && !$server->settings->is_build_server) @if (!$server->isSwarmWorker() && !$server->settings->is_build_server)
<a class="{{ request()->routeIs('server.proxy') ? 'dark:text-white' : '' }}" <a class="{{ request()->routeIs('server.proxy') ? 'dark:text-white' : '' }}"
href="{{ route('server.proxy', [ href="{{ route('server.proxy', [
@@ -34,18 +32,6 @@
]) }}"> ]) }}">
<button>Proxy</button> <button>Proxy</button>
</a> </a>
<a class="{{ request()->routeIs('server.destinations') ? 'dark:text-white' : '' }}"
href="{{ route('server.destinations', [
'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}">
<button>Destinations</button>
</a>
<a class="{{ request()->routeIs('server.log-drains') ? 'dark:text-white' : '' }}"
href="{{ route('server.log-drains', [
'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}">
<button>Log Drains</button>
</a>
@endif @endif
</nav> </nav>
<div class="order-first sm:order-last"> <div class="order-first sm:order-last">

View File

@@ -5,10 +5,10 @@
<x-modal-input buttonTitle="+ Add" title="New Destination"> <x-modal-input buttonTitle="+ Add" title="New Destination">
<livewire:destination.new.docker :server_id="$server->id" /> <livewire:destination.new.docker :server_id="$server->id" />
</x-modal-input> </x-modal-input>
<x-forms.button wire:click='scan'>Scan Destinations</x-forms.button> <x-forms.button wire:click='scan'>Scan for Destinations</x-forms.button>
</div> </div>
<div class="pt-2 pb-6 ">Destinations are used to segregate resources by network.</div> <div>Destinations are used to segregate resources by network.</div>
<div class="flex gap-2 "> <div class="flex gap-2 pt-6">
Available for using: Available for using:
@forelse ($server->standaloneDockers as $docker) @forelse ($server->standaloneDockers as $docker)
<a href="{{ route('destination.show', ['destination_uuid' => data_get($docker, 'uuid')]) }}"> <a href="{{ route('destination.show', ['destination_uuid' => data_get($docker, 'uuid')]) }}">

View File

@@ -0,0 +1,84 @@
<form wire:submit='submit'>
<div>
<div class="flex items-center gap-2">
<h2>Advanced</h2>
<x-forms.button type="submit">Save</x-forms.button>
<x-modal-confirmation title="Confirm Docker Cleanup?" buttonTitle="Trigger Manual Cleanup"
submitAction="manualCleanup" :actions="[
'Permanently deletes all stopped containers managed by Coolify (as containers are non-persistent, no data will be lost)',
'Permanently deletes all unused images',
'Clears build cache',
'Removes old versions of the Coolify helper image',
'Optionally permanently deletes all unused volumes (if enabled in advanced options).',
'Optionally permanently deletes all unused networks (if enabled in advanced options).',
]" :confirmWithText="false" :confirmWithPassword="false"
step2ButtonText="Trigger Docker Cleanup" />
</div>
<div>Advanced configuration for your server.</div>
</div>
<div class="flex flex-col gap-4 pt-4">
<div class="flex flex-col gap-2">
<div class="flex items-center gap-2">
<h3>Docker Cleanup</h3>
</div>
<div class="flex flex-wrap items-center gap-4">
@if ($server->settings->force_docker_cleanup)
<x-forms.input placeholder="*/10 * * * *" id="server.settings.docker_cleanup_frequency"
label="Docker cleanup frequency" required
helper="Cron expression for Docker Cleanup.<br>You can use every_minute, hourly, daily, weekly, monthly, yearly.<br><br>Default is every night at midnight." />
@else
<x-forms.input id="server.settings.docker_cleanup_threshold" label="Docker cleanup threshold (%)"
required
helper="The Docker cleanup tasks will run when the disk usage exceeds this threshold." />
@endif
<div class="w-96">
<x-forms.checkbox
helper="Enabling Force Docker Cleanup or manually triggering a cleanup will perform the following actions:
<ul class='list-disc pl-4 mt-2'>
<li>Removes stopped containers manged by Coolify (as containers are none persistent, no data will be lost).</li>
<li>Deletes unused images.</li>
<li>Clears build cache.</li>
<li>Removes old versions of the Coolify helper image.</li>
<li>Optionally delete unused volumes (if enabled in advanced options).</li>
<li>Optionally remove unused networks (if enabled in advanced options).</li>
</ul>"
instantSave id="server.settings.force_docker_cleanup" label="Force Docker Cleanup" />
</div>
</div>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-2">
<span class="dark:text-warning font-bold">Warning: Enable these
options only if you fully understand their implications and
consequences!</span><br>Improper use will result in data loss and could cause
functional issues.
</p>
<div class="w-96">
<x-forms.checkbox instantSave id="server.settings.delete_unused_volumes" label="Delete Unused Volumes"
helper="This option will remove all unused Docker volumes during cleanup.<br><br><strong>Warning: Data form stopped containers will be lost!</strong><br><br>Consequences include:<br>
<ul class='list-disc pl-4 mt-2'>
<li>Volumes not attached to running containers will be deleted and data will be permanently lost (stopped containers are affected).</li>
<li>Data from stopped containers volumes will be permanently lost.</li>
<li>No way to recover deleted volume data.</li>
</ul>" />
<x-forms.checkbox instantSave id="server.settings.delete_unused_networks" label="Delete Unused Networks"
helper="This option will remove all unused Docker networks during cleanup.<br><br><strong>Warning: Functionality may be lost and containers may not be able to communicate with each other!</strong><br><br>Consequences include:<br>
<ul class='list-disc pl-4 mt-2'>
<li>Networks not attached to running containers will be permanently deleted (stopped containers are affected).</li>
<li>Custom networks for stopped containers will be permanently deleted.</li>
<li>Functionality may be lost and containers may not be able to communicate with each other.</li>
</ul>" />
</div>
</div>
<div class="flex flex-col">
<h3>Builds</h3>
<div>Customize the build process.</div>
<div class="flex flex-wrap gap-2 sm:flex-nowrap pt-4">
<x-forms.input id="server.settings.concurrent_builds" label="Number of concurrent builds" required
helper="You can specify the number of simultaneous build processes/deployments that should run concurrently." />
<x-forms.input id="server.settings.dynamic_timeout" label="Deployment timeout (seconds)" required
helper="You can define the maximum duration for a deployment to run before timing it out." />
</div>
</div>
</div>
</form>

View File

@@ -0,0 +1,42 @@
<div>
<div class="flex gap-1 items-center">
<h2>Cloudflare Tunnels</h2>
<x-helper class="inline-flex"
helper="If you are using Cloudflare Tunnels, enable this. It will proxy all SSH requests to your server through Cloudflare.<br> You then can close your server's SSH port in the firewall of your hosting provider.<br><span class='dark:text-warning'>If you choose manual configuration, Coolify does not install or set up Cloudflare (cloudflared) on your server.</span>" />
</div>
<div class="flex flex-col gap-2 pt-6">
@if ($server->settings->is_cloudflare_tunnel)
<div class="w-64">
<x-forms.checkbox instantSave id="server.settings.is_cloudflare_tunnel"
label="Enabled" />
</div>
@elseif (!$server->isFunctional())
<div
class="p-4 mb-4 w-full text-sm text-yellow-800 bg-yellow-100 rounded dark:bg-yellow-900 dark:text-yellow-300">
To <span class="font-semibold">automatically</span> configure Cloudflare Tunnels, please
validate your server first.</span> Then you will need a Cloudflare token and an SSH
domain configured.
<br />
To <span class="font-semibold">manually</span> configure Cloudflare Tunnels, please
click <span wire:click="manualCloudflareConfig"
class="underline cursor-pointer">here</span>, then you should validate the server.
<br /><br />
For more information, please read our <a
href="https://coolify.io/docs/knowledge-base/cloudflare/tunnels/" target="_blank"
class="font-medium underline hover:text-yellow-600 dark:hover:text-yellow-200">documentation</a>.
</div>
@endif
@if (!$server->settings->is_cloudflare_tunnel && $server->isFunctional())
<x-modal-input buttonTitle="Automated Configuration" title="Cloudflare Tunnels"
class="w-full" :closeOutside="false">
<livewire:server.configure-cloudflare-tunnels :server_id="$server->id" />
</x-modal-input>
@endif
@if ($server->isFunctional() && !$server->settings->is_cloudflare_tunnel)
<div wire:click="manualCloudflareConfig" class="w-full underline cursor-pointer">
I have configured Cloudflare Tunnels manually
</div>
@endif
</div>
</div>

View File

@@ -1,6 +1,6 @@
<div> <div>
@if ($server->id !== 0) @if ($server->id !== 0)
<h2 class="pt-4">Danger Zone</h2> <h2>Danger Zone</h2>
<div class="">Woah. I hope you know what are you doing.</div> <div class="">Woah. I hope you know what are you doing.</div>
<h4 class="pt-4">Delete Server</h4> <h4 class="pt-4">Delete Server</h4>
<div class="pb-4">This will remove this server from Coolify. Beware! There is no coming <div class="pb-4">This will remove this server from Coolify. Beware! There is no coming

View File

@@ -2,6 +2,6 @@
<x-slot:title> <x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > Server Destinations | Coolify {{ data_get_str($server, 'name')->limit(10) }} > Server Destinations | Coolify
</x-slot> </x-slot>
<x-server.navbar :server="$server" :parameters="$parameters" /> {{-- <x-server.navbar :server="$server" :parameters="$parameters" /> --}}
<livewire:destination.show :server="$server" /> <livewire:destination.show :server="$server" />
</div> </div>

View File

@@ -119,55 +119,19 @@
</div> </div>
</div> </div>
<div class="{{ $server->isFunctional() ? 'w-96' : 'w-full' }}"> <div class="w-full">
@if (!$server->isLocalhost()) @if (!$server->isLocalhost())
<div class="w-96">
<x-forms.checkbox instantSave id="server.settings.is_build_server" <x-forms.checkbox instantSave id="server.settings.is_build_server"
label="Use it as a build server?" /> label="Use it as a build server?" />
<div class="flex flex-col gap-2 pt-6">
<div class="flex gap-1 items-center">
<h3 class="text-lg font-semibold">Cloudflare Tunnels</h3>
<x-helper class="inline-flex"
helper="If you are using Cloudflare Tunnels, enable this. It will proxy all SSH requests to your server through Cloudflare.<br> You then can close your server's SSH port in the firewall of your hosting provider.<br><span class='dark:text-warning'>If you choose manual configuration, Coolify does not install or set up Cloudflare (cloudflared) on your server.</span>" />
</div> </div>
@if ($server->settings->is_cloudflare_tunnel)
<div class="w-64">
<x-forms.checkbox instantSave id="server.settings.is_cloudflare_tunnel"
label="Enabled" />
</div>
@elseif (!$server->isFunctional())
<div
class="p-4 mb-4 w-full text-sm text-yellow-800 bg-yellow-100 rounded dark:bg-yellow-900 dark:text-yellow-300">
To <span class="font-semibold">automatically</span> configure Cloudflare Tunnels, please
validate your server first.</span> Then you will need a Cloudflare token and an SSH
domain configured.
<br />
To <span class="font-semibold">manually</span> configure Cloudflare Tunnels, please
click <span wire:click="manualCloudflareConfig"
class="underline cursor-pointer">here</span>, then you should validate the server.
<br /><br />
For more information, please read our <a
href="https://coolify.io/docs/knowledge-base/cloudflare/tunnels/" target="_blank"
class="font-medium underline hover:text-yellow-600 dark:hover:text-yellow-200">documentation</a>.
</div>
@endif
@if (!$server->settings->is_cloudflare_tunnel && $server->isFunctional())
<x-modal-input buttonTitle="Automated Configuration" title="Cloudflare Tunnels"
class="w-full" :closeOutside="false">
<livewire:server.configure-cloudflare-tunnels :server_id="$server->id" />
</x-modal-input>
@endif
@if ($server->isFunctional() && !$server->settings->is_cloudflare_tunnel)
<div wire:click="manualCloudflareConfig" class="w-full underline cursor-pointer">
I have configured Cloudflare Tunnels manually
</div>
@endif
</div>
@if (!$server->isBuildServer() && !$server->settings->is_cloudflare_tunnel) @if (!$server->isBuildServer() && !$server->settings->is_cloudflare_tunnel)
<h3 class="pt-6">Swarm <span class="text-xs text-neutral-500">(experimental)</span></h3> <h3 class="pt-6">Swarm <span class="text-xs text-neutral-500">(experimental)</span></h3>
<div class="pb-4">Read the docs <a class='underline dark:text-white' <div class="pb-4">Read the docs <a class='underline dark:text-white'
href='https://coolify.io/docs/knowledge-base/docker/swarm' target='_blank'>here</a>. href='https://coolify.io/docs/knowledge-base/docker/swarm' target='_blank'>here</a>.
</div> </div>
<div class="w-96">
@if ($server->settings->is_swarm_worker) @if ($server->settings->is_swarm_worker)
<x-forms.checkbox disabled instantSave type="checkbox" <x-forms.checkbox disabled instantSave type="checkbox"
id="server.settings.is_swarm_manager" id="server.settings.is_swarm_manager"
@@ -189,92 +153,11 @@
helper="For more information, please read the documentation <a class='dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/swarm' target='_blank'>here</a>." helper="For more information, please read the documentation <a class='dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/swarm' target='_blank'>here</a>."
label="Is it a Swarm Worker?" /> label="Is it a Swarm Worker?" />
@endif @endif
</div>
@endif @endif
@endif @endif
</div> </div>
</div> </div>
@if ($server->isFunctional())
<h3 class="pt-4">Settings</h3>
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-2">
<div class="flex flex-wrap items-center gap-4">
<div class="w-64">
<x-forms.checkbox
helper="Enabling Force Docker Cleanup or manually triggering a cleanup will perform the following actions:
<ul class='list-disc pl-4 mt-2'>
<li>Removes stopped containers manged by Coolify (as containers are none persistent, no data will be lost).</li>
<li>Deletes unused images.</li>
<li>Clears build cache.</li>
<li>Removes old versions of the Coolify helper image.</li>
<li>Optionally delete unused volumes (if enabled in advanced options).</li>
<li>Optionally remove unused networks (if enabled in advanced options).</li>
</ul>"
instantSave id="server.settings.force_docker_cleanup" label="Force Docker Cleanup" />
</div>
<x-modal-confirmation title="Confirm Docker Cleanup?" buttonTitle="Trigger Docker Cleanup"
submitAction="manualCleanup" :actions="[
'Permanently deletes all stopped containers managed by Coolify (as containers are non-persistent, no data will be lost)',
'Permanently deletes all unused images',
'Clears build cache',
'Removes old versions of the Coolify helper image',
'Optionally permanently deletes all unused volumes (if enabled in advanced options).',
'Optionally permanently deletes all unused networks (if enabled in advanced options).',
]" :confirmWithText="false" :confirmWithPassword="false"
step2ButtonText="Trigger Docker Cleanup" />
</div>
@if ($server->settings->force_docker_cleanup)
<x-forms.input placeholder="*/10 * * * *" id="server.settings.docker_cleanup_frequency"
label="Docker cleanup frequency" required
helper="Cron expression for Docker Cleanup.<br>You can use every_minute, hourly, daily, weekly, monthly, yearly.<br><br>Default is every night at midnight." />
@else
<x-forms.input id="server.settings.docker_cleanup_threshold"
label="Docker cleanup threshold (%)" required
helper="The Docker cleanup tasks will run when the disk usage exceeds this threshold." />
@endif
<div x-data="{ open: false }" class="mt-4 max-w-md">
<button @click="open = !open" type="button"
class="flex items-center justify-between w-full text-left text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100">
<span>Advanced Options</span>
<svg :class="{ 'rotate-180': open }" class="w-5 h-5 transition-transform duration-200"
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clip-rule="evenodd" />
</svg>
</button>
<div x-show="open" class="mt-2 space-y-2">
<p class="text-sm text-gray-600 dark:text-gray-400 mb-2"><strong>Warning: Enable these
options only if you fully understand their implications and
consequences!</strong><br>Improper use will result in data loss and could cause
functional issues.</p>
<x-forms.checkbox instantSave id="server.settings.delete_unused_volumes"
label="Delete Unused Volumes"
helper="This option will remove all unused Docker volumes during cleanup.<br><br><strong>Warning: Data form stopped containers will be lost!</strong><br><br>Consequences include:<br>
<ul class='list-disc pl-4 mt-2'>
<li>Volumes not attached to running containers will be deleted and data will be permanently lost (stopped containers are affected).</li>
<li>Data from stopped containers volumes will be permanently lost.</li>
<li>No way to recover deleted volume data.</li>
</ul>" />
<x-forms.checkbox instantSave id="server.settings.delete_unused_networks"
label="Delete Unused Networks"
helper="This option will remove all unused Docker networks during cleanup.<br><br><strong>Warning: Functionality may be lost and containers may not be able to communicate with each other!</strong><br><br>Consequences include:<br>
<ul class='list-disc pl-4 mt-2'>
<li>Networks not attached to running containers will be permanently deleted (stopped containers are affected).</li>
<li>Custom networks for stopped containers will be permanently deleted.</li>
<li>Functionality may be lost and containers may not be able to communicate with each other.</li>
</ul>" />
</div>
</div>
</div>
<div class="flex flex-wrap gap-4 sm:flex-nowrap">
<x-forms.input id="server.settings.concurrent_builds" label="Number of concurrent builds" required
helper="You can specify the number of simultaneous build processes/deployments that should run concurrently." />
<x-forms.input id="server.settings.dynamic_timeout" label="Deployment timeout (seconds)" required
helper="You can define the maximum duration for a deployment to run before timing it out." />
</div>
</div>
@if (isDev()) @if (isDev())
<div class="flex gap-2 items-center pt-4 pb-2"> <div class="flex gap-2 items-center pt-4 pb-2">
<h3>Sentinel</h3> <h3>Sentinel</h3>
@@ -283,17 +166,17 @@
wire:poll.{{ $server->settings->sentinel_push_interval_seconds }}s="checkSyncStatus"> wire:poll.{{ $server->settings->sentinel_push_interval_seconds }}s="checkSyncStatus">
@if ($server->isSentinelLive()) @if ($server->isSentinelLive())
<x-status.running status="In-sync" noLoading /> <x-status.running status="In-sync" noLoading />
<x-forms.button wire:click='restartSentinel'>Restart</x-forms.button>
@else @else
<x-status.stopped status="Out-of-sync" noLoading /> <x-status.stopped status="Out-of-sync" noLoading />
<x-forms.button wire:click='restartSentinel'>Sync</x-forms.button>
@endif @endif
<x-forms.button wire:click='restartSentinel'>Restart</x-forms.button>
</div> </div>
@endif @endif
</div> </div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div class="w-64"> <div class="w-64">
<x-forms.checkbox instantSave id="server.settings.is_sentinel_enabled" <x-forms.checkbox instantSave id="server.settings.is_sentinel_enabled" label="Enable Sentinel" />
label="Enable Sentinel" />
@if ($server->isSentinelEnabled()) @if ($server->isSentinelEnabled())
<x-forms.checkbox instantSave id="server.settings.is_metrics_enabled" <x-forms.checkbox instantSave id="server.settings.is_metrics_enabled"
label="Enable Metrics" /> label="Enable Metrics" />
@@ -302,14 +185,16 @@
label="Enable Metrics" /> label="Enable Metrics" />
@endif @endif
</div> </div>
@if ($server->isSentinelEnabled())
<div class="flex flex-wrap gap-2 sm:flex-nowrap items-end"> <div class="flex flex-wrap gap-2 sm:flex-nowrap items-end">
<x-forms.input type="password" id="server.settings.sentinel_token" label="Sentinel token" <x-forms.input type="password" id="server.settings.sentinel_token" label="Sentinel token"
required helper="Token for Sentinel." /> required helper="Token for Sentinel." />
<x-forms.input id="server.settings.sentinel_custom_url" label="Sentinel custom URL"
helper="Custom URL for Sentinel." />
<x-forms.button wire:click="regenerateSentinelToken">Regenerate</x-forms.button> <x-forms.button wire:click="regenerateSentinelToken">Regenerate</x-forms.button>
</div> </div>
<x-forms.input id="server.settings.sentinel_custom_url" required label="Coolify URL"
helper="URL to your Coolify instance. If it is empty that means you do not have a FQDN set for your Coolify instance." />
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div class="flex flex-wrap gap-2 sm:flex-nowrap"> <div class="flex flex-wrap gap-2 sm:flex-nowrap">
<x-forms.input id="server.settings.sentinel_metrics_refresh_rate_seconds" <x-forms.input id="server.settings.sentinel_metrics_refresh_rate_seconds"
@@ -323,8 +208,9 @@
helper="How many seconds should the metrics data should be pushed to the collector." /> helper="How many seconds should the metrics data should be pushed to the collector." />
</div> </div>
</div> </div>
@endif
</div> </div>
@endif @endif
@endif
</form> </form>
</div> </div>

View File

@@ -2,7 +2,7 @@
<x-slot:title> <x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > Server LogDrains | Coolify {{ data_get_str($server, 'name')->limit(10) }} > Server LogDrains | Coolify
</x-slot> </x-slot>
<x-server.navbar :server="$server" :parameters="$parameters" /> {{-- <x-server.navbar :server="$server" :parameters="$parameters" /> --}}
@if ($server->isFunctional()) @if ($server->isFunctional())
<h2>Log Drains</h2> <h2>Log Drains</h2>
<div class="pb-4">Sends service logs to 3rd party tools.</div> <div class="pb-4">Sends service logs to 3rd party tools.</div>

View File

@@ -2,6 +2,5 @@
<x-slot:title> <x-slot:title>
Server Connection | Coolify Server Connection | Coolify
</x-slot> </x-slot>
<x-server.navbar :server="$server" :parameters="$parameters" />
<livewire:server.show-private-key :server="$server" :privateKeys="$privateKeys" /> <livewire:server.show-private-key :server="$server" :privateKeys="$privateKeys" />
</div> </div>

View File

@@ -1,12 +0,0 @@
<div>
<x-modal submitWireAction="proxyStatusUpdated" modalId="startProxy">
<x-slot:modalBody>
<livewire:activity-monitor header="Proxy Startup Logs" />
</x-slot:modalBody>
<x-slot:modalSubmit>
<x-forms.button onclick="startProxy.close()" type="submit">
Close
</x-forms.button>
</x-slot:modalSubmit>
</x-modal>
</div>

View File

@@ -2,24 +2,30 @@
<x-slot:title> <x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > Server Resources | Coolify {{ data_get_str($server, 'name')->limit(10) }} > Server Resources | Coolify
</x-slot> </x-slot>
<x-server.navbar :server="$server" :parameters="$parameters" /> {{-- <x-server.navbar :server="$server" :parameters="$parameters" /> --}}
<div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'managed' }" class="flex flex-col h-full gap-8 md:flex-row"> <div x-data="{ activeTab: 'managed' }" class="flex flex-col h-full gap-8 md:flex-row">
<div class="flex flex-row gap-4 md:flex-col">
<a :class="activeTab === 'managed' && 'dark:text-white'"
@click.prevent="activeTab = 'managed'; window.location.hash = 'managed'" href="#">Managed</a>
<a :class="activeTab === 'unmanaged' && 'dark:text-white'"
@click.prevent="activeTab = 'unmanaged'; window.location.hash = 'unmanaged'" href="#">Unmanaged</a>
</div>
<div class="w-full"> <div class="w-full">
<div x-cloak x-show="activeTab === 'managed'" class="h-full">
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex gap-2"> <div class="flex gap-2">
<h2>Resources</h2> <h2>Resources</h2>
<x-forms.button wire:click="refreshStatus">Refresh</x-forms.button> <x-forms.button wire:click="refreshStatus">Refresh</x-forms.button>
</div> </div>
<div class="subtitle">Here you can find all resources that are managed by Coolify.</div> <div>Here you can find all resources that are managed by Coolify.</div>
<div class="flex flex-row gap-4 py-10">
<div @class([
'box-without-bg cursor-pointer bg-coolgray-100 text-white w-full text-center items-center justify-center',
'bg-coollabs' => $activeTab === 'managed',
]) wire:click="loadManagedContainers">
Managed</div>
<div @class([
'box-without-bg cursor-pointer bg-coolgray-100 text-white w-full text-center items-center justify-center',
'bg-coollabs' => $activeTab === 'unmanaged',
]) wire:click="loadUnmanagedContainers">
Unmanaged</div>
</div> </div>
@if ($server->definedResources()->count() > 0) </div>
@if ($containers->count() > 0)
@if ($activeTab === 'managed')
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex flex-col"> <div class="flex flex-col">
<div class="overflow-x-auto"> <div class="overflow-x-auto">
@@ -78,19 +84,7 @@
</div> </div>
</div> </div>
</div> </div>
@else @elseif ($activeTab === 'unmanaged')
<div>No resources found.</div>
@endif
</div>
<div x-cloak x-show="activeTab === 'unmanaged'" class="h-full">
<div class="flex flex-col" x-init="$wire.loadUnmanagedContainers()">
<div class="flex gap-2">
<h2>Resources</h2>
<x-forms.button wire:click="refreshStatus">Refresh</x-forms.button>
</div>
<div class="subtitle">Here you can find all other containers running on the server.</div>
</div>
@if ($unmanagedContainers->count() > 0)
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex flex-col"> <div class="flex flex-col">
<div class="overflow-x-auto"> <div class="overflow-x-auto">
@@ -114,7 +108,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@forelse ($unmanagedContainers->sortBy('name',SORT_NATURAL) as $resource) @forelse ($containers->sortBy('name',SORT_NATURAL) as $resource)
<tr> <tr>
<td class="px-5 py-4 text-sm whitespace-nowrap"> <td class="px-5 py-4 text-sm whitespace-nowrap">
{{ data_get($resource, 'Names') }} {{ data_get($resource, 'Names') }}
@@ -152,11 +146,14 @@
</div> </div>
</div> </div>
</div> </div>
</div> @endif
@else @else
<div>No resources found.</div> @if ($activeTab === 'managed')
<div>No managed resources found.</div>
@elseif ($activeTab === 'unmanaged')
<div>No unmanaged resources found.</div>
@endif
@endif @endif
</div> </div>
</div> </div>
</div> </div>
</div>

View File

@@ -1,5 +1,5 @@
<div> <div>
<div class="flex items-end gap-2 pb-6 "> <div class="flex items-end gap-2">
<h2>Private Key</h2> <h2>Private Key</h2>
<x-modal-input buttonTitle="+ Add" title="New Private Key"> <x-modal-input buttonTitle="+ Add" title="New Private Key">
<livewire:security.private-key.create /> <livewire:security.private-key.create />
@@ -9,29 +9,25 @@
</x-forms.button> </x-forms.button>
</div> </div>
<div class="flex flex-col gap-2 pb-6"> <div class="flex flex-col gap-2">
@if (data_get($server, 'privateKey.uuid')) <div class="pb-4">Change your server's private key.</div>
<div>
Currently attached Private Key:
<a
href="{{ route('security.private-key.show', ['private_key_uuid' => data_get($server, 'privateKey.uuid')]) }}">
<button class="dark:text-white btn-link">{{ data_get($server, 'privateKey.name') }}</button>
</a>
</div> </div>
@else <div class="grid xl:grid-cols-2 grid-cols-1 gap-2">
<div class="">No private key attached.</div>
@endif
</div>
<h3 class="pb-4">Choose another Key</h3>
<div class="grid grid-cols-3 gap-2">
@forelse ($privateKeys as $private_key) @forelse ($privateKeys as $private_key)
<div class="box group cursor-pointer" <div class="box-without-bg justify-between dark:bg-coolgray-100 bg-white items-center">
wire:click='setPrivateKey({{ $private_key->id }})'>
<div class="flex flex-col "> <div class="flex flex-col ">
<div class="box-title">{{ $private_key->name }}</div> <div class="box-title">{{ $private_key->name }}</div>
<div class="box-description">{{ $private_key->description }}</div> <div class="box-description">{{ $private_key->description }}</div>
</div> </div>
@if (data_get($server, 'privateKey.uuid') !== $private_key->uuid)
<x-forms.button wire:click='setPrivateKey({{ $private_key->id }})'>
Use this key
</x-forms.button>
@else
<x-forms.button disabled>
Currently used
</x-forms.button>
@endif
</div> </div>
@empty @empty
<div>No private keys found. </div> <div>No private keys found. </div>

View File

@@ -3,11 +3,75 @@
{{ data_get_str($server, 'name')->limit(10) }} > Server Configurations | Coolify {{ data_get_str($server, 'name')->limit(10) }} > Server Configurations | Coolify
</x-slot> </x-slot>
<x-server.navbar :server="$server" :parameters="$parameters" /> <x-server.navbar :server="$server" :parameters="$parameters" />
<div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'general' }" class="flex flex-col h-full gap-8 sm:flex-row">
<div class="flex flex-col items-start gap-2 min-w-fit">
<a class="menu-item" :class="activeTab === 'general' && 'menu-item-active'"
@click.prevent="activeTab = 'general'; window.location.hash = 'general'" href="#">General</a>
@if ($server->isFunctional())
<a class="menu-item" :class="activeTab === 'advanced' && 'menu-item-active'"
@click.prevent="activeTab = 'advanced'; window.location.hash = 'advanced'" href="#">Advanced
</a>
@endif
<a class="menu-item" :class="activeTab === 'private-key' && 'menu-item-active'"
@click.prevent="activeTab = 'private-key'; window.location.hash = 'private-key'" href="#">Private
Key</a>
@if ($server->isFunctional())
<a class="menu-item" :class="activeTab === 'cloudflare-tunnels' && 'menu-item-active'"
@click.prevent="activeTab = 'cloudflare-tunnels'; window.location.hash = 'cloudflare-tunnels'"
href="#">Cloudflare Tunnels</a>
<a class="menu-item" :class="activeTab === 'resources' && 'menu-item-active'"
@click.prevent="activeTab = 'resources'; window.location.hash = 'resources'"
href="#">Resources</a>
<a class="menu-item" :class="activeTab === 'destinations' && 'menu-item-active'"
@click.prevent="activeTab = 'destinations'; window.location.hash = 'destinations'"
href="#">Destinations</a>
<a class="menu-item" :class="activeTab === 'log-drains' && 'menu-item-active'"
@click.prevent="activeTab = 'log-drains'; window.location.hash = 'log-drains'" href="#">Log
Drains</a>
<a class="menu-item" :class="activeTab === 'metrics' && 'menu-item-active'"
@click.prevent="activeTab = 'metrics'; window.location.hash = 'metrics'" href="#">Metrics</a>
@endif
@if (!$server->isLocalhost())
<a class="menu-item" :class="activeTab === 'danger' && 'menu-item-active'"
@click.prevent="activeTab = 'danger'; window.location.hash = 'danger'" href="#">Danger</a>
@endif
</div>
<div class="w-full">
<div x-cloak x-show="activeTab === 'general'" class="h-full">
<livewire:server.form :server="$server" /> <livewire:server.form :server="$server" />
</div>
<div x-cloak x-show="activeTab === 'advanced'" class="h-full">
<livewire:server.advanced :server="$server" />
</div>
<div x-cloak x-show="activeTab === 'private-key'" class="h-full">
<livewire:server.private-key.show :server="$server" />
</div>
<div x-cloak x-show="activeTab === 'cloudflare-tunnels'" class="h-full">
<livewire:server.cloudflare-tunnels :server="$server" />
</div>
<div x-cloak x-show="activeTab === 'resources'" class="h-full">
<livewire:server.resources :server="$server" />
</div>
<div x-cloak x-show="activeTab === 'destinations'" class="h-full">
<livewire:server.destination.show :server="$server" />
</div>
<div x-cloak x-show="activeTab === 'log-drains'" class="h-full">
<livewire:server.log-drains :server="$server" />
</div>
<div x-cloak x-show="activeTab === 'metrics'" class="h-full">
@if ($server->isFunctional() && $server->isMetricsEnabled()) @if ($server->isFunctional() && $server->isMetricsEnabled())
<div class="pt-10"> <div class="pt-10">
<livewire:server.charts :server="$server" /> <livewire:server.charts :server="$server" />
</div> </div>
@else
No metrics available.
@endif @endif
</div>
@if (!$server->isLocalhost())
<div x-cloak x-show="activeTab === 'danger'" class="h-full">
<livewire:server.delete :server="$server" /> <livewire:server.delete :server="$server" />
</div> </div>
@endif
</div>
</div>
</div>