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 stopped.');
return;
}
$this->dispatch('serverRefresh');
$this->dispatch('success', 'Log drain service started.'); $this->dispatch('success', 'Log drain service started.');
} catch (\Throwable $e) { } else {
return handleError($e, $this); StopLogDrain::run($this->server);
$this->dispatch('success', 'Log drain service stopped.');
} }
}
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> </a>
<a class="{{ request()->routeIs('server.proxy.logs') ? 'menu-item menu-item-active' : 'menu-item' }}" @if ($server->isFunctional())
href="{{ route('server.proxy.logs', $parameters) }}"> <a class="menu-item {{ $activeMenu === 'cloudflare-tunnels' ? 'menu-item-active' : '' }}"
<button>Logs</button> 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>
</div> <a class="menu-item {{ $activeMenu === 'log-drains' ? 'menu-item-active' : '' }}"
@endif href="{{ route('server.log-drains', ['server_uuid' => $server->uuid]) }}" wire:navigate>Log
Drains</a>
<a class="menu-item {{ $activeMenu === 'metrics' ? 'menu-item-active' : '' }}"
href="{{ route('server.charts', ['server_uuid' => $server->uuid]) }}">Metrics</a>
@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,18 +1,22 @@
<form wire:submit='submit'> <div>
<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">
<x-server.sidebar :server="$server" activeMenu="advanced" />
<form wire:submit='submit' class="w-full">
<div> <div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h2>Advanced</h2> <h2>Advanced</h2>
<x-forms.button type="submit">Save</x-forms.button> <x-forms.button type="submit">Save</x-forms.button>
<x-modal-confirmation title="Confirm Docker Cleanup?" buttonTitle="Trigger Manual Cleanup" <x-modal-confirmation title="Confirm Docker Cleanup?" buttonTitle="Trigger Manual Cleanup"
submitAction="manualCleanup" :actions="[ isHighlightedButton submitAction="manualCleanup" :actions="[
'Permanently deletes all stopped containers managed by Coolify (as containers are non-persistent, no data will be lost)', 'Permanently deletes all stopped containers managed by Coolify (as containers are non-persistent, no data will be lost)',
'Permanently deletes all unused images', 'Permanently deletes all unused images',
'Clears build cache', 'Clears build cache',
'Removes old versions of the Coolify helper image', 'Removes old versions of the Coolify helper image',
'Optionally permanently deletes all unused volumes (if enabled in advanced options).', 'Optionally permanently deletes all unused volumes (if enabled in advanced options).',
'Optionally permanently deletes all unused networks (if enabled in advanced options).', 'Optionally permanently deletes all unused networks (if enabled in advanced options).',
]" :confirmWithText="false" :confirmWithPassword="false" ]" :confirmWithText="false"
step2ButtonText="Trigger Docker Cleanup" /> :confirmWithPassword="false" step2ButtonText="Trigger Docker Cleanup" />
</div> </div>
<div>Advanced configuration for your server.</div> <div>Advanced configuration for your server.</div>
</div> </div>
@@ -20,7 +24,7 @@
<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>
@@ -30,13 +34,12 @@
<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">
@@ -50,7 +53,7 @@
<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>
@@ -61,14 +64,14 @@
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>
@@ -82,11 +85,13 @@
<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> </div>
</form> </form>
</div>
</div>

View File

@@ -1,4 +1,14 @@
<div @if ($poll) wire:poll.5000ms='pollData' @endif x-init="$wire.loadData()"> <div>
<x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > Server Destinations | Coolify
</x-slot>
<x-server.navbar :server="$server" />
<div class="flex flex-col h-full gap-8 sm:flex-row">
<x-server.sidebar :server="$server" activeMenu="metrics" />
<div class="w-full">
<h2>Metrics</h2>
<div class="pb-4">Basic metrics for your container.</div>
<div @if ($poll) wire:poll.5000ms='pollData' @endif x-init="$wire.loadData()">
<x-forms.select label="Interval" wire:change="setInterval" id="interval"> <x-forms.select label="Interval" wire:change="setInterval" id="interval">
<option value="5">5 minutes (live)</option> <option value="5">5 minutes (live)</option>
<option value="10">10 minutes (live)</option> <option value="10">10 minutes (live)</option>
@@ -237,4 +247,7 @@
</script> </script>
</div> </div>
</div>
</div>
</div>
</div> </div>

View File

@@ -1,14 +1,24 @@
<div> <div>
<x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > Cloudflare Tunnels | Coolify
</x-slot>
<x-server.navbar :server="$server" />
<div class="flex flex-col h-full gap-8 sm:flex-row">
<x-server.sidebar :server="$server" activeMenu="cloudflare-tunnels" />
<div class="w-full">
<div class="flex flex-col">
<div class="flex gap-1 items-center"> <div class="flex gap-1 items-center">
<h2>Cloudflare Tunnels</h2> <h2>Cloudflare Tunnels</h2>
<x-helper class="inline-flex" <x-helper class="inline-flex"
helper="If you are using Cloudflare Tunnels, enable this. It will proxy all SSH requests to your server through Cloudflare.<br> You then can close your server's SSH port in the firewall of your hosting provider.<br><span class='dark:text-warning'>If you choose manual configuration, Coolify does not install or set up Cloudflare (cloudflared) on your server.</span>" /> 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>
<div>Secure your servers with Cloudflare Tunnels</div>
</div>
<div class="flex flex-col gap-2 pt-6"> <div class="flex flex-col gap-2 pt-6">
@if ($server->settings->is_cloudflare_tunnel) @if ($isCloudflareTunnelsEnabled)
<div class="w-64"> <div class="w-64">
<x-forms.checkbox instantSave id="server.settings.is_cloudflare_tunnel" <x-forms.checkbox instantSave id="isCloudflareTunnelsEnabled" label="Enabled" />
label="Enabled" />
</div> </div>
@elseif (!$server->isFunctional()) @elseif (!$server->isFunctional())
<div <div
@@ -18,25 +28,27 @@
domain configured. domain configured.
<br /> <br />
To <span class="font-semibold">manually</span> configure Cloudflare Tunnels, please To <span class="font-semibold">manually</span> configure Cloudflare Tunnels, please
click <span wire:click="manualCloudflareConfig" click <span wire:click="manualCloudflareConfig" class="underline cursor-pointer">here</span>,
class="underline cursor-pointer">here</span>, then you should validate the server. then you should validate the server.
<br /><br /> <br /><br />
For more information, please read our <a For more information, please read our <a
href="https://coolify.io/docs/knowledge-base/cloudflare/tunnels/" target="_blank" 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>. class="font-medium underline hover:text-yellow-600 dark:hover:text-yellow-200">documentation</a>.
</div> </div>
@endif @endif
@if (!$server->settings->is_cloudflare_tunnel && $server->isFunctional()) @if (!$isCloudflareTunnelsEnabled && $server->isFunctional())
<x-modal-input buttonTitle="Automated Configuration" title="Cloudflare Tunnels" <h4>Configuration</h4>
class="w-full" :closeOutside="false"> <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" /> <livewire:server.configure-cloudflare-tunnels :server_id="$server->id" />
</x-modal-input> </x-modal-input>
@endif <x-forms.button wire:click="manualCloudflareConfig" class="w-20">
@if ($server->isFunctional() && !$server->settings->is_cloudflare_tunnel) Manual
<div wire:click="manualCloudflareConfig" class="w-full underline cursor-pointer"> </x-forms.button>
I have configured Cloudflare Tunnels manually
</div> </div>
@endif @endif
</div>
</div>
</div> </div>
</div> </div>

View File

@@ -1,4 +1,11 @@
<div> <div>
<x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > Delete Server | Coolify
</x-slot>
<x-server.navbar :server="$server" />
<div class="flex flex-col h-full gap-8 sm:flex-row">
<x-server.sidebar :server="$server" activeMenu="danger" />
<div class="w-full">
@if ($server->id !== 0) @if ($server->id !== 0)
<h2>Danger Zone</h2> <h2>Danger Zone</h2>
<div class="">Woah. I hope you know what are you doing.</div> <div class="">Woah. I hope you know what are you doing.</div>
@@ -8,8 +15,8 @@
</div> </div>
@if ($server->definedResources()->count() > 0) @if ($server->definedResources()->count() > 0)
<div class="pb-2 text-red-500">You need to delete all resources before deleting this server.</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" <x-modal-confirmation title="Confirm Server Deletion?" isErrorButton buttonTitle="Delete"
:actions="['This server will be permanently deleted.']" confirmationText="{{ $server->name }}" 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" confirmationLabel="Please confirm the execution of the actions by entering the Server Name below"
shortConfirmationLabel="Server Name" step3ButtonText="Permanently Delete" /> shortConfirmationLabel="Server Name" step3ButtonText="Permanently Delete" />
@else @else
@@ -19,4 +26,6 @@
shortConfirmationLabel="Server Name" step3ButtonText="Permanently Delete" /> shortConfirmationLabel="Server Name" step3ButtonText="Permanently Delete" />
@endif @endif
@endif @endif
</div>
</div>
</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,32 +1,41 @@
<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" />
<div class="flex flex-col h-full gap-8 sm:flex-row">
<x-server.sidebar :server="$server" activeMenu="log-drains" />
<div class="w-full">
@if ($server->isFunctional()) @if ($server->isFunctional())
<div class="flex gap-2 items-center">
<h2>Log Drains</h2> <h2>Log Drains</h2>
<div class="pb-4">Sends service logs to 3rd party tools.</div> <x-loading wire:target="instantSave" wire:loading.delay />
</div>
<div class="">Sends service logs to 3rd party tools.</div>
<div class="flex flex-col gap-4 pt-4"> <div class="flex flex-col gap-4 pt-4">
<div class="p-4 border dark:border-coolgray-300"> <div class="p-4 border dark:border-coolgray-300">
<form wire:submit='submit("newrelic")' class="flex flex-col"> <form wire:submit='submit("newrelic")' class="flex flex-col">
<h3>New Relic</h3> <h3>New Relic</h3>
<div class="w-32"> <div class="w-32">
<x-forms.checkbox instantSave='instantSave("newrelic")' @if ($isLogDrainAxiomEnabled || $isLogDrainCustomEnabled)
id="server.settings.is_logdrain_newrelic_enabled" label="Enabled" /> <x-forms.checkbox disabled id="isLogDrainNewRelicEnabled" label="Enabled" />
@else
<x-forms.checkbox instantSave id="isLogDrainNewRelicEnabled" label="Enabled" />
@endif
</div> </div>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<div class="flex flex-col w-full gap-2 xl:flex-row"> <div class="flex flex-col w-full gap-2 xl:flex-row">
@if ($server->isLogDrainEnabled()) @if ($server->isLogDrainEnabled())
<x-forms.input disabled type="password" required <x-forms.input disabled type="password" required id="logDrainNewRelicLicenseKey"
id="server.settings.logdrain_newrelic_license_key" label="License Key" /> label="License Key" />
<x-forms.input disabled required id="server.settings.logdrain_newrelic_base_uri" <x-forms.input disabled required id="logDrainNewRelicBaseUri"
placeholder="https://log-api.eu.newrelic.com/log/v1" 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" 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" /> label="Endpoint" />
@else @else
<x-forms.input type="password" required <x-forms.input type="password" required id="logDrainNewRelicLicenseKey"
id="server.settings.logdrain_newrelic_license_key" label="License Key" /> label="License Key" />
<x-forms.input required id="server.settings.logdrain_newrelic_base_uri" <x-forms.input required id="logDrainNewRelicBaseUri"
placeholder="https://log-api.eu.newrelic.com/log/v1" 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" 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" /> label="Endpoint" />
@@ -42,22 +51,24 @@
<h3>Axiom</h3> <h3>Axiom</h3>
<div class="w-32"> <div class="w-32">
<x-forms.checkbox instantSave='instantSave("axiom")' id="server.settings.is_logdrain_axiom_enabled" @if ($isLogDrainNewRelicEnabled || $isLogDrainCustomEnabled)
label="Enabled" /> <x-forms.checkbox disabled id="isLogDrainAxiomEnabled" label="Enabled" />
@else
<x-forms.checkbox instantSave id="isLogDrainAxiomEnabled" label="Enabled" />
@endif
</div> </div>
<form wire:submit='submit("axiom")' class="flex flex-col"> <form wire:submit='submit("axiom")' class="flex flex-col">
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<div class="flex flex-col w-full gap-2 xl:flex-row"> <div class="flex flex-col w-full gap-2 xl:flex-row">
@if ($server->isLogDrainEnabled()) @if ($server->isLogDrainEnabled())
<x-forms.input disabled type="password" required <x-forms.input disabled type="password" required id="logDrainAxiomApiKey"
id="server.settings.logdrain_axiom_api_key" label="API Key" /> label="API Key" />
<x-forms.input disabled required id="server.settings.logdrain_axiom_dataset_name" <x-forms.input disabled required id="logDrainAxiomDatasetName"
label="Dataset Name" /> label="Dataset Name" />
@else @else
<x-forms.input type="password" required id="server.settings.logdrain_axiom_api_key" <x-forms.input type="password" required id="logDrainAxiomApiKey"
label="API Key" /> label="API Key" />
<x-forms.input required id="server.settings.logdrain_axiom_dataset_name" <x-forms.input required id="logDrainAxiomDatasetName" label="Dataset Name" />
label="Dataset Name" />
@endif @endif
</div> </div>
</div> </div>
@@ -67,40 +78,25 @@
</x-forms.button> </x-forms.button>
</div> </div>
</form> </form>
{{-- <h3>Highlight.io</h3> <h3>Custom FluentBit</h3>
<div class="w-32"> <div class="w-32">
<x-forms.checkbox instantSave='instantSave("highlight")' @if ($isLogDrainNewRelicEnabled || $isLogDrainAxiomEnabled)
id="server.settings.is_logdrain_highlight_enabled" label="Enabled" /> <x-forms.checkbox disabled id="isLogDrainCustomEnabled" label="Enabled" />
</div> @else
<form wire:submit='submit("highlight")' class="flex flex-col"> <x-forms.checkbox instantSave id="isLogDrainCustomEnabled" label="Enabled" />
<div class="flex flex-col gap-4"> @endif
<div class="flex flex-col w-full gap-2 xl:flex-row">
<x-forms.input type="password" required id="server.settings.logdrain_highlight_project_id"
label="Project Id" />
</div>
</div>
<div class="flex justify-end gap-4 pt-6">
<x-forms.button type="submit">
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> </div>
<form wire:submit='submit("custom")' class="flex flex-col"> <form wire:submit='submit("custom")' class="flex flex-col">
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
@if ($server->isLogDrainEnabled()) @if ($server->isLogDrainEnabled())
<x-forms.textarea disabled rows="6" required <x-forms.textarea disabled rows="6" required id="logDrainCustomConfig"
id="server.settings.logdrain_custom_config" label="Custom FluentBit Configuration" /> label="Custom FluentBit Configuration" />
<x-forms.textarea disabled id="server.settings.logdrain_custom_config_parser" <x-forms.textarea disabled id="logDrainCustomConfigParser"
label="Custom Parser Configuration" /> label="Custom Parser Configuration" />
@else @else
<x-forms.textarea rows="6" required id="server.settings.logdrain_custom_config" <x-forms.textarea rows="6" required id="logDrainCustomConfig"
label="Custom FluentBit Configuration" /> label="Custom FluentBit Configuration" />
<x-forms.textarea id="server.settings.logdrain_custom_config_parser" <x-forms.textarea id="logDrainCustomConfigParser"
label="Custom Parser Configuration" /> label="Custom Parser Configuration" />
@endif @endif
@@ -117,4 +113,6 @@
@else @else
<div>Server is not validated. Validate first.</div> <div>Server is not validated. Validate first.</div>
@endif @endif
</div>
</div>
</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,72 +2,238 @@
<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">
<livewire:server.cloudflare-tunnels :server="$server" />
</div>
<div x-cloak x-show="activeTab === 'destinations'" class="h-full">
<livewire:server.destination.show :server="$server" />
</div>
<div x-cloak x-show="activeTab === 'log-drains'" class="h-full">
<livewire:server.log-drains :server="$server" />
</div>
<div x-cloak x-show="activeTab === 'metrics'" class="h-full">
@if ($server->isFunctional() && $server->isMetricsEnabled())
<h2>Metrics</h2>
<div class="pb-4">Basic metrics for your container.</div>
<div>
<livewire:server.charts :server="$server" />
</div>
@else @else
No metrics available. <x-forms.button type="submit">Save</x-forms.button>
@if ($server->isFunctional())
<x-slide-over closeWithX fullScreen>
<x-slot:title>Validate & configure</x-slot:title>
<x-slot:content>
<livewire:server.validate-and-install :server="$server" ask />
</x-slot:content>
<x-forms.button @click="slideOverOpen=true" wire:click.prevent='validateServer'
isHighlighted>
Revalidate server
</x-forms.button>
</x-slide-over>
@endif
@endif @endif
</div> </div>
@if (!$server->isLocalhost()) @if ($server->isFunctional())
<div x-cloak x-show="activeTab === 'danger'" class="h-full"> Server is reachable and validated.
<livewire:server.delete :server="$server" /> @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> </div>
@endif @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
</div> </div>
</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');
}); });