refactor server view (phuuu)

This commit is contained in:
Andras Bacsai
2024-10-30 14:54:27 +01:00
parent ee79faf542
commit 96ca72fcdb
38 changed files with 1365 additions and 893 deletions

View File

@@ -5,7 +5,7 @@ namespace App\Actions\Server;
use App\Models\Server; use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
class InstallLogDrain class StartLogDrain
{ {
use AsAction; use AsAction;
@@ -13,12 +13,16 @@ class InstallLogDrain
{ {
if ($server->settings->is_logdrain_newrelic_enabled) { if ($server->settings->is_logdrain_newrelic_enabled) {
$type = 'newrelic'; $type = 'newrelic';
StopLogDrain::run($server);
} elseif ($server->settings->is_logdrain_highlight_enabled) { } elseif ($server->settings->is_logdrain_highlight_enabled) {
$type = 'highlight'; $type = 'highlight';
StopLogDrain::run($server);
} elseif ($server->settings->is_logdrain_axiom_enabled) { } elseif ($server->settings->is_logdrain_axiom_enabled) {
$type = 'axiom'; $type = 'axiom';
StopLogDrain::run($server);
} elseif ($server->settings->is_logdrain_custom_enabled) { } elseif ($server->settings->is_logdrain_custom_enabled) {
$type = 'custom'; $type = 'custom';
StopLogDrain::run($server);
} else { } else {
$type = 'none'; $type = 'none';
} }
@@ -151,6 +155,8 @@ services:
- ./parsers.conf:/parsers.conf - ./parsers.conf:/parsers.conf
ports: ports:
- 127.0.0.1:24224:24224 - 127.0.0.1:24224:24224
labels:
- coolify.managed=true
restart: unless-stopped restart: unless-stopped
'); ');
$readme = base64_encode('# New Relic Log Drain $readme = base64_encode('# New Relic Log Drain
@@ -202,15 +208,11 @@ Files:
throw new \Exception('Unknown log drain type.'); throw new \Exception('Unknown log drain type.');
} }
$restart_command = [ $restart_command = [
"echo 'Stopping old Fluent Bit'",
"cd $config_path && docker compose down --remove-orphans || true",
"echo 'Starting Fluent Bit'", "echo 'Starting Fluent Bit'",
"cd $config_path && docker compose up -d --remove-orphans", "cd $config_path && docker compose up -d",
]; ];
$command = array_merge($command, $add_envs_command, $restart_command); $command = array_merge($command, $add_envs_command, $restart_command);
StopLogDrain::run($server);
return instant_remote_process($command, $server); return instant_remote_process($command, $server);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e); return handleError($e);

View File

@@ -12,7 +12,7 @@ class StopLogDrain
public function handle(Server $server) public function handle(Server $server)
{ {
try { try {
return instant_remote_process(['docker rm -f coolify-log-drain || true'], $server); return instant_remote_process(['docker rm -f coolify-log-drain'], $server, false);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e); return handleError($e);
} }

View File

@@ -6,7 +6,7 @@ use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy; use App\Actions\Database\StopDatabaseProxy;
use App\Actions\Proxy\CheckProxy; use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy; use App\Actions\Proxy\StartProxy;
use App\Actions\Server\InstallLogDrain; use App\Actions\Server\StartLogDrain;
use App\Actions\Shared\ComplexStatusCheck; use App\Actions\Shared\ComplexStatusCheck;
use App\Models\Application; use App\Models\Application;
use App\Models\ApplicationPreview; use App\Models\ApplicationPreview;
@@ -362,7 +362,7 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
private function checkLogDrainContainer() private function checkLogDrainContainer()
{ {
if ($this->server->isLogDrainEnabled() && $this->foundLogDrainContainer === false) { if ($this->server->isLogDrainEnabled() && $this->foundLogDrainContainer === false) {
InstallLogDrain::dispatch($this->server); StartLogDrain::dispatch($this->server);
} }
} }
} }

View File

@@ -5,7 +5,7 @@ namespace App\Jobs;
use App\Actions\Docker\GetContainersStatus; use App\Actions\Docker\GetContainersStatus;
use App\Actions\Proxy\CheckProxy; use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy; use App\Actions\Proxy\StartProxy;
use App\Actions\Server\InstallLogDrain; use App\Actions\Server\StartLogDrain;
use App\Models\Server; use App\Models\Server;
use App\Notifications\Container\ContainerRestarted; use App\Notifications\Container\ContainerRestarted;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
@@ -109,10 +109,10 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
if ($foundLogDrainContainer) { if ($foundLogDrainContainer) {
$status = data_get($foundLogDrainContainer, 'State.Status'); $status = data_get($foundLogDrainContainer, 'State.Status');
if ($status !== 'running') { if ($status !== 'running') {
InstallLogDrain::dispatch($this->server); StartLogDrain::dispatch($this->server);
} }
} else { } else {
InstallLogDrain::dispatch($this->server); StartLogDrain::dispatch($this->server);
} }
} }
} }

View File

@@ -38,7 +38,6 @@ class ServerStorageCheckJob implements ShouldBeEncrypted, ShouldQueue
if (is_null($this->percentage)) { if (is_null($this->percentage)) {
$this->percentage = $this->server->storageCheck(); $this->percentage = $this->server->storageCheck();
loggy('Server storage check percentage: '.$this->percentage);
} }
if (! $this->percentage) { if (! $this->percentage) {
return 'No percentage could be retrieved.'; return 'No percentage could be retrieved.';

View File

@@ -4,45 +4,82 @@ namespace App\Livewire\Server;
use App\Jobs\DockerCleanupJob; use App\Jobs\DockerCleanupJob;
use App\Models\Server; use App\Models\Server;
use Livewire\Attributes\Rule;
use Livewire\Component; use Livewire\Component;
class Advanced extends Component class Advanced extends Component
{ {
public Server $server; public Server $server;
protected $rules = [ public array $parameters = [];
'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.server_disk_usage_notification_threshold' => 'required|integer|min:50|max:100',
'server.settings.delete_unused_volumes' => 'boolean',
'server.settings.delete_unused_networks' => 'boolean',
];
protected $validationAttributes = [ #[Rule(['integer', 'min:1'])]
public int $concurrentBuilds = 1;
'server.settings.concurrent_builds' => 'Concurrent Builds', #[Rule(['integer', 'min:1'])]
'server.settings.dynamic_timeout' => 'Dynamic Timeout', public int $dynamicTimeout = 1;
'server.settings.force_docker_cleanup' => 'Force Docker Cleanup',
'server.settings.docker_cleanup_frequency' => 'Docker Cleanup Frequency', #[Rule('boolean')]
'server.settings.docker_cleanup_threshold' => 'Docker Cleanup Threshold', public bool $forceDockerCleanup = false;
'server.settings.server_disk_usage_notification_threshold' => 'Server Disk Usage Notification Threshold',
'server.settings.delete_unused_volumes' => 'Delete Unused Volumes', #[Rule('string')]
'server.settings.delete_unused_networks' => 'Delete Unused Networks', public string $dockerCleanupFrequency = '*/10 * * * *';
];
#[Rule(['integer', 'min:1', 'max:99'])]
public int $dockerCleanupThreshold = 10;
#[Rule(['integer', 'min:1', 'max:99'])]
public int $serverDiskUsageNotificationThreshold = 50;
#[Rule('boolean')]
public bool $deleteUnusedVolumes = false;
#[Rule('boolean')]
public bool $deleteUnusedNetworks = false;
public function mount(string $server_uuid)
{
try {
$this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail();
$this->parameters = get_route_parameters();
$this->syncData();
} catch (\Throwable $e) {
return redirect()->route('server.show');
}
}
public function syncData(bool $toModel = false)
{
if ($toModel) {
$this->validate();
$this->server->settings->concurrent_builds = $this->concurrentBuilds;
$this->server->settings->dynamic_timeout = $this->dynamicTimeout;
$this->server->settings->force_docker_cleanup = $this->forceDockerCleanup;
$this->server->settings->docker_cleanup_frequency = $this->dockerCleanupFrequency;
$this->server->settings->docker_cleanup_threshold = $this->dockerCleanupThreshold;
$this->server->settings->server_disk_usage_notification_threshold = $this->serverDiskUsageNotificationThreshold;
$this->server->settings->delete_unused_volumes = $this->deleteUnusedVolumes;
$this->server->settings->delete_unused_networks = $this->deleteUnusedNetworks;
$this->server->settings->save();
} else {
$this->concurrentBuilds = $this->server->settings->concurrent_builds;
$this->dynamicTimeout = $this->server->settings->dynamic_timeout;
$this->forceDockerCleanup = $this->server->settings->force_docker_cleanup;
$this->dockerCleanupFrequency = $this->server->settings->docker_cleanup_frequency;
$this->dockerCleanupThreshold = $this->server->settings->docker_cleanup_threshold;
$this->serverDiskUsageNotificationThreshold = $this->server->settings->server_disk_usage_notification_threshold;
$this->deleteUnusedVolumes = $this->server->settings->delete_unused_volumes;
$this->deleteUnusedNetworks = $this->server->settings->delete_unused_networks;
}
}
public function instantSave() public function instantSave()
{ {
try { try {
$this->validate(); $this->syncData(true);
$this->server->settings->save();
$this->dispatch('success', 'Server updated.'); $this->dispatch('success', 'Server updated.');
$this->dispatch('refreshServerShow'); // $this->dispatch('refreshServerShow');
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->server->settings->refresh();
return handleError($e, $this); return handleError($e, $this);
} }
} }
@@ -60,12 +97,11 @@ class Advanced extends Component
public function submit() public function submit()
{ {
try { try {
$frequency = $this->server->settings->docker_cleanup_frequency; if (! validate_cron_expression($this->dockerCleanupFrequency)) {
if (empty($frequency) || ! validate_cron_expression($frequency)) { $this->dockerCleanupFrequency = $this->server->settings->getOriginal('docker_cleanup_frequency');
$this->server->settings->docker_cleanup_frequency = '*/10 * * * *'; throw new \Exception('Invalid Cron / Human expression for Docker Cleanup Frequency.');
throw new \Exception('Invalid Cron / Human expression for Docker Cleanup Frequency. Resetting to default 10 minutes.');
} }
$this->server->settings->save(); $this->syncData(true);
$this->dispatch('success', 'Server updated.'); $this->dispatch('success', 'Server updated.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);

View File

@@ -19,6 +19,15 @@ class Charts extends Component
public bool $poll = true; public bool $poll = true;
public function mount(string $server_uuid)
{
try {
$this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function pollData() public function pollData()
{ {
if ($this->poll || $this->interval <= 10) { if ($this->poll || $this->interval <= 10) {

View File

@@ -3,27 +3,33 @@
namespace App\Livewire\Server; namespace App\Livewire\Server;
use App\Models\Server; use App\Models\Server;
use Livewire\Attributes\Rule;
use Livewire\Component; use Livewire\Component;
class CloudflareTunnels extends Component class CloudflareTunnels extends Component
{ {
public Server $server; public Server $server;
protected $rules = [ #[Rule(['required', 'boolean'])]
'server.settings.is_cloudflare_tunnel' => 'required|boolean', public bool $isCloudflareTunnelsEnabled;
];
protected $validationAttributes = [ public function mount(string $server_uuid)
'server.settings.is_cloudflare_tunnel' => 'Cloudflare Tunnel', {
]; try {
$this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail();
$this->isCloudflareTunnelsEnabled = $this->server->settings->is_cloudflare_tunnel;
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function instantSave() public function instantSave()
{ {
try { try {
$this->validate(); $this->validate();
$this->server->settings->is_cloudflare_tunnel = $this->isCloudflareTunnelsEnabled;
$this->server->settings->save(); $this->server->settings->save();
$this->dispatch('success', 'Server updated.'); $this->dispatch('success', 'Server updated.');
$this->dispatch('refreshServerShow');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }
@@ -31,6 +37,7 @@ class CloudflareTunnels extends Component
public function manualCloudflareConfig() public function manualCloudflareConfig()
{ {
$this->isCloudflareTunnelsEnabled = true;
$this->server->settings->is_cloudflare_tunnel = true; $this->server->settings->is_cloudflare_tunnel = true;
$this->server->settings->save(); $this->server->settings->save();
$this->server->refresh(); $this->server->refresh();

View File

@@ -4,6 +4,7 @@ namespace App\Livewire\Server;
use App\Actions\Server\DeleteServer; use App\Actions\Server\DeleteServer;
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
use App\Models\Server;
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;
@@ -13,7 +14,16 @@ class Delete extends Component
{ {
use AuthorizesRequests; use AuthorizesRequests;
public $server; public Server $server;
public function mount(string $server_uuid)
{
try {
$this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function delete($password) public function delete($password)
{ {

View File

@@ -3,27 +3,87 @@
namespace App\Livewire\Server\Destination; namespace App\Livewire\Server\Destination;
use App\Models\Server; use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use Illuminate\Support\Collection;
use Livewire\Component; use Livewire\Component;
class Show extends Component class Show extends Component
{ {
public ?Server $server = null; public Server $server;
public $parameters = []; public Collection $networks;
public function mount() public function mount(string $server_uuid)
{ {
$this->parameters = get_route_parameters();
try { try {
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first(); $this->networks = collect();
if (is_null($this->server)) { $this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail();
return redirect()->route('server.index'); loggy($this->server);
}
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }
} }
private function createNetworkAndAttachToProxy()
{
$connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
}
public function add($name)
{
if ($this->server->isSwarm()) {
$found = $this->server->swarmDockers()->where('network', $name)->first();
if ($found) {
$this->dispatch('error', 'Network already added to this server.');
return;
} else {
SwarmDocker::create([
'name' => $this->server->name.'-'.$name,
'network' => $this->name,
'server_id' => $this->server->id,
]);
}
} else {
$found = $this->server->standaloneDockers()->where('network', $name)->first();
if ($found) {
$this->dispatch('error', 'Network already added to this server.');
return;
} else {
StandaloneDocker::create([
'name' => $this->server->name.'-'.$name,
'network' => $name,
'server_id' => $this->server->id,
]);
}
$this->createNetworkAndAttachToProxy();
}
}
public function scan()
{
if ($this->server->isSwarm()) {
$alreadyAddedNetworks = $this->server->swarmDockers;
} else {
$alreadyAddedNetworks = $this->server->standaloneDockers;
}
$networks = instant_remote_process(['docker network ls --format "{{json .}}"'], $this->server, false);
$this->networks = format_docker_command_output_to_json($networks)->filter(function ($network) {
return $network['Name'] !== 'bridge' && $network['Name'] !== 'host' && $network['Name'] !== 'none';
})->filter(function ($network) use ($alreadyAddedNetworks) {
return ! $alreadyAddedNetworks->contains('network', $network['Name']);
});
if ($this->networks->count() === 0) {
$this->dispatch('success', 'No new destinations found on this server.');
return;
}
$this->dispatch('success', 'Scan done.');
}
public function render() public function render()
{ {
return view('livewire.server.destination.show'); return view('livewire.server.destination.show');

View File

@@ -111,7 +111,7 @@ class Form extends Component
{ {
if ($field === 'server.settings.docker_cleanup_frequency') { if ($field === 'server.settings.docker_cleanup_frequency') {
$frequency = $this->server->settings->docker_cleanup_frequency; $frequency = $this->server->settings->docker_cleanup_frequency;
if (empty($frequency) || ! validate_cron_expression($frequency)) { if (! validate_cron_expression($frequency)) {
$this->dispatch('error', 'Invalid Cron / Human expression for Docker Cleanup Frequency. Resetting to default 10 minutes.'); $this->dispatch('error', 'Invalid Cron / Human expression for Docker Cleanup Frequency. Resetting to default 10 minutes.');
$this->server->settings->docker_cleanup_frequency = '*/10 * * * *'; $this->server->settings->docker_cleanup_frequency = '*/10 * * * *';
} }

View File

@@ -2,84 +2,96 @@
namespace App\Livewire\Server; namespace App\Livewire\Server;
use App\Actions\Server\InstallLogDrain; use App\Actions\Server\StartLogDrain;
use App\Actions\Server\StopLogDrain; use App\Actions\Server\StopLogDrain;
use App\Models\Server; use App\Models\Server;
use Livewire\Attributes\Rule;
use Livewire\Component; use Livewire\Component;
class LogDrains extends Component class LogDrains extends Component
{ {
public Server $server; public Server $server;
public $parameters = []; #[Rule(['boolean'])]
public bool $isLogDrainNewRelicEnabled = false;
protected $rules = [ #[Rule(['boolean'])]
'server.settings.is_logdrain_newrelic_enabled' => 'required|boolean', public bool $isLogDrainCustomEnabled = false;
'server.settings.logdrain_newrelic_license_key' => 'required|string',
'server.settings.logdrain_newrelic_base_uri' => 'required|string',
'server.settings.is_logdrain_highlight_enabled' => 'required|boolean',
'server.settings.logdrain_highlight_project_id' => 'required|string',
'server.settings.is_logdrain_axiom_enabled' => 'required|boolean',
'server.settings.logdrain_axiom_dataset_name' => 'required|string',
'server.settings.logdrain_axiom_api_key' => 'required|string',
'server.settings.is_logdrain_custom_enabled' => 'required|boolean',
'server.settings.logdrain_custom_config' => 'required|string',
'server.settings.logdrain_custom_config_parser' => 'nullable',
];
protected $validationAttributes = [ #[Rule(['boolean'])]
'server.settings.is_logdrain_newrelic_enabled' => 'New Relic log drain', public bool $isLogDrainAxiomEnabled = false;
'server.settings.logdrain_newrelic_license_key' => 'New Relic license key',
'server.settings.logdrain_newrelic_base_uri' => 'New Relic base URI',
'server.settings.is_logdrain_highlight_enabled' => 'Highlight log drain',
'server.settings.logdrain_highlight_project_id' => 'Highlight project ID',
'server.settings.is_logdrain_axiom_enabled' => 'Axiom log drain',
'server.settings.logdrain_axiom_dataset_name' => 'Axiom dataset name',
'server.settings.logdrain_axiom_api_key' => 'Axiom API key',
'server.settings.is_logdrain_custom_enabled' => 'Custom log drain',
'server.settings.logdrain_custom_config' => 'Custom log drain configuration',
'server.settings.logdrain_custom_config_parser' => 'Custom log drain configuration parser',
];
public function mount() #[Rule(['string', 'nullable'])]
public ?string $logDrainNewRelicLicenseKey = null;
#[Rule(['url', 'nullable'])]
public ?string $logDrainNewRelicBaseUri = null;
#[Rule(['string', 'nullable'])]
public ?string $logDrainAxiomDatasetName = null;
#[Rule(['string', 'nullable'])]
public ?string $logDrainAxiomApiKey = null;
#[Rule(['string', 'nullable'])]
public ?string $logDrainCustomConfig = null;
#[Rule(['string', 'nullable'])]
public ?string $logDrainCustomConfigParser = null;
public function mount(string $server_uuid)
{ {
$this->parameters = get_route_parameters();
try { try {
$server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first(); $this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail();
if (is_null($server)) { $this->syncData();
return redirect()->route('server.index');
}
$this->server = $server;
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }
} }
public function configureLogDrain() public function syncData(bool $toModel = false)
{
if ($toModel) {
$this->validate();
$this->server->settings->is_logdrain_newrelic_enabled = $this->isLogDrainNewRelicEnabled;
$this->server->settings->is_logdrain_axiom_enabled = $this->isLogDrainAxiomEnabled;
$this->server->settings->is_logdrain_custom_enabled = $this->isLogDrainCustomEnabled;
$this->server->settings->logdrain_newrelic_license_key = $this->logDrainNewRelicLicenseKey;
$this->server->settings->logdrain_newrelic_base_uri = $this->logDrainNewRelicBaseUri;
$this->server->settings->logdrain_axiom_dataset_name = $this->logDrainAxiomDatasetName;
$this->server->settings->logdrain_axiom_api_key = $this->logDrainAxiomApiKey;
$this->server->settings->logdrain_custom_config = $this->logDrainCustomConfig;
$this->server->settings->logdrain_custom_config_parser = $this->logDrainCustomConfigParser;
$this->server->settings->save();
} else {
$this->isLogDrainNewRelicEnabled = $this->server->settings->is_logdrain_newrelic_enabled;
$this->isLogDrainAxiomEnabled = $this->server->settings->is_logdrain_axiom_enabled;
$this->isLogDrainCustomEnabled = $this->server->settings->is_logdrain_custom_enabled;
$this->logDrainNewRelicLicenseKey = $this->server->settings->logdrain_newrelic_license_key;
$this->logDrainNewRelicBaseUri = $this->server->settings->logdrain_newrelic_base_uri;
$this->logDrainAxiomDatasetName = $this->server->settings->logdrain_axiom_dataset_name;
$this->logDrainAxiomApiKey = $this->server->settings->logdrain_axiom_api_key;
$this->logDrainCustomConfig = $this->server->settings->logdrain_custom_config;
$this->logDrainCustomConfigParser = $this->server->settings->logdrain_custom_config_parser;
}
}
public function instantSave()
{ {
try { try {
InstallLogDrain::run($this->server); $this->syncData(true);
if (! $this->server->isLogDrainEnabled()) { if ($this->server->isLogDrainEnabled()) {
$this->dispatch('serverRefresh'); StartLogDrain::run($this->server);
$this->dispatch('success', 'Log drain service started.');
} else {
StopLogDrain::run($this->server);
$this->dispatch('success', 'Log drain service stopped.'); $this->dispatch('success', 'Log drain service stopped.');
return;
} }
$this->dispatch('serverRefresh');
$this->dispatch('success', 'Log drain service started.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function instantSave(string $type)
{
try {
$ok = $this->submit($type);
if (! $ok) {
return;
}
$this->configureLogDrain();
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }
@@ -88,79 +100,10 @@ class LogDrains extends Component
public function submit(string $type) public function submit(string $type)
{ {
try { try {
$this->resetErrorBag(); $this->syncData(true);
if ($type === 'newrelic') {
$this->validate([
'server.settings.is_logdrain_newrelic_enabled' => 'required|boolean',
'server.settings.logdrain_newrelic_license_key' => 'required|string',
'server.settings.logdrain_newrelic_base_uri' => 'required|string',
]);
$this->server->settings->update([
'is_logdrain_highlight_enabled' => false,
'is_logdrain_axiom_enabled' => false,
'is_logdrain_custom_enabled' => false,
]);
} elseif ($type === 'highlight') {
$this->validate([
'server.settings.is_logdrain_highlight_enabled' => 'required|boolean',
'server.settings.logdrain_highlight_project_id' => 'required|string',
]);
$this->server->settings->update([
'is_logdrain_newrelic_enabled' => false,
'is_logdrain_axiom_enabled' => false,
'is_logdrain_custom_enabled' => false,
]);
} elseif ($type === 'axiom') {
$this->validate([
'server.settings.is_logdrain_axiom_enabled' => 'required|boolean',
'server.settings.logdrain_axiom_dataset_name' => 'required|string',
'server.settings.logdrain_axiom_api_key' => 'required|string',
]);
$this->server->settings->update([
'is_logdrain_newrelic_enabled' => false,
'is_logdrain_highlight_enabled' => false,
'is_logdrain_custom_enabled' => false,
]);
} elseif ($type === 'custom') {
$this->validate([
'server.settings.is_logdrain_custom_enabled' => 'required|boolean',
'server.settings.logdrain_custom_config' => 'required|string',
'server.settings.logdrain_custom_config_parser' => 'nullable',
]);
$this->server->settings->update([
'is_logdrain_newrelic_enabled' => false,
'is_logdrain_highlight_enabled' => false,
'is_logdrain_axiom_enabled' => false,
]);
}
if (! $this->server->isLogDrainEnabled()) {
StopLogDrain::dispatch($this->server);
}
$this->server->settings->save();
$this->dispatch('success', 'Settings saved.'); $this->dispatch('success', 'Settings saved.');
return true;
} catch (\Throwable $e) { } catch (\Throwable $e) {
if ($type === 'newrelic') { return handleError($e, $this);
$this->server->settings->update([
'is_logdrain_newrelic_enabled' => false,
]);
} elseif ($type === 'highlight') {
$this->server->settings->update([
'is_logdrain_highlight_enabled' => false,
]);
} elseif ($type === 'axiom') {
$this->server->settings->update([
'is_logdrain_axiom_enabled' => false,
]);
} elseif ($type === 'custom') {
$this->server->settings->update([
'is_logdrain_custom_enabled' => false,
]);
}
handleError($e, $this);
return false;
} }
} }

View File

@@ -8,26 +8,63 @@ use Livewire\Component;
class Show extends Component class Show extends Component
{ {
public ?Server $server = null; public Server $server;
public $privateKeys = []; public $privateKeys = [];
public $parameters = []; public $parameters = [];
public function mount() public function mount(string $server_uuid)
{ {
$this->parameters = get_route_parameters();
try { try {
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first(); $this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail();
if (is_null($this->server)) {
return redirect()->route('server.index');
}
$this->privateKeys = PrivateKey::ownedByCurrentTeam()->get()->where('is_git_related', false); $this->privateKeys = PrivateKey::ownedByCurrentTeam()->get()->where('is_git_related', false);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }
} }
public function setPrivateKey($privateKeyId)
{
$ownedPrivateKey = PrivateKey::ownedByCurrentTeam()->find($privateKeyId);
if (is_null($ownedPrivateKey)) {
$this->dispatch('error', 'You are not allowed to use this private key.');
return;
}
$originalPrivateKeyId = $this->server->getOriginal('private_key_id');
try {
$this->server->update(['private_key_id' => $privateKeyId]);
['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection(justCheckingNewKey: true);
if ($uptime) {
$this->dispatch('success', 'Private key updated successfully.');
} else {
throw new \Exception($error);
}
} catch (\Exception $e) {
$this->server->update(['private_key_id' => $originalPrivateKeyId]);
$this->server->validateConnection();
$this->dispatch('error', $e->getMessage());
}
}
public function checkConnection()
{
try {
['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection();
if ($uptime) {
$this->dispatch('success', 'Server is reachable.');
} else {
$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;
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render() public function render()
{ {
return view('livewire.server.private-key.show'); return view('livewire.server.private-key.show');

View File

@@ -3,38 +3,203 @@
namespace App\Livewire\Server; namespace App\Livewire\Server;
use App\Models\Server; use App\Models\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Livewire\Attributes\Rule;
use Livewire\Component; use Livewire\Component;
class Show extends Component class Show extends Component
{ {
use AuthorizesRequests;
public Server $server; public Server $server;
public array $parameters; #[Rule(['required'])]
public string $name;
protected $listeners = ['refreshServerShow']; #[Rule(['nullable'])]
public ?string $description;
public function mount() #[Rule(['required'])]
public string $ip;
#[Rule(['required'])]
public string $user;
#[Rule(['required'])]
public string $port;
#[Rule(['nullable'])]
public ?string $validationLogs = null;
#[Rule(['nullable', 'url'])]
public ?string $wildcardDomain;
#[Rule(['required'])]
public bool $isReachable;
#[Rule(['required'])]
public bool $isUsable;
#[Rule(['required'])]
public bool $isSwarmManager;
#[Rule(['required'])]
public bool $isSwarmWorker;
#[Rule(['required'])]
public bool $isBuildServer;
#[Rule(['required'])]
public bool $isMetricsEnabled;
#[Rule(['required'])]
public string $sentinelToken;
#[Rule(['nullable'])]
public ?string $sentinelUpdatedAt;
#[Rule(['required', 'integer', 'min:1'])]
public int $sentinelMetricsRefreshRateSeconds;
#[Rule(['required', 'integer', 'min:1'])]
public int $sentinelMetricsHistoryDays;
#[Rule(['required', 'integer', 'min:10'])]
public int $sentinelPushIntervalSeconds;
#[Rule(['nullable', 'url'])]
public ?string $sentinelCustomUrl;
#[Rule(['required'])]
public bool $isSentinelEnabled;
#[Rule(['required'])]
public bool $isSentinelDebugEnabled;
#[Rule(['required'])]
public string $serverTimezone;
public array $timezones;
public function getListeners()
{
$teamId = auth()->user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},CloudflareTunnelConfigured" => '$refresh',
'refreshServerShow' => '$refresh',
];
}
public function mount(string $server_uuid)
{ {
try { try {
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->firstOrFail(); $this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail();
$this->parameters = get_route_parameters(); $this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray();
$this->syncData();
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }
} }
public function refreshServerShow() public function syncData(bool $toModel = false)
{ {
$this->server->refresh(); if ($toModel) {
$this->dispatch('$refresh'); $this->validate();
$this->server->name = $this->name;
$this->server->description = $this->description;
$this->server->ip = $this->ip;
$this->server->user = $this->user;
$this->server->port = $this->port;
$this->server->validation_logs = $this->validationLogs;
$this->server->save();
$this->server->settings->is_swarm_manager = $this->isSwarmManager;
$this->server->settings->is_swarm_worker = $this->isSwarmWorker;
$this->server->settings->is_build_server = $this->isBuildServer;
$this->server->settings->is_metrics_enabled = $this->isMetricsEnabled;
$this->server->settings->sentinel_token = $this->sentinelToken;
$this->server->settings->sentinel_metrics_refresh_rate_seconds = $this->sentinelMetricsRefreshRateSeconds;
$this->server->settings->sentinel_metrics_history_days = $this->sentinelMetricsHistoryDays;
$this->server->settings->sentinel_push_interval_seconds = $this->sentinelPushIntervalSeconds;
$this->server->settings->sentinel_custom_url = $this->sentinelCustomUrl;
$this->server->settings->is_sentinel_enabled = $this->isSentinelEnabled;
$this->server->settings->is_sentinel_debug_enabled = $this->isSentinelDebugEnabled;
$this->server->settings->server_timezone = $this->serverTimezone;
$this->server->settings->save();
} else {
$this->name = $this->server->name;
$this->description = $this->server->description;
$this->ip = $this->server->ip;
$this->user = $this->server->user;
$this->port = $this->server->port;
$this->wildcardDomain = $this->server->settings->wildcard_domain;
$this->isReachable = $this->server->settings->is_reachable;
$this->isUsable = $this->server->settings->is_usable;
$this->isSwarmManager = $this->server->settings->is_swarm_manager;
$this->isSwarmWorker = $this->server->settings->is_swarm_worker;
$this->isBuildServer = $this->server->settings->is_build_server;
$this->isMetricsEnabled = $this->server->settings->is_metrics_enabled;
$this->sentinelToken = $this->server->settings->sentinel_token;
$this->sentinelMetricsRefreshRateSeconds = $this->server->settings->sentinel_metrics_refresh_rate_seconds;
$this->sentinelMetricsHistoryDays = $this->server->settings->sentinel_metrics_history_days;
$this->sentinelPushIntervalSeconds = $this->server->settings->sentinel_push_interval_seconds;
$this->sentinelCustomUrl = $this->server->settings->sentinel_custom_url;
$this->isSentinelEnabled = $this->server->settings->is_sentinel_enabled;
$this->isSentinelDebugEnabled = $this->server->settings->is_sentinel_debug_enabled;
$this->sentinelUpdatedAt = $this->server->settings->updated_at;
$this->serverTimezone = $this->server->settings->server_timezone;
}
}
public function validateServer($install = true)
{
try {
$this->validationLogs = $this->server->validation_logs = null;
$this->server->save();
$this->dispatch('init', $install);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function checkLocalhostConnection()
{
$this->syncData(true);
['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection();
if ($uptime) {
$this->dispatch('success', 'Server is reachable.');
$this->server->settings->is_reachable = $this->isReachable = true;
$this->server->settings->is_usable = $this->isUsable = true;
$this->server->settings->save();
$this->dispatch('proxyStatusUpdated');
} else {
$this->dispatch('error', 'Server is not reachable.', 'Please validate your configuration and connection.<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;
}
}
public function regenerateSentinelToken()
{
try {
$this->server->settings->generateSentinelToken();
$this->dispatch('success', 'Token regenerated & Sentinel restarted.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function instantSave()
{
$this->submit();
} }
public function submit() public function submit()
{ {
$this->dispatch('serverRefresh', false); try {
$this->syncData(true);
$this->dispatch('success', 'Server updated');
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
public function render() public function render()

View File

@@ -1,68 +0,0 @@
<?php
namespace App\Livewire\Server;
use App\Models\PrivateKey;
use App\Models\Server;
use Livewire\Component;
class ShowPrivateKey extends Component
{
public Server $server;
public $privateKeys;
public $parameters;
public function mount()
{
$this->parameters = get_route_parameters();
}
public function setPrivateKey($privateKeyId)
{
$ownedPrivateKey = PrivateKey::ownedByCurrentTeam()->find($privateKeyId);
if (is_null($ownedPrivateKey)) {
$this->dispatch('error', 'You are not allowed to use this private key.');
return;
}
$originalPrivateKeyId = $this->server->getOriginal('private_key_id');
try {
$this->server->update(['private_key_id' => $privateKeyId]);
['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection(justCheckingNewKey: true);
if ($uptime) {
$this->dispatch('success', 'Private key updated successfully.');
} else {
throw new \Exception($error);
}
} catch (\Exception $e) {
$this->server->update(['private_key_id' => $originalPrivateKeyId]);
$this->server->validateConnection();
$this->dispatch('error', $e->getMessage());
} finally {
$this->dispatch('refreshServerShow');
$this->server->refresh();
}
}
public function checkConnection()
{
try {
['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection();
if ($uptime) {
$this->dispatch('success', 'Server is reachable.');
} else {
$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;
}
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
$this->dispatch('refreshServerShow');
$this->server->refresh();
}
}
}

View File

@@ -63,7 +63,11 @@ class Server extends BaseModel
$payload['ip'] = str($server->ip)->trim(); $payload['ip'] = str($server->ip)->trim();
} }
$server->forceFill($payload); $server->forceFill($payload);
});
static::saved(function ($server) {
if ($server->privateKey->isDirty()) {
refresh_server_connection($server->privateKey);
}
}); });
static::created(function ($server) { static::created(function ($server) {
ServerSetting::create([ ServerSetting::create([
@@ -1027,7 +1031,6 @@ $schema://$host {
$this->refresh(); $this->refresh();
$unreachableNotificationSent = (bool) $this->unreachable_notification_sent; $unreachableNotificationSent = (bool) $this->unreachable_notification_sent;
$isReachable = (bool) $this->settings->is_reachable; $isReachable = (bool) $this->settings->is_reachable;
loggy('Server setting is_reachable changed to '.$isReachable.' for server '.$this->id.'. Unreachable notification sent: '.$unreachableNotificationSent);
// If the server is reachable, send the reachable notification if it was sent before // If the server is reachable, send the reachable notification if it was sent before
if ($isReachable === true) { if ($isReachable === true) {
if ($unreachableNotificationSent === true) { if ($unreachableNotificationSent === true) {

View File

@@ -117,7 +117,6 @@ class ServerSetting extends Model
$domain = 'http://'.$settings->public_ipv6.':8000'; $domain = 'http://'.$settings->public_ipv6.':8000';
} }
$this->sentinel_custom_url = $domain; $this->sentinel_custom_url = $domain;
loggy('Sentinel URL: '.$domain);
if ($save) { if ($save) {
$this->save(); $this->save();
} }

View File

@@ -1,27 +0,0 @@
<?php
namespace App\View\Components\Server;
use App\Models\Server;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class Sidebar extends Component
{
/**
* Create a new component instance.
*/
public function __construct(public Server $server, public $parameters)
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.server.sidebar');
}
}

View File

@@ -127,7 +127,6 @@ 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)
{ {
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.");
@@ -370,6 +369,9 @@ function translate_cron_expression($expression_to_validate): string
} }
function validate_cron_expression($expression_to_validate): bool function validate_cron_expression($expression_to_validate): bool
{ {
if (empty($expression_to_validate)) {
return false;
}
$isValid = false; $isValid = false;
$expression = new CronExpression($expression_to_validate); $expression = new CronExpression($expression_to_validate);
$isValid = $expression->isValid(); $isValid = $expression->isValid();

View File

@@ -14,10 +14,7 @@
'w-full' => $fullWidth, 'w-full' => $fullWidth,
])> ])>
@if (!$hideLabel) @if (!$hideLabel)
<label @class([ <label @class(['flex gap-4 px-0 min-w-fit label', 'opacity-40' => $disabled])>
"flex gap-4 px-0 min-w-fit label",
'opacity-40' => $disabled,
])>
<span class="flex gap-2"> <span class="flex gap-2">
@if ($label) @if ($label)
{!! $label !!} {!! $label !!}
@@ -34,4 +31,5 @@
<input @disabled($disabled) type="checkbox" {{ $attributes->merge(['class' => $defaultClass]) }} <input @disabled($disabled) type="checkbox" {{ $attributes->merge(['class' => $defaultClass]) }}
@if ($instantSave) wire:loading.attr="disabled" wire:click='{{ $instantSave === 'instantSave' || $instantSave == '1' ? 'instantSave' : $instantSave }}' @if ($instantSave) wire:loading.attr="disabled" wire:click='{{ $instantSave === 'instantSave' || $instantSave == '1' ? 'instantSave' : $instantSave }}'
wire:model={{ $id }} @else wire:model={{ $value ?? $id }} @endif /> wire:model={{ $id }} @else wire:model={{ $value ?? $id }} @endif />
</div> </div>

View File

@@ -1,6 +1,7 @@
@props([ @props([
'title' => 'Are you sure?', 'title' => 'Are you sure?',
'isErrorButton' => false, 'isErrorButton' => false,
'isHighlightedButton' => false,
'buttonTitle' => 'Confirm Action', 'buttonTitle' => 'Confirm Action',
'buttonFullWidth' => false, 'buttonFullWidth' => false,
'customButton' => null, 'customButton' => null,
@@ -143,6 +144,16 @@
{{ $buttonTitle }} {{ $buttonTitle }}
</x-forms.button> </x-forms.button>
@endif @endif
@elseif($isHighlightedButton)
@if ($buttonFullWidth)
<x-forms.button @click="modalOpen=true" class="flex gap-2 w-full" isHighlighted wire:target>
{{ $buttonTitle }}
</x-forms.button>
@else
<x-forms.button @click="modalOpen=true" class="flex gap-2" isHighlighted wire:target>
{{ $buttonTitle }}
</x-forms.button>
@endif
@else @else
@if ($buttonFullWidth) @if ($buttonFullWidth)
<x-forms.button @click="modalOpen=true" class="flex gap-2 w-full" wire:target> <x-forms.button @click="modalOpen=true" class="flex gap-2 w-full" wire:target>

View File

@@ -2,6 +2,7 @@
'title' => 'Are you sure?', 'title' => 'Are you sure?',
'buttonTitle' => 'Open Modal', 'buttonTitle' => 'Open Modal',
'isErrorButton' => false, 'isErrorButton' => false,
'isHighlightedButton' => false,
'disabled' => false, 'disabled' => false,
'action' => 'delete', 'action' => 'delete',
'content' => null, 'content' => null,
@@ -18,6 +19,8 @@
<x-forms.button isError disabled>{{ $buttonTitle }}</x-forms.button> <x-forms.button isError disabled>{{ $buttonTitle }}</x-forms.button>
@elseif ($isErrorButton) @elseif ($isErrorButton)
<x-forms.button isError @click="modalOpen=true">{{ $buttonTitle }}</x-forms.button> <x-forms.button isError @click="modalOpen=true">{{ $buttonTitle }}</x-forms.button>
@elseif ($isHighlightedButton)
<x-forms.button isHighlighted @click="modalOpen=true">{{ $buttonTitle }}</x-forms.button>
@else @else
<x-forms.button @click="modalOpen=true">{{ $buttonTitle }}</x-forms.button> <x-forms.button @click="modalOpen=true">{{ $buttonTitle }}</x-forms.button>
@endif @endif

View File

@@ -20,7 +20,7 @@
<nav class="flex items-center gap-6 overflow-x-scroll sm:overflow-x-hidden scrollbar min-h-10 whitespace-nowrap"> <nav class="flex items-center gap-6 overflow-x-scroll sm:overflow-x-hidden scrollbar min-h-10 whitespace-nowrap">
<a class="{{ request()->routeIs('server.show') ? 'dark:text-white' : '' }}" <a class="{{ request()->routeIs('server.show') ? 'dark:text-white' : '' }}"
href="{{ route('server.show', [ href="{{ route('server.show', [
'server_uuid' => data_get($parameters, 'server_uuid'), 'server_uuid' => data_get($server, 'uuid'),
]) }}"> ]) }}">
<button>Configuration</button> <button>Configuration</button>
</a> </a>
@@ -28,20 +28,20 @@
@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', [
'server_uuid' => data_get($parameters, 'server_uuid'), 'server_uuid' => data_get($server, 'uuid'),
]) }}"> ]) }}">
<button>Proxy</button> <button>Proxy</button>
</a> </a>
@endif @endif
<a class="{{ request()->routeIs('server.resources') ? 'dark:text-white' : '' }}" <a class="{{ request()->routeIs('server.resources') ? 'dark:text-white' : '' }}"
href="{{ route('server.resources', [ href="{{ route('server.resources', [
'server_uuid' => data_get($parameters, 'server_uuid'), 'server_uuid' => data_get($server, 'uuid'),
]) }}"> ]) }}">
<button>Resources</button> <button>Resources</button>
</a> </a>
<a class="{{ request()->routeIs('server.command') ? 'dark:text-white' : '' }}" <a class="{{ request()->routeIs('server.command') ? 'dark:text-white' : '' }}"
href="{{ route('server.command', [ href="{{ route('server.command', [
'server_uuid' => data_get($parameters, 'server_uuid'), 'server_uuid' => data_get($server, 'uuid'),
]) }}"> ]) }}">
<button>Terminal</button> <button>Terminal</button>
</a> </a>

View File

@@ -0,0 +1,16 @@
@if ($server->proxySet())
<div class="flex flex-col items-start gap-2 min-w-fit">
<a class="{{ request()->routeIs('server.proxy') ? 'menu-item menu-item-active' : 'menu-item' }}"
href="{{ route('server.proxy', $parameters) }}">
<button>Configuration</button>
</a>
<a class="{{ request()->routeIs('server.proxy.dynamic-confs') ? 'menu-item menu-item-active' : 'menu-item' }}"
href="{{ route('server.proxy.dynamic-confs', $parameters) }}">
<button>Dynamic Configurations</button>
</a>
<a class="{{ request()->routeIs('server.proxy.logs') ? 'menu-item menu-item-active' : 'menu-item' }}"
href="{{ route('server.proxy.logs', $parameters) }}">
<button>Logs</button>
</a>
</div>
@endif

View File

@@ -1,16 +1,29 @@
@if ($server->proxySet()) <div class="flex flex-col items-start gap-2 min-w-fit">
<div class="flex flex-col items-start gap-2 min-w-fit"> <a class="menu-item {{ $activeMenu === 'general' ? 'menu-item-active' : '' }}"
<a class="{{ request()->routeIs('server.proxy') ? 'menu-item menu-item-active' : 'menu-item' }}" href="{{ route('server.show', ['server_uuid' => $server->uuid]) }}" wire:navigate>General</a>
href="{{ route('server.proxy', $parameters) }}"> @if ($server->isFunctional())
<button>Configuration</button> <a class="menu-item {{ $activeMenu === 'advanced' ? 'menu-item-active' : '' }}"
href="{{ route('server.advanced', ['server_uuid' => $server->uuid]) }}" wire:navigate>Advanced
</a> </a>
<a class="{{ request()->routeIs('server.proxy.dynamic-confs') ? 'menu-item menu-item-active' : 'menu-item' }}" @endif
href="{{ route('server.proxy.dynamic-confs', $parameters) }}"> <a class="menu-item {{ $activeMenu === 'private-key' ? 'menu-item-active' : '' }}"
<button>Dynamic Configurations</button> href="{{ route('server.private-key', ['server_uuid' => $server->uuid]) }}" wire:navigate>Private Key
</a>
@if ($server->isFunctional())
<a class="menu-item {{ $activeMenu === 'cloudflare-tunnels' ? 'menu-item-active' : '' }}"
href="{{ route('server.cloudflare-tunnels', ['server_uuid' => $server->uuid]) }}" wire:navigate>Cloudflare
Tunnels</a>
<a class="menu-item {{ $activeMenu === 'destinations' ? 'menu-item-active' : '' }}"
href="{{ route('server.destinations', ['server_uuid' => $server->uuid]) }}" wire:navigate>Destinations
</a> </a>
<a class="{{ request()->routeIs('server.proxy.logs') ? 'menu-item menu-item-active' : 'menu-item' }}" <a class="menu-item {{ $activeMenu === 'log-drains' ? 'menu-item-active' : '' }}"
href="{{ route('server.proxy.logs', $parameters) }}"> href="{{ route('server.log-drains', ['server_uuid' => $server->uuid]) }}" wire:navigate>Log
<button>Logs</button> Drains</a>
</a> <a class="menu-item {{ $activeMenu === 'metrics' ? 'menu-item-active' : '' }}"
</div> href="{{ route('server.charts', ['server_uuid' => $server->uuid]) }}">Metrics</a>
@endif @endif
@if (!$server->isLocalhost())
<a class="menu-item {{ $activeMenu === 'danger' ? 'menu-item-active' : '' }}"
href="{{ route('server.delete', ['server_uuid' => $server->uuid]) }}" wire:navigate>Danger</a>
@endif
</div>

View File

@@ -1,47 +1,50 @@
<form wire:submit='submit'> <div>
<div> <x-server.navbar :server="$server" />
<div class="flex items-center gap-2"> <div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'general' }" class="flex flex-col h-full gap-8 sm:flex-row">
<h2>Advanced</h2> <x-server.sidebar :server="$server" activeMenu="advanced" />
<x-forms.button type="submit">Save</x-forms.button> <form wire:submit='submit' class="w-full">
<x-modal-confirmation title="Confirm Docker Cleanup?" buttonTitle="Trigger Manual Cleanup" <div>
submitAction="manualCleanup" :actions="[ <div class="flex items-center gap-2">
'Permanently deletes all stopped containers managed by Coolify (as containers are non-persistent, no data will be lost)', <h2>Advanced</h2>
'Permanently deletes all unused images', <x-forms.button type="submit">Save</x-forms.button>
'Clears build cache', <x-modal-confirmation title="Confirm Docker Cleanup?" buttonTitle="Trigger Manual Cleanup"
'Removes old versions of the Coolify helper image', isHighlightedButton submitAction="manualCleanup" :actions="[
'Optionally permanently deletes all unused volumes (if enabled in advanced options).', 'Permanently deletes all stopped containers managed by Coolify (as containers are non-persistent, no data will be lost)',
'Optionally permanently deletes all unused networks (if enabled in advanced options).', 'Permanently deletes all unused images',
]" :confirmWithText="false" :confirmWithPassword="false" 'Clears build cache',
step2ButtonText="Trigger Docker Cleanup" /> 'Removes old versions of the Coolify helper image',
</div> 'Optionally permanently deletes all unused volumes (if enabled in advanced options).',
<div>Advanced configuration for your server.</div> 'Optionally permanently deletes all unused networks (if enabled in advanced options).',
</div> ]" :confirmWithText="false"
:confirmWithPassword="false" step2ButtonText="Trigger Docker Cleanup" />
</div>
<div>Advanced configuration for your server.</div>
</div>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex flex-wrap gap-2 sm:flex-nowrap pt-4"> <div class="flex flex-wrap gap-2 sm:flex-nowrap pt-4">
<x-forms.input id="server.settings.server_disk_usage_notification_threshold" <x-forms.input id="serverDiskUsageNotificationThreshold"
label="Server disk usage notification threshold (%)" required label="Server disk usage notification threshold (%)" required
helper="If the server disk usage exceeds this threshold, Coolify will send a notification to the team members." /> helper="If the server disk usage exceeds this threshold, Coolify will send a notification to the team members." />
</div> </div>
</div> </div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h3>Docker Cleanup</h3> <h3>Docker Cleanup</h3>
</div> </div>
<div class="flex flex-wrap items-center gap-4"> <div class="flex flex-wrap items-center gap-4">
@if ($server->settings->force_docker_cleanup) @if ($forceDockerCleanup)
<x-forms.input placeholder="*/10 * * * *" id="server.settings.docker_cleanup_frequency" <x-forms.input placeholder="*/10 * * * *" id="dockerCleanupFrequency"
label="Docker cleanup frequency" required 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." /> 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 @else
<x-forms.input id="server.settings.docker_cleanup_threshold" label="Docker cleanup threshold (%)" <x-forms.input id="dockerCleanupThreshold" label="Docker cleanup threshold (%)" required
required helper="The Docker cleanup tasks will run when the disk usage exceeds this threshold." />
helper="The Docker cleanup tasks will run when the disk usage exceeds this threshold." /> @endif
@endif <div class="w-96">
<div class="w-96"> <x-forms.checkbox
<x-forms.checkbox helper="Enabling Force Docker Cleanup or manually triggering a cleanup will perform the following actions:
helper="Enabling Force Docker Cleanup or manually triggering a cleanup will perform the following actions:
<ul class='list-disc pl-4 mt-2'> <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>Removes stopped containers manged by Coolify (as containers are none persistent, no data will be lost).</li>
<li>Deletes unused images.</li> <li>Deletes unused images.</li>
@@ -50,43 +53,45 @@
<li>Optionally delete unused volumes (if enabled in advanced options).</li> <li>Optionally delete unused volumes (if enabled in advanced options).</li>
<li>Optionally remove unused networks (if enabled in advanced options).</li> <li>Optionally remove unused networks (if enabled in advanced options).</li>
</ul>" </ul>"
instantSave id="server.settings.force_docker_cleanup" label="Force Docker Cleanup" /> instantSave id="forceDockerCleanup" label="Force Docker Cleanup" />
</div> </div>
</div> </div>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-2"> <p class="text-sm text-gray-600 dark:text-gray-400 mb-2">
<span class="dark:text-warning font-bold">Warning: Enable these <span class="dark:text-warning font-bold">Warning: Enable these
options only if you fully understand their implications and options only if you fully understand their implications and
consequences!</span><br>Improper use will result in data loss and could cause consequences!</span><br>Improper use will result in data loss and could cause
functional issues. functional issues.
</p> </p>
<div class="w-96"> <div class="w-96">
<x-forms.checkbox instantSave id="server.settings.delete_unused_volumes" label="Delete Unused Volumes" <x-forms.checkbox instantSave id="deleteUnusedVolumes" 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> 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'> <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>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>Data from stopped containers volumes will be permanently lost.</li>
<li>No way to recover deleted volume data.</li> <li>No way to recover deleted volume data.</li>
</ul>" /> </ul>" />
<x-forms.checkbox instantSave id="server.settings.delete_unused_networks" label="Delete Unused Networks" <x-forms.checkbox instantSave id="deleteUnusedNetworks" 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> 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'> <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>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>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> <li>Functionality may be lost and containers may not be able to communicate with each other.</li>
</ul>" /> </ul>" />
</div> </div>
</div> </div>
<div class="flex flex-col"> <div class="flex flex-col">
<h3>Builds</h3> <h3>Builds</h3>
<div>Customize the build process.</div> <div>Customize the build process.</div>
<div class="flex flex-wrap gap-2 sm:flex-nowrap pt-4"> <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 <x-forms.input id="concurrentBuilds" label="Number of concurrent builds" required
helper="You can specify the number of simultaneous build processes/deployments that should run concurrently." /> 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 <x-forms.input id="dynamicTimeout" label="Deployment timeout (seconds)" required
helper="You can define the maximum duration for a deployment to run before timing it out." /> helper="You can define the maximum duration for a deployment to run before timing it out." />
</div>
</div>
</div> </div>
</div> </form>
</div> </div>
</form> </div>

View File

@@ -1,240 +1,253 @@
<div @if ($poll) wire:poll.5000ms='pollData' @endif x-init="$wire.loadData()"> <div>
<x-forms.select label="Interval" wire:change="setInterval" id="interval"> <x-slot:title>
<option value="5">5 minutes (live)</option> {{ data_get_str($server, 'name')->limit(10) }} > Server Destinations | Coolify
<option value="10">10 minutes (live)</option> </x-slot>
<option value="30">30 minutes</option> <x-server.navbar :server="$server" />
<option value="60">1 hour</option> <div class="flex flex-col h-full gap-8 sm:flex-row">
<option value="720">12 hours</option> <x-server.sidebar :server="$server" activeMenu="metrics" />
<option value="10080">1 week</option> <div class="w-full">
<option value="43200">30 days</option> <h2>Metrics</h2>
</x-forms.select> <div class="pb-4">Basic metrics for your container.</div>
<h4 class="pt-4">CPU (%)</h4> <div @if ($poll) wire:poll.5000ms='pollData' @endif x-init="$wire.loadData()">
<div wire:ignore id="{!! $chartId !!}-cpu"></div> <x-forms.select label="Interval" wire:change="setInterval" id="interval">
<option value="5">5 minutes (live)</option>
<option value="10">10 minutes (live)</option>
<option value="30">30 minutes</option>
<option value="60">1 hour</option>
<option value="720">12 hours</option>
<option value="10080">1 week</option>
<option value="43200">30 days</option>
</x-forms.select>
<h4 class="pt-4">CPU (%)</h4>
<div wire:ignore id="{!! $chartId !!}-cpu"></div>
<script> <script>
checkTheme();
const optionsServerCpu = {
stroke: {
curve: 'straight',
},
chart: {
height: '150px',
id: '{!! $chartId !!}-cpu',
type: 'area',
toolbar: {
show: true,
tools: {
download: false,
selection: false,
zoom: true,
zoomin: false,
zoomout: false,
pan: false,
reset: true
},
},
animations: {
enabled: false,
},
},
fill: {
type: 'gradient',
},
dataLabels: {
enabled: false,
offsetY: -10,
style: {
colors: ['#FCD452'],
},
background: {
enabled: false,
}
},
grid: {
show: true,
borderColor: '',
},
colors: [baseColor],
xaxis: {
type: 'datetime',
},
series: [{
name: 'CPU %',
data: []
}],
noData: {
text: 'Loading...',
style: {
color: textColor,
}
},
tooltip: {
enabled: true,
marker: {
show: false,
}
},
legend: {
show: false
}
}
const serverCpuChart = new ApexCharts(document.getElementById(`{!! $chartId !!}-cpu`),
optionsServerCpu);
serverCpuChart.render();
document.addEventListener('livewire:init', () => {
Livewire.on('refreshChartData-{!! $chartId !!}-cpu', (chartData) => {
checkTheme();
serverCpuChart.updateOptions({
series: [{
data: chartData[0].seriesData,
}],
colors: [baseColor],
xaxis: {
type: 'datetime',
labels: {
show: true,
style: {
colors: textColor,
}
}
},
yaxis: {
show: true,
labels: {
show: true,
style: {
colors: textColor,
}
}
},
noData: {
text: 'Loading...',
style: {
color: textColor,
}
}
});
});
});
</script>
<div>
<h4>Memory (%)</h4>
<div wire:ignore id="{!! $chartId !!}-memory"></div>
<script>
checkTheme();
const optionsServerMemory = {
stroke: {
curve: 'straight',
},
chart: {
height: '150px',
id: '{!! $chartId !!}-memory',
type: 'area',
toolbar: {
show: true,
tools: {
download: false,
selection: false,
zoom: true,
zoomin: false,
zoomout: false,
pan: false,
reset: true
},
},
animations: {
enabled: false,
},
},
fill: {
type: 'gradient',
},
dataLabels: {
enabled: false,
offsetY: -10,
style: {
colors: ['#FCD452'],
},
background: {
enabled: false,
}
},
grid: {
show: true,
borderColor: '',
},
colors: [baseColor],
xaxis: {
type: 'datetime',
labels: {
show: true,
style: {
colors: textColor,
}
}
},
series: [{
name: "Memory (%)",
data: []
}],
noData: {
text: 'Loading...',
style: {
color: textColor,
}
},
tooltip: {
enabled: true,
marker: {
show: false,
}
},
legend: {
show: false
}
}
const serverMemoryChart = new ApexCharts(document.getElementById(`{!! $chartId !!}-memory`),
optionsServerMemory);
serverMemoryChart.render();
document.addEventListener('livewire:init', () => {
Livewire.on('refreshChartData-{!! $chartId !!}-memory', (chartData) => {
checkTheme(); checkTheme();
serverMemoryChart.updateOptions({ const optionsServerCpu = {
series: [{ stroke: {
data: chartData[0].seriesData, curve: 'straight',
}], },
chart: {
height: '150px',
id: '{!! $chartId !!}-cpu',
type: 'area',
toolbar: {
show: true,
tools: {
download: false,
selection: false,
zoom: true,
zoomin: false,
zoomout: false,
pan: false,
reset: true
},
},
animations: {
enabled: false,
},
},
fill: {
type: 'gradient',
},
dataLabels: {
enabled: false,
offsetY: -10,
style: {
colors: ['#FCD452'],
},
background: {
enabled: false,
}
},
grid: {
show: true,
borderColor: '',
},
colors: [baseColor], colors: [baseColor],
xaxis: { xaxis: {
type: 'datetime', type: 'datetime',
labels: {
show: true,
style: {
colors: textColor,
}
}
},
yaxis: {
min: 0,
show: true,
labels: {
show: true,
style: {
colors: textColor,
}
}
}, },
series: [{
name: 'CPU %',
data: []
}],
noData: { noData: {
text: 'Loading...', text: 'Loading...',
style: { style: {
color: textColor, color: textColor,
} }
},
tooltip: {
enabled: true,
marker: {
show: false,
}
},
legend: {
show: false
} }
}
const serverCpuChart = new ApexCharts(document.getElementById(`{!! $chartId !!}-cpu`),
optionsServerCpu);
serverCpuChart.render();
document.addEventListener('livewire:init', () => {
Livewire.on('refreshChartData-{!! $chartId !!}-cpu', (chartData) => {
checkTheme();
serverCpuChart.updateOptions({
series: [{
data: chartData[0].seriesData,
}],
colors: [baseColor],
xaxis: {
type: 'datetime',
labels: {
show: true,
style: {
colors: textColor,
}
}
},
yaxis: {
show: true,
labels: {
show: true,
style: {
colors: textColor,
}
}
},
noData: {
text: 'Loading...',
style: {
color: textColor,
}
}
});
});
}); });
}); </script>
});
</script>
<div>
<h4>Memory (%)</h4>
<div wire:ignore id="{!! $chartId !!}-memory"></div>
<script>
checkTheme();
const optionsServerMemory = {
stroke: {
curve: 'straight',
},
chart: {
height: '150px',
id: '{!! $chartId !!}-memory',
type: 'area',
toolbar: {
show: true,
tools: {
download: false,
selection: false,
zoom: true,
zoomin: false,
zoomout: false,
pan: false,
reset: true
},
},
animations: {
enabled: false,
},
},
fill: {
type: 'gradient',
},
dataLabels: {
enabled: false,
offsetY: -10,
style: {
colors: ['#FCD452'],
},
background: {
enabled: false,
}
},
grid: {
show: true,
borderColor: '',
},
colors: [baseColor],
xaxis: {
type: 'datetime',
labels: {
show: true,
style: {
colors: textColor,
}
}
},
series: [{
name: "Memory (%)",
data: []
}],
noData: {
text: 'Loading...',
style: {
color: textColor,
}
},
tooltip: {
enabled: true,
marker: {
show: false,
}
},
legend: {
show: false
}
}
const serverMemoryChart = new ApexCharts(document.getElementById(`{!! $chartId !!}-memory`),
optionsServerMemory);
serverMemoryChart.render();
document.addEventListener('livewire:init', () => {
Livewire.on('refreshChartData-{!! $chartId !!}-memory', (chartData) => {
checkTheme();
serverMemoryChart.updateOptions({
series: [{
data: chartData[0].seriesData,
}],
colors: [baseColor],
xaxis: {
type: 'datetime',
labels: {
show: true,
style: {
colors: textColor,
}
}
},
yaxis: {
min: 0,
show: true,
labels: {
show: true,
style: {
colors: textColor,
}
}
},
noData: {
text: 'Loading...',
style: {
color: textColor,
}
}
});
});
});
</script>
</div>
</div>
</div>
</div> </div>
</div> </div>

View File

@@ -1,42 +1,54 @@
<div> <div>
<div class="flex gap-1 items-center"> <x-slot:title>
<h2>Cloudflare Tunnels</h2> {{ data_get_str($server, 'name')->limit(10) }} > Cloudflare Tunnels | Coolify
<x-helper class="inline-flex" </x-slot>
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>" /> <x-server.navbar :server="$server" />
</div> <div class="flex flex-col h-full gap-8 sm:flex-row">
<div class="flex flex-col gap-2 pt-6"> <x-server.sidebar :server="$server" activeMenu="cloudflare-tunnels" />
@if ($server->settings->is_cloudflare_tunnel) <div class="w-full">
<div class="w-64"> <div class="flex flex-col">
<x-forms.checkbox instantSave id="server.settings.is_cloudflare_tunnel" <div class="flex gap-1 items-center">
label="Enabled" /> <h2>Cloudflare Tunnels</h2>
</div> <x-helper class="inline-flex"
@elseif (!$server->isFunctional()) 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="p-4 mb-4 w-full text-sm text-yellow-800 bg-yellow-100 rounded dark:bg-yellow-900 dark:text-yellow-300"> <div>Secure your servers with Cloudflare Tunnels</div>
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 class="flex flex-col gap-2 pt-6">
@if ($isCloudflareTunnelsEnabled)
<div class="w-64">
<x-forms.checkbox instantSave id="isCloudflareTunnelsEnabled" 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 (!$isCloudflareTunnelsEnabled && $server->isFunctional())
<h4>Configuration</h4>
<div class="flex gap-2">
<x-modal-input buttonTitle="Automated" title="Cloudflare Tunnels" :closeOutside="false"
isHighlightedButton>
<livewire:server.configure-cloudflare-tunnels :server_id="$server->id" />
</x-modal-input>
<x-forms.button wire:click="manualCloudflareConfig" class="w-20">
Manual
</x-forms.button>
</div>
@endif
</div>
</div>
</div> </div>
</div> </div>

View File

@@ -1,22 +1,31 @@
<div> <div>
@if ($server->id !== 0) <x-slot:title>
<h2>Danger Zone</h2> {{ data_get_str($server, 'name')->limit(10) }} > Delete Server | Coolify
<div class="">Woah. I hope you know what are you doing.</div> </x-slot>
<h4 class="pt-4">Delete Server</h4> <x-server.navbar :server="$server" />
<div class="pb-4">This will remove this server from Coolify. Beware! There is no coming <div class="flex flex-col h-full gap-8 sm:flex-row">
back! <x-server.sidebar :server="$server" activeMenu="danger" />
<div class="w-full">
@if ($server->id !== 0)
<h2>Danger Zone</h2>
<div class="">Woah. I hope you know what are you doing.</div>
<h4 class="pt-4">Delete Server</h4>
<div class="pb-4">This will remove this server from Coolify. Beware! There is no coming
back!
</div>
@if ($server->definedResources()->count() > 0)
<div class="pb-2 text-red-500">You need to delete all resources before deleting this server.</div>
<x-modal-confirmation title="Confirm Server Deletion?" isErrorButton buttonTitle="Delete"
submitAction="delete" :actions="['This server will be permanently deleted.']" confirmationText="{{ $server->name }}"
confirmationLabel="Please confirm the execution of the actions by entering the Server Name below"
shortConfirmationLabel="Server Name" step3ButtonText="Permanently Delete" />
@else
<x-modal-confirmation title="Confirm Server Deletion?" isErrorButton buttonTitle="Delete"
submitAction="delete" :actions="['This server will be permanently deleted.']" confirmationText="{{ $server->name }}"
confirmationLabel="Please confirm the execution of the actions by entering the Server Name below"
shortConfirmationLabel="Server Name" step3ButtonText="Permanently Delete" />
@endif
@endif
</div> </div>
@if ($server->definedResources()->count() > 0) </div>
<div class="pb-2 text-red-500">You need to delete all resources before deleting this server.</div>
<x-modal-confirmation title="Confirm Server Deletion?" isErrorButton buttonTitle="Delete" submitAction="delete"
:actions="['This server will be permanently deleted.']" confirmationText="{{ $server->name }}"
confirmationLabel="Please confirm the execution of the actions by entering the Server Name below"
shortConfirmationLabel="Server Name" step3ButtonText="Permanently Delete" />
@else
<x-modal-confirmation title="Confirm Server Deletion?" isErrorButton buttonTitle="Delete"
submitAction="delete" :actions="['This server will be permanently deleted.']" confirmationText="{{ $server->name }}"
confirmationLabel="Please confirm the execution of the actions by entering the Server Name below"
shortConfirmationLabel="Server Name" step3ButtonText="Permanently Delete" />
@endif
@endif
</div> </div>

View File

@@ -2,6 +2,48 @@
<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" />
<livewire:destination.show :server="$server" /> <div class="flex flex-col h-full gap-8 sm:flex-row">
<x-server.sidebar :server="$server" activeMenu="destinations" />
<div class="w-full">
@if ($server->isFunctional())
<div class="flex items-end gap-2">
<h2>Destinations</h2>
<x-modal-input buttonTitle="+ Add" title="New Destination">
<livewire:destination.new.docker :server_id="$server->id" />
</x-modal-input>
<x-forms.button isHighlighted wire:click='scan'>Scan for Destinations</x-forms.button>
</div>
<div>Destinations are used to segregate resources by network.</div>
<h4 class="pt-4 pb-2">Available Destinations</h4>
<div class="flex gap-2">
@foreach ($server->standaloneDockers as $docker)
<a href="{{ route('destination.show', ['destination_uuid' => data_get($docker, 'uuid')]) }}">
<x-forms.button>{{ data_get($docker, 'network') }} </x-forms.button>
</a>
@endforeach
@foreach ($server->swarmDockers as $docker)
<a href="{{ route('destination.show', ['destination_uuid' => data_get($docker, 'uuid')]) }}">
<x-forms.button>{{ data_get($docker, 'network') }} </x-forms.button>
</a>
@endforeach
</div>
@if ($networks->count() > 0)
<div class="pt-2">
<h3 class="pb-4">Found Destinations</h3>
<div class="flex flex-wrap gap-2 ">
@foreach ($networks as $network)
<div class="min-w-fit">
<x-forms.button wire:click="add('{{ data_get($network, 'Name') }}')">Add
{{ data_get($network, 'Name') }}</x-forms.button>
</div>
@endforeach
</div>
</div>
@endif
@else
<div>Server is not validated. Validate first.</div>
@endif
</div>
</div>
</div> </div>

View File

@@ -1,120 +1,118 @@
<div> <div>
<x-slot:title> <x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > Server LogDrains | Coolify {{ data_get_str($server, 'name')->limit(10) }} > Server Log Drains | Coolify
</x-slot> </x-slot>
{{-- <x-server.navbar :server="$server" :parameters="$parameters" /> --}} <x-server.navbar :server="$server" />
@if ($server->isFunctional()) <div class="flex flex-col h-full gap-8 sm:flex-row">
<h2>Log Drains</h2> <x-server.sidebar :server="$server" activeMenu="log-drains" />
<div class="pb-4">Sends service logs to 3rd party tools.</div> <div class="w-full">
<div class="flex flex-col gap-4 pt-4"> @if ($server->isFunctional())
<div class="p-4 border dark:border-coolgray-300"> <div class="flex gap-2 items-center">
<form wire:submit='submit("newrelic")' class="flex flex-col"> <h2>Log Drains</h2>
<h3>New Relic</h3> <x-loading wire:target="instantSave" wire:loading.delay />
<div class="w-32"> </div>
<x-forms.checkbox instantSave='instantSave("newrelic")' <div class="">Sends service logs to 3rd party tools.</div>
id="server.settings.is_logdrain_newrelic_enabled" label="Enabled" /> <div class="flex flex-col gap-4 pt-4">
</div> <div class="p-4 border dark:border-coolgray-300">
<div class="flex flex-col gap-4"> <form wire:submit='submit("newrelic")' class="flex flex-col">
<div class="flex flex-col w-full gap-2 xl:flex-row"> <h3>New Relic</h3>
@if ($server->isLogDrainEnabled()) <div class="w-32">
<x-forms.input disabled type="password" required @if ($isLogDrainAxiomEnabled || $isLogDrainCustomEnabled)
id="server.settings.logdrain_newrelic_license_key" label="License Key" /> <x-forms.checkbox disabled id="isLogDrainNewRelicEnabled" label="Enabled" />
<x-forms.input disabled required id="server.settings.logdrain_newrelic_base_uri" @else
placeholder="https://log-api.eu.newrelic.com/log/v1" <x-forms.checkbox instantSave id="isLogDrainNewRelicEnabled" label="Enabled" />
helper="For EU use: https://log-api.eu.newrelic.com/log/v1<br>For US use: https://log-api.newrelic.com/log/v1" @endif
label="Endpoint" /> </div>
<div class="flex flex-col gap-4">
<div class="flex flex-col w-full gap-2 xl:flex-row">
@if ($server->isLogDrainEnabled())
<x-forms.input disabled type="password" required id="logDrainNewRelicLicenseKey"
label="License Key" />
<x-forms.input disabled required id="logDrainNewRelicBaseUri"
placeholder="https://log-api.eu.newrelic.com/log/v1"
helper="For EU use: https://log-api.eu.newrelic.com/log/v1<br>For US use: https://log-api.newrelic.com/log/v1"
label="Endpoint" />
@else
<x-forms.input type="password" required id="logDrainNewRelicLicenseKey"
label="License Key" />
<x-forms.input required id="logDrainNewRelicBaseUri"
placeholder="https://log-api.eu.newrelic.com/log/v1"
helper="For EU use: https://log-api.eu.newrelic.com/log/v1<br>For US use: https://log-api.newrelic.com/log/v1"
label="Endpoint" />
@endif
</div>
</div>
<div class="flex justify-end gap-4 pt-6">
<x-forms.button type="submit">
Save
</x-forms.button>
</div>
</form>
<h3>Axiom</h3>
<div class="w-32">
@if ($isLogDrainNewRelicEnabled || $isLogDrainCustomEnabled)
<x-forms.checkbox disabled id="isLogDrainAxiomEnabled" label="Enabled" />
@else @else
<x-forms.input type="password" required <x-forms.checkbox instantSave id="isLogDrainAxiomEnabled" label="Enabled" />
id="server.settings.logdrain_newrelic_license_key" label="License Key" />
<x-forms.input required id="server.settings.logdrain_newrelic_base_uri"
placeholder="https://log-api.eu.newrelic.com/log/v1"
helper="For EU use: https://log-api.eu.newrelic.com/log/v1<br>For US use: https://log-api.newrelic.com/log/v1"
label="Endpoint" />
@endif @endif
</div> </div>
</div> <form wire:submit='submit("axiom")' class="flex flex-col">
<div class="flex justify-end gap-4 pt-6"> <div class="flex flex-col gap-4">
<x-forms.button type="submit"> <div class="flex flex-col w-full gap-2 xl:flex-row">
Save @if ($server->isLogDrainEnabled())
</x-forms.button> <x-forms.input disabled type="password" required id="logDrainAxiomApiKey"
</div> label="API Key" />
</form> <x-forms.input disabled required id="logDrainAxiomDatasetName"
label="Dataset Name" />
<h3>Axiom</h3> @else
<div class="w-32"> <x-forms.input type="password" required id="logDrainAxiomApiKey"
<x-forms.checkbox instantSave='instantSave("axiom")' id="server.settings.is_logdrain_axiom_enabled" label="API Key" />
label="Enabled" /> <x-forms.input required id="logDrainAxiomDatasetName" label="Dataset Name" />
</div> @endif
<form wire:submit='submit("axiom")' class="flex flex-col"> </div>
<div class="flex flex-col gap-4"> </div>
<div class="flex flex-col w-full gap-2 xl:flex-row"> <div class="flex justify-end gap-4 pt-6">
@if ($server->isLogDrainEnabled()) <x-forms.button type="submit">
<x-forms.input disabled type="password" required Save
id="server.settings.logdrain_axiom_api_key" label="API Key" /> </x-forms.button>
<x-forms.input disabled required id="server.settings.logdrain_axiom_dataset_name" </div>
label="Dataset Name" /> </form>
<h3>Custom FluentBit</h3>
<div class="w-32">
@if ($isLogDrainNewRelicEnabled || $isLogDrainAxiomEnabled)
<x-forms.checkbox disabled id="isLogDrainCustomEnabled" label="Enabled" />
@else @else
<x-forms.input type="password" required id="server.settings.logdrain_axiom_api_key" <x-forms.checkbox instantSave id="isLogDrainCustomEnabled" label="Enabled" />
label="API Key" />
<x-forms.input required id="server.settings.logdrain_axiom_dataset_name"
label="Dataset Name" />
@endif @endif
</div> </div>
</div> <form wire:submit='submit("custom")' class="flex flex-col">
<div class="flex justify-end gap-4 pt-6"> <div class="flex flex-col gap-4">
<x-forms.button type="submit"> @if ($server->isLogDrainEnabled())
Save <x-forms.textarea disabled rows="6" required id="logDrainCustomConfig"
</x-forms.button> label="Custom FluentBit Configuration" />
</div> <x-forms.textarea disabled id="logDrainCustomConfigParser"
</form> label="Custom Parser Configuration" />
{{-- <h3>Highlight.io</h3> @else
<div class="w-32"> <x-forms.textarea rows="6" required id="logDrainCustomConfig"
<x-forms.checkbox instantSave='instantSave("highlight")' label="Custom FluentBit Configuration" />
id="server.settings.is_logdrain_highlight_enabled" label="Enabled" /> <x-forms.textarea id="logDrainCustomConfigParser"
</div> label="Custom Parser Configuration" />
<form wire:submit='submit("highlight")' class="flex flex-col"> @endif
<div class="flex flex-col gap-4">
<div class="flex flex-col w-full gap-2 xl:flex-row"> </div>
<x-forms.input type="password" required id="server.settings.logdrain_highlight_project_id" <div class="flex justify-end gap-4 pt-6">
label="Project Id" /> <x-forms.button type="submit">
</div> Save
</div> </x-forms.button>
<div class="flex justify-end gap-4 pt-6"> </div>
<x-forms.button type="submit"> </form>
Save
</x-forms.button>
</div>
</form> --}}
<h3>Custom FluentBit configuration</h3>
<div class="w-32">
<x-forms.checkbox instantSave='instantSave("custom")'
id="server.settings.is_logdrain_custom_enabled" label="Enabled" />
</div>
<form wire:submit='submit("custom")' class="flex flex-col">
<div class="flex flex-col gap-4">
@if ($server->isLogDrainEnabled())
<x-forms.textarea disabled rows="6" required
id="server.settings.logdrain_custom_config" label="Custom FluentBit Configuration" />
<x-forms.textarea disabled id="server.settings.logdrain_custom_config_parser"
label="Custom Parser Configuration" />
@else
<x-forms.textarea rows="6" required id="server.settings.logdrain_custom_config"
label="Custom FluentBit Configuration" />
<x-forms.textarea id="server.settings.logdrain_custom_config_parser"
label="Custom Parser Configuration" />
@endif
</div> </div>
<div class="flex justify-end gap-4 pt-6"> </div>
<x-forms.button type="submit"> @else
Save <div>Server is not validated. Validate first.</div>
</x-forms.button> @endif
</div>
</form>
</div>
</div> </div>
@else </div>
<div>Server is not validated. Validate first.</div>
@endif
</div> </div>

View File

@@ -1,6 +1,43 @@
<div> <div>
<x-slot:title> <x-slot:title>
Server Connection | Coolify {{ data_get_str($server, 'name')->limit(10) }} > Server Connection | Coolify
</x-slot> </x-slot>
<livewire:server.show-private-key :server="$server" :privateKeys="$privateKeys" /> <x-server.navbar :server="$server" />
<div class="flex flex-col h-full gap-8 sm:flex-row">
<x-server.sidebar :server="$server" activeMenu="private-key" />
<div class="w-full">
<div class="flex items-end gap-2">
<h2>Private Key</h2>
<x-modal-input buttonTitle="+ Add" title="New Private Key">
<livewire:security.private-key.create />
</x-modal-input>
<x-forms.button isHighlighted wire:click.prevent='checkConnection'>
Check connection
</x-forms.button>
</div>
<div class="pb-4">Change your server's private key.</div>
<div class="grid xl:grid-cols-2 grid-cols-1 gap-2">
@forelse ($privateKeys as $private_key)
<div
class="box-without-bg justify-between dark:bg-coolgray-100 bg-white items-center flex flex-col gap-2">
<div class="flex flex-col w-full">
<div class="box-title">{{ $private_key->name }}</div>
<div class="box-description">{{ $private_key->description }}</div>
</div>
@if (data_get($server, 'privateKey.uuid') !== $private_key->uuid)
<x-forms.button class="w-full" wire:click='setPrivateKey({{ $private_key->id }})'>
Use this key
</x-forms.button>
@else
<x-forms.button class="w-full" disabled>
Currently used
</x-forms.button>
@endif
</div>
@empty
<div>No private keys found. </div>
@endforelse
</div>
</div>
</div>
</div> </div>

View File

@@ -4,7 +4,7 @@
</x-slot> </x-slot>
<x-server.navbar :server="$server" :parameters="$parameters" /> <x-server.navbar :server="$server" :parameters="$parameters" />
<div class="flex flex-col h-full gap-8 sm:flex-row"> <div class="flex flex-col h-full gap-8 sm:flex-row">
<x-server.sidebar :server="$server" :parameters="$parameters" /> <x-server.sidebar-proxy :server="$server" :parameters="$parameters" />
<div class="w-full"> <div class="w-full">
@if ($server->isFunctional()) @if ($server->isFunctional())
<div class="flex gap-2"> <div class="flex gap-2">

View File

@@ -4,7 +4,7 @@
</x-slot> </x-slot>
<x-server.navbar :server="$server" :parameters="$parameters" /> <x-server.navbar :server="$server" :parameters="$parameters" />
<div class="flex flex-col h-full gap-8 sm:flex-row"> <div class="flex flex-col h-full gap-8 sm:flex-row">
<x-server.sidebar :server="$server" :parameters="$parameters" /> <x-server.sidebar-proxy :server="$server" :parameters="$parameters" />
<div class="w-full"> <div class="w-full">
<h2 class="pb-4">Logs</h2> <h2 class="pb-4">Logs</h2>
<livewire:project.shared.get-logs :server="$server" container="coolify-proxy" /> <livewire:project.shared.get-logs :server="$server" container="coolify-proxy" />

View File

@@ -5,7 +5,7 @@
<x-server.navbar :server="$server" :parameters="$parameters" /> <x-server.navbar :server="$server" :parameters="$parameters" />
@if ($server->isFunctional()) @if ($server->isFunctional())
<div class="flex flex-col h-full gap-8 sm:flex-row"> <div class="flex flex-col h-full gap-8 sm:flex-row">
<x-server.sidebar :server="$server" :parameters="$parameters" /> <x-server.sidebar-proxy :server="$server" :parameters="$parameters" />
<div class="w-full"> <div class="w-full">
<livewire:server.proxy :server="$server" /> <livewire:server.proxy :server="$server" />
</div> </div>

View File

@@ -1,36 +0,0 @@
<div>
<div class="flex items-end gap-2">
<h2>Private Key</h2>
<x-modal-input buttonTitle="+ Add" title="New Private Key">
<livewire:security.private-key.create />
</x-modal-input>
<x-forms.button wire:click.prevent='checkConnection'>
Check connection
</x-forms.button>
</div>
<div class="flex flex-col gap-2">
<div class="pb-4">Change your server's private key.</div>
</div>
<div class="grid xl:grid-cols-2 grid-cols-1 gap-2">
@forelse ($privateKeys as $private_key)
<div class="box-without-bg justify-between dark:bg-coolgray-100 bg-white items-center flex flex-col gap-2">
<div class="flex flex-col w-full">
<div class="box-title">{{ $private_key->name }}</div>
<div class="box-description">{{ $private_key->description }}</div>
</div>
@if (data_get($server, 'privateKey.uuid') !== $private_key->uuid)
<x-forms.button class="w-full" wire:click='setPrivateKey({{ $private_key->id }})'>
Use this key
</x-forms.button>
@else
<x-forms.button class="w-full" disabled>
Currently used
</x-forms.button>
@endif
</div>
@empty
<div>No private keys found. </div>
@endforelse
</div>
</div>

View File

@@ -2,71 +2,237 @@
<x-slot:title> <x-slot:title>
{{ 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" />
<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 h-full gap-8 sm:flex-row">
<div class="flex flex-col items-start gap-2 min-w-fit"> <x-server.sidebar :server="$server" activeMenu="general" />
<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 === '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 class="w-full">
<div x-cloak x-show="activeTab === 'general'" class="h-full"> <form wire:submit.prevent='submit' class="flex flex-col">
<livewire:server.form :server="$server" /> <div class="flex gap-2">
</div> <h2>General</h2>
<div x-cloak x-show="activeTab === 'advanced'" class="h-full"> @if ($server->id === 0)
<livewire:server.advanced :server="$server" /> <x-modal-confirmation title="Confirm Server Settings Change?" buttonTitle="Save"
</div> submitAction="submit" :actions="[
<div x-cloak x-show="activeTab === 'private-key'" class="h-full"> 'If you missconfigure the server, you could lose a lot of functionalities of Coolify.',
<livewire:server.private-key.show :server="$server" /> ]" :confirmWithText="false" :confirmWithPassword="false"
</div> step2ButtonText="Save" />
<div x-cloak x-show="activeTab === 'cloudflare-tunnels'" class="h-full"> @else
<livewire:server.cloudflare-tunnels :server="$server" /> <x-forms.button type="submit">Save</x-forms.button>
</div> @if ($server->isFunctional())
<div x-cloak x-show="activeTab === 'destinations'" class="h-full"> <x-slide-over closeWithX fullScreen>
<livewire:server.destination.show :server="$server" /> <x-slot:title>Validate & configure</x-slot:title>
</div> <x-slot:content>
<div x-cloak x-show="activeTab === 'log-drains'" class="h-full"> <livewire:server.validate-and-install :server="$server" ask />
<livewire:server.log-drains :server="$server" /> </x-slot:content>
</div> <x-forms.button @click="slideOverOpen=true" wire:click.prevent='validateServer'
<div x-cloak x-show="activeTab === 'metrics'" class="h-full"> isHighlighted>
@if ($server->isFunctional() && $server->isMetricsEnabled()) Revalidate server
<h2>Metrics</h2> </x-forms.button>
<div class="pb-4">Basic metrics for your container.</div> </x-slide-over>
<div> @endif
<livewire:server.charts :server="$server" /> @endif
</div>
@else
No metrics available.
@endif
</div>
@if (!$server->isLocalhost())
<div x-cloak x-show="activeTab === 'danger'" class="h-full">
<livewire:server.delete :server="$server" />
</div> </div>
@if ($server->isFunctional())
Server is reachable and validated.
@else
You can't use this server until it is validated.
@endif
@if ((!$isReachable || !$isUsable) && $server->id !== 0)
<x-slide-over closeWithX fullScreen>
<x-slot:title>Validate & configure</x-slot:title>
<x-slot:content>
<livewire:server.validate-and-install :server="$server" />
</x-slot:content>
<x-forms.button @click="slideOverOpen=true"
class="mt-8 mb-4 w-full font-bold box-without-bg bg-coollabs hover:bg-coollabs-100"
wire:click.prevent='validateServer' isHighlighted>
Validate Server & Install Docker Engine
</x-forms.button>
</x-slide-over>
@if ($server->validation_logs)
<h4>Previous Validation Logs</h4>
<div class="pb-8">
{!! $server->validation_logs !!}
</div>
@endif
@endif
@if ((!$isReachable || !$isUsable) && $server->id === 0)
<x-forms.button class="mt-8 mb-4 font-bold box-without-bg bg-coollabs hover:bg-coollabs-100"
wire:click.prevent='checkLocalhostConnection' isHighlighted>
Validate Server
</x-forms.button>
@endif
@if ($server->isForceDisabled() && isCloud())
<div class="pt-4 font-bold text-red-500">The system has disabled the server because you have
exceeded the
number of servers for which you have paid.</div>
@endif
<div class="flex flex-col gap-2 pt-4">
<div class="flex flex-col gap-2 w-full lg:flex-row">
<x-forms.input id="name" label="Name" required />
<x-forms.input id="description" label="Description" />
@if (!$isSwarmWorker && !$isBuildServer)
<x-forms.input placeholder="https://example.com" id="wildcard_domain"
label="Wildcard Domain"
helper='A wildcard domain allows you to receive a randomly generated domain for your new applications. <br><br>For instance, if you set "https://example.com" as your wildcard domain, your applications will receive domains like "https://randomId.example.com".' />
@endif
</div>
<div class="flex flex-col gap-2 w-full lg:flex-row">
<x-forms.input type="password" id="ip" label="IP Address/Domain"
helper="An IP Address (127.0.0.1) or domain (example.com). Make sure there is no protocol like http(s):// so you provide a FQDN not a URL."
required />
<div class="flex gap-2">
<x-forms.input id="user" label="User" required />
<x-forms.input type="number" id="port" label="Port" required />
</div>
</div>
<div class="w-full" x-data="{
open: false,
search: '{{ $serverTimezone ?: '' }}',
timezones: @js($timezones),
placeholder: '{{ $serverTimezone ? 'Search timezone...' : 'Select Server Timezone' }}',
init() {
this.$watch('search', value => {
if (value === '') {
this.open = true;
}
})
}
}">
<div class="flex items-center mb-1">
<label for="serverTimezone">Server
Timezone</label>
<x-helper class="ml-2"
helper="Server's timezone. This is used for backups, cron jobs, etc." />
</div>
<div class="relative">
<div class="inline-flex relative items-center w-64">
<input autocomplete="off"
wire:dirty.class.remove='dark:focus:ring-coolgray-300 dark:ring-coolgray-300'
wire:dirty.class="dark:focus:ring-warning dark:ring-warning" x-model="search"
@focus="open = true" @click.away="open = false" @input="open = true"
class="w-full input" :placeholder="placeholder"
wire:model.debounce.300ms="serverTimezone">
<svg class="absolute right-0 mr-2 w-4 h-4" xmlns="http://www.w3.org/2000/svg"
fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
@click="open = true">
<path stroke-linecap="round" stroke-linejoin="round"
d="M8.25 15L12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9" />
</svg>
</div>
<div x-show="open"
class="overflow-auto overflow-x-hidden absolute z-50 mt-1 w-64 max-h-60 bg-white rounded-md border shadow-lg dark:bg-coolgray-100 dark:border-coolgray-200 scrollbar">
<template
x-for="timezone in timezones.filter(tz => tz.toLowerCase().includes(search.toLowerCase()))"
:key="timezone">
<div @click="search = timezone; open = false; $wire.set('server.settings.server_timezone', timezone)"
class="px-4 py-2 text-gray-800 cursor-pointer hover:bg-gray-100 dark:hover:bg-coolgray-300 dark:text-gray-200"
x-text="timezone"></div>
</template>
</div>
</div>
</div>
<div class="w-full">
@if (!$server->isLocalhost())
<div class="w-96">
<x-forms.checkbox instantSave id="isBuildServer" label="Use it as a build server?" />
</div>
@if (!$server->isBuildServer() && !$server->settings->is_cloudflare_tunnel)
<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'
href='https://coolify.io/docs/knowledge-base/docker/swarm'
target='_blank'>here</a>.
</div>
<div class="w-96">
@if ($server->settings->is_swarm_worker)
<x-forms.checkbox disabled instantSave type="checkbox" id="isSwarmManager"
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 Manager?" />
@else
<x-forms.checkbox instantSave type="checkbox" id="isSwarmManager"
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 Manager?" />
@endif
@if ($server->settings->is_swarm_manager)
<x-forms.checkbox disabled instantSave type="checkbox" id="isSwarmWorker"
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?" />
@else
<x-forms.checkbox instantSave type="checkbox" id="isSwarmWorker"
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?" />
@endif
</div>
@endif
@endif
</div>
</div>
</form>
@if ($server->isFunctional() && !$server->isSwarm() && !$server->isBuildServer())
<form wire:submit.prevent='submit'>
<div class="flex gap-2 items-center pt-4 pb-2">
<h3>Sentinel</h3>
@if ($server->isSentinelEnabled())
<div class="flex gap-2 items-center">
@if ($server->isSentinelLive())
<x-status.running status="In sync" noLoading title="{{ $sentinelUpdatedAt }}" />
<x-forms.button type="submit">Save</x-forms.button>
<x-forms.button wire:click='restartSentinel'>Restart</x-forms.button>
@else
<x-status.stopped status="Out of sync" noLoading
title="{{ $sentinelUpdatedAt }}" />
<x-forms.button type="submit">Save</x-forms.button>
<x-forms.button wire:click='restartSentinel'>Sync</x-forms.button>
@endif
</div>
@endif
</div>
<div class="flex flex-col gap-2">
<div class="flex gap-2">Experimental feature <x-helper
helper="Sentinel reports your server's & container's health and collects metrics." />
</div>
<div class="w-64">
<x-forms.checkbox wire:model.live="isSentinelEnabled" label="Enable Sentinel" />
@if ($server->isSentinelEnabled())
<x-forms.checkbox id="isSentinelDebugEnabled" label="Enable Sentinel Debug"
instantSave />
<x-forms.checkbox instantSave id="isMetricsEnabled" label="Enable Metrics" />
@else
<x-forms.checkbox id="isSentinelDebugEnabled" label="Enable Sentinel Debug" disabled
instantSave />
<x-forms.checkbox instantSave disabled id="isMetricsEnabled" label="Enable Metrics" />
label="Enable Metrics" />
@endif
</div>
@if ($server->isSentinelEnabled())
<div class="flex flex-wrap gap-2 sm:flex-nowrap items-end">
<x-forms.input type="password" id="sentinelToken" label="Sentinel token" required
helper="Token for Sentinel." />
<x-forms.button wire:click="regenerateSentinelToken">Regenerate</x-forms.button>
</div>
<x-forms.input id="sentinelCustomUrl" 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-wrap gap-2 sm:flex-nowrap">
<x-forms.input id="sentinelMetricsRefreshRateSeconds"
label="Metrics rate (seconds)" required
helper="The interval for gathering metrics. Lower means more disk space will be used." />
<x-forms.input id="sentinelMetricsHistoryDays" label="Metrics history (days)"
required helper="How many days should the metrics data should be reserved." />
<x-forms.input id="sentinelPushIntervalSeconds" label="Push interval (seconds)"
required
helper="How many seconds should the metrics data should be pushed to the collector." />
</div>
</div>
@endif
</div>
</form>
@endif @endif
</div> </div>
</div> </div>

View File

@@ -34,6 +34,10 @@ use App\Livewire\Project\Show as ProjectShow;
use App\Livewire\Security\ApiTokens; use App\Livewire\Security\ApiTokens;
use App\Livewire\Security\PrivateKey\Index as SecurityPrivateKeyIndex; use App\Livewire\Security\PrivateKey\Index as SecurityPrivateKeyIndex;
use App\Livewire\Security\PrivateKey\Show as SecurityPrivateKeyShow; use App\Livewire\Security\PrivateKey\Show as SecurityPrivateKeyShow;
use App\Livewire\Server\Advanced as ServerAdvanced;
use App\Livewire\Server\Charts as ServerCharts;
use App\Livewire\Server\CloudflareTunnels;
use App\Livewire\Server\Delete as DeleteServer;
use App\Livewire\Server\Destination\Show as DestinationShow; use App\Livewire\Server\Destination\Show as DestinationShow;
use App\Livewire\Server\Index as ServerIndex; use App\Livewire\Server\Index as ServerIndex;
use App\Livewire\Server\LogDrains; use App\Livewire\Server\LogDrains;
@@ -205,13 +209,17 @@ Route::middleware(['auth', 'verified'])->group(function () {
Route::prefix('server/{server_uuid}')->group(function () { Route::prefix('server/{server_uuid}')->group(function () {
Route::get('/', ServerShow::class)->name('server.show'); Route::get('/', ServerShow::class)->name('server.show');
Route::get('/advanced', ServerAdvanced::class)->name('server.advanced');
Route::get('/private-key', PrivateKeyShow::class)->name('server.private-key');
Route::get('/resources', ResourcesShow::class)->name('server.resources'); Route::get('/resources', ResourcesShow::class)->name('server.resources');
Route::get('/cloudflare-tunnels', CloudflareTunnels::class)->name('server.cloudflare-tunnels');
Route::get('/destinations', DestinationShow::class)->name('server.destinations');
Route::get('/log-drains', LogDrains::class)->name('server.log-drains');
Route::get('/metrics', ServerCharts::class)->name('server.charts');
Route::get('/danger', DeleteServer::class)->name('server.delete');
Route::get('/proxy', ProxyShow::class)->name('server.proxy'); Route::get('/proxy', ProxyShow::class)->name('server.proxy');
Route::get('/proxy/dynamic', ProxyDynamicConfigurations::class)->name('server.proxy.dynamic-confs'); Route::get('/proxy/dynamic', ProxyDynamicConfigurations::class)->name('server.proxy.dynamic-confs');
Route::get('/proxy/logs', ProxyLogs::class)->name('server.proxy.logs'); Route::get('/proxy/logs', ProxyLogs::class)->name('server.proxy.logs');
Route::get('/private-key', PrivateKeyShow::class)->name('server.private-key');
Route::get('/destinations', DestinationShow::class)->name('server.destinations');
Route::get('/log-drains', LogDrains::class)->name('server.log-drains');
Route::get('/terminal', ExecuteContainerCommand::class)->name('server.command'); Route::get('/terminal', ExecuteContainerCommand::class)->name('server.command');
}); });