Merge branch 'next' into shadow/fix-typo-slash-proxy-page

This commit is contained in:
Andras Bacsai
2025-09-22 09:49:59 +02:00
committed by GitHub
30 changed files with 1680 additions and 377 deletions

View File

@@ -652,3 +652,7 @@ it('has emails', function (string $email) {
- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass.
- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test` with a specific filename or filter.
</laravel-boost-guidelines>
Random other things you should remember:
- App\Models\Application::team must return a relationship instance., always use team()

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class ApplicationConfigurationChanged implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public ?int $teamId = null;
public function __construct($teamId = null)
{
if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) {
$teamId = auth()->user()->currentTeam()->id;
}
$this->teamId = $teamId;
}
public function broadcastOn(): array
{
if (is_null($this->teamId)) {
return [];
}
return [
new PrivateChannel("team.{$this->teamId}"),
];
}
}

View File

@@ -5,6 +5,7 @@ namespace App\Jobs;
use App\Actions\Docker\GetContainersStatus;
use App\Enums\ApplicationDeploymentStatus;
use App\Enums\ProcessStatus;
use App\Events\ApplicationConfigurationChanged;
use App\Events\ServiceStatusChanged;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
@@ -147,6 +148,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private Collection $saved_outputs;
private ?string $secrets_hash_key = null;
private ?string $full_healthcheck_url = null;
private string $serverUser = 'root';
@@ -606,6 +609,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d | tee {$this->workdir}{$this->docker_compose_location} > /dev/null"),
'hidden' => true,
]);
// Modify Dockerfiles for ARGs and build secrets
$this->modify_dockerfiles_for_compose($composeFile);
// Build new container to limit downtime.
$this->application_deployment_queue->addLogEntry('Pulling & building required images.');
@@ -632,6 +638,13 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
} else {
$command .= " --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build --pull";
}
if (! $this->application->settings->use_build_secrets && $this->build_args instanceof \Illuminate\Support\Collection && $this->build_args->isNotEmpty()) {
$build_args_string = $this->build_args->implode(' ');
$command .= " {$build_args_string}";
$this->application_deployment_queue->addLogEntry('Adding build arguments to Docker Compose build command.');
}
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, $command), 'hidden' => true],
);
@@ -2702,22 +2715,28 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
if ($this->application->build_pack === 'nixpacks') {
$variables = collect($this->nixpacks_plan_json->get('variables'));
} else {
// Generate environment variables for build process (filters by is_buildtime = true)
$this->generate_env_variables();
$variables = collect([])->merge($this->env_args);
}
// Check if build secrets are enabled and BuildKit is supported
if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) {
$this->generate_build_secrets($variables);
$this->build_args = '';
} else {
// Fall back to traditional build args
$secrets_hash = '';
if ($variables->isNotEmpty()) {
$secrets_hash = $this->generate_secrets_hash($variables);
}
$this->build_args = $variables->map(function ($value, $key) {
$value = escapeshellarg($value);
return "--build-arg {$key}={$value}";
});
if ($secrets_hash) {
$this->build_args->push("--build-arg COOLIFY_BUILD_SECRETS_HASH={$secrets_hash}");
}
}
}
@@ -2736,13 +2755,18 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
return '';
}
return $variables
$secrets_hash = $this->generate_secrets_hash($variables);
$env_flags = $variables
->map(function ($env) {
$escaped_value = escapeshellarg($env->real_value);
return "-e {$env->key}={$escaped_value}";
})
->implode(' ');
$env_flags .= " -e COOLIFY_BUILD_SECRETS_HASH={$secrets_hash}";
return $env_flags;
}
private function generate_build_secrets(Collection $variables)
@@ -2758,6 +2782,36 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
return "--secret id={$key},env={$key}";
})
->implode(' ');
$this->build_secrets .= ' --secret id=COOLIFY_BUILD_SECRETS_HASH,env=COOLIFY_BUILD_SECRETS_HASH';
}
private function generate_secrets_hash($variables)
{
if (! $this->secrets_hash_key) {
$this->secrets_hash_key = bin2hex(random_bytes(32));
}
if ($variables instanceof Collection) {
$secrets_string = $variables
->mapWithKeys(function ($value, $key) {
return [$key => $value];
})
->sortKeys()
->map(function ($value, $key) {
return "{$key}={$value}";
})
->implode('|');
} else {
$secrets_string = $variables
->map(function ($env) {
return "{$env->key}={$env->real_value}";
})
->sort()
->implode('|');
}
return hash_hmac('sha256', $secrets_string, $this->secrets_hash_key);
}
private function add_build_env_variables_to_dockerfile()
@@ -2799,6 +2853,12 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
}
}
}
if ($envs->isNotEmpty()) {
$secrets_hash = $this->generate_secrets_hash($envs);
$dockerfile->splice(1, 0, ["ARG COOLIFY_BUILD_SECRETS_HASH={$secrets_hash}"]);
}
$dockerfile_base64 = base64_encode($dockerfile->implode("\n"));
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d | tee {$this->workdir}{$this->dockerfile_location} > /dev/null"),
@@ -2830,8 +2890,8 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
// Get environment variables for secrets
$variables = $this->pull_request_id === 0
? $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->get()
: $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->get();
? $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->where('is_buildtime', true)->get()
: $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->where('is_buildtime', true)->get();
if ($variables->isEmpty()) {
return;
@@ -2840,6 +2900,9 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
// Generate mount strings for all secrets
$mountStrings = $variables->map(fn ($env) => "--mount=type=secret,id={$env->key},env={$env->key}")->implode(' ');
// Add mount for the secrets hash to ensure cache invalidation
$mountStrings .= ' --mount=type=secret,id=COOLIFY_BUILD_SECRETS_HASH,env=COOLIFY_BUILD_SECRETS_HASH';
$modified = false;
$dockerfile = $dockerfile->map(function ($line) use ($mountStrings, &$modified) {
$trimmed = ltrim($line);
@@ -2868,6 +2931,164 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
}
}
private function modify_dockerfiles_for_compose($composeFile)
{
if ($this->application->build_pack !== 'dockercompose') {
return;
}
$variables = $this->pull_request_id === 0
? $this->application->environment_variables()
->where('key', 'not like', 'NIXPACKS_%')
->where('is_buildtime', true)
->get()
: $this->application->environment_variables_preview()
->where('key', 'not like', 'NIXPACKS_%')
->where('is_buildtime', true)
->get();
if ($variables->isEmpty()) {
$this->application_deployment_queue->addLogEntry('No build-time variables to add to Dockerfiles.');
return;
}
$services = data_get($composeFile, 'services', []);
foreach ($services as $serviceName => $service) {
if (! isset($service['build'])) {
continue;
}
$context = '.';
$dockerfile = 'Dockerfile';
if (is_string($service['build'])) {
$context = $service['build'];
} elseif (is_array($service['build'])) {
$context = data_get($service['build'], 'context', '.');
$dockerfile = data_get($service['build'], 'dockerfile', 'Dockerfile');
}
$dockerfilePath = rtrim($context, '/').'/'.ltrim($dockerfile, '/');
if (str_starts_with($dockerfilePath, './')) {
$dockerfilePath = substr($dockerfilePath, 2);
}
if (str_starts_with($dockerfilePath, '/')) {
$dockerfilePath = substr($dockerfilePath, 1);
}
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "test -f {$this->workdir}/{$dockerfilePath} && echo 'exists' || echo 'not found'"),
'hidden' => true,
'save' => 'dockerfile_check_'.$serviceName,
]);
if (str($this->saved_outputs->get('dockerfile_check_'.$serviceName))->trim()->toString() !== 'exists') {
$this->application_deployment_queue->addLogEntry("Dockerfile not found for service {$serviceName} at {$dockerfilePath}, skipping ARG injection.");
continue;
}
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "cat {$this->workdir}/{$dockerfilePath}"),
'hidden' => true,
'save' => 'dockerfile_content_'.$serviceName,
]);
$dockerfileContent = $this->saved_outputs->get('dockerfile_content_'.$serviceName);
if (! $dockerfileContent) {
continue;
}
$dockerfile_lines = collect(str($dockerfileContent)->trim()->explode("\n"));
$fromIndices = [];
$dockerfile_lines->each(function ($line, $index) use (&$fromIndices) {
if (str($line)->trim()->startsWith('FROM')) {
$fromIndices[] = $index;
}
});
if (empty($fromIndices)) {
$this->application_deployment_queue->addLogEntry("No FROM instruction found in Dockerfile for service {$serviceName}, skipping.");
continue;
}
$isMultiStage = count($fromIndices) > 1;
$argsToAdd = collect([]);
foreach ($variables as $env) {
$argsToAdd->push("ARG {$env->key}");
}
ray($argsToAdd);
if ($argsToAdd->isEmpty()) {
$this->application_deployment_queue->addLogEntry("Service {$serviceName}: No build-time variables to add.");
continue;
}
$totalAdded = 0;
$offset = 0;
foreach ($fromIndices as $stageIndex => $fromIndex) {
$adjustedIndex = $fromIndex + $offset;
$stageStart = $adjustedIndex + 1;
$stageEnd = isset($fromIndices[$stageIndex + 1])
? $fromIndices[$stageIndex + 1] + $offset
: $dockerfile_lines->count();
$existingStageArgs = collect([]);
for ($i = $stageStart; $i < $stageEnd; $i++) {
$line = $dockerfile_lines->get($i);
if (! $line || ! str($line)->trim()->startsWith('ARG')) {
break;
}
$parts = explode(' ', trim($line), 2);
if (count($parts) >= 2) {
$argPart = $parts[1];
$keyValue = explode('=', $argPart, 2);
$existingStageArgs->push($keyValue[0]);
}
}
$stageArgsToAdd = $argsToAdd->filter(function ($arg) use ($existingStageArgs) {
$key = str($arg)->after('ARG ')->trim()->toString();
return ! $existingStageArgs->contains($key);
});
if ($stageArgsToAdd->isNotEmpty()) {
$dockerfile_lines->splice($adjustedIndex + 1, 0, $stageArgsToAdd->toArray());
$totalAdded += $stageArgsToAdd->count();
$offset += $stageArgsToAdd->count();
}
}
if ($totalAdded > 0) {
$dockerfile_base64 = base64_encode($dockerfile_lines->implode("\n"));
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d | tee {$this->workdir}/{$dockerfilePath} > /dev/null"),
'hidden' => true,
]);
$stageInfo = $isMultiStage ? ' (multi-stage build, added to '.count($fromIndices).' stages)' : '';
$this->application_deployment_queue->addLogEntry("Added {$totalAdded} ARG declarations to Dockerfile for service {$serviceName}{$stageInfo}.");
} else {
$this->application_deployment_queue->addLogEntry("Service {$serviceName}: All required ARG declarations already exist.");
}
if ($this->application->settings->use_build_secrets && $this->dockerBuildkitSupported && ! empty($this->build_secrets)) {
$fullDockerfilePath = "{$this->workdir}/{$dockerfilePath}";
$this->modify_dockerfile_for_secrets($fullDockerfilePath);
$this->application_deployment_queue->addLogEntry("Modified Dockerfile for service {$serviceName} to use build secrets.");
}
}
}
private function add_build_secrets_to_compose($composeFile)
{
// Get environment variables for secrets
@@ -3018,6 +3239,9 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
queue_next_deployment($this->application);
if ($status === ApplicationDeploymentStatus::FINISHED->value) {
ray($this->application->team()->id);
event(new ApplicationConfigurationChanged($this->application->team()->id));
if (! $this->only_this_server) {
$this->deploy_to_additional_destinations();
}

View File

@@ -0,0 +1,372 @@
<?php
namespace App\Livewire;
use App\Models\Application;
use App\Models\Server;
use App\Models\Service;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Illuminate\Support\Facades\Cache;
use Livewire\Component;
class GlobalSearch extends Component
{
public $searchQuery = '';
public $isModalOpen = false;
public $searchResults = [];
public $allSearchableItems = [];
public function mount()
{
$this->searchQuery = '';
$this->isModalOpen = false;
$this->searchResults = [];
$this->allSearchableItems = [];
}
public function openSearchModal()
{
$this->isModalOpen = true;
$this->loadSearchableItems();
$this->dispatch('search-modal-opened');
}
public function closeSearchModal()
{
$this->isModalOpen = false;
$this->searchQuery = '';
$this->searchResults = [];
}
public static function getCacheKey($teamId)
{
return 'global_search_items_'.$teamId;
}
public static function clearTeamCache($teamId)
{
Cache::forget(self::getCacheKey($teamId));
}
public function updatedSearchQuery()
{
$this->search();
}
private function loadSearchableItems()
{
// Try to get from Redis cache first
$cacheKey = self::getCacheKey(auth()->user()->currentTeam()->id);
$this->allSearchableItems = Cache::remember($cacheKey, 300, function () {
ray()->showQueries();
$items = collect();
$team = auth()->user()->currentTeam();
// Get all applications
$applications = Application::ownedByCurrentTeam()
->with(['environment.project'])
->get()
->map(function ($app) {
// Collect all FQDNs from the application
$fqdns = collect([]);
// For regular applications
if ($app->fqdn) {
$fqdns = collect(explode(',', $app->fqdn))->map(fn ($fqdn) => trim($fqdn));
}
// For docker compose based applications
if ($app->build_pack === 'dockercompose' && $app->docker_compose_domains) {
try {
$composeDomains = json_decode($app->docker_compose_domains, true);
if (is_array($composeDomains)) {
foreach ($composeDomains as $serviceName => $domains) {
if (is_array($domains)) {
$fqdns = $fqdns->merge($domains);
}
}
}
} catch (\Exception $e) {
// Ignore JSON parsing errors
}
}
$fqdnsString = $fqdns->implode(' ');
return [
'id' => $app->id,
'name' => $app->name,
'type' => 'application',
'uuid' => $app->uuid,
'description' => $app->description,
'link' => $app->link(),
'project' => $app->environment->project->name ?? null,
'environment' => $app->environment->name ?? null,
'fqdns' => $fqdns->take(2)->implode(', '), // Show first 2 FQDNs in UI
'search_text' => strtolower($app->name.' '.$app->description.' '.$fqdnsString),
];
});
// Get all services
$services = Service::ownedByCurrentTeam()
->with(['environment.project', 'applications'])
->get()
->map(function ($service) {
// Collect all FQDNs from service applications
$fqdns = collect([]);
foreach ($service->applications as $app) {
if ($app->fqdn) {
$appFqdns = collect(explode(',', $app->fqdn))->map(fn ($fqdn) => trim($fqdn));
$fqdns = $fqdns->merge($appFqdns);
}
}
$fqdnsString = $fqdns->implode(' ');
return [
'id' => $service->id,
'name' => $service->name,
'type' => 'service',
'uuid' => $service->uuid,
'description' => $service->description,
'link' => $service->link(),
'project' => $service->environment->project->name ?? null,
'environment' => $service->environment->name ?? null,
'fqdns' => $fqdns->take(2)->implode(', '), // Show first 2 FQDNs in UI
'search_text' => strtolower($service->name.' '.$service->description.' '.$fqdnsString),
];
});
// Get all standalone databases
$databases = collect();
// PostgreSQL
$databases = $databases->merge(
StandalonePostgresql::ownedByCurrentTeam()
->with(['environment.project'])
->get()
->map(function ($db) {
return [
'id' => $db->id,
'name' => $db->name,
'type' => 'database',
'subtype' => 'postgresql',
'uuid' => $db->uuid,
'description' => $db->description,
'link' => $db->link(),
'project' => $db->environment->project->name ?? null,
'environment' => $db->environment->name ?? null,
'search_text' => strtolower($db->name.' postgresql '.$db->description),
];
})
);
// MySQL
$databases = $databases->merge(
StandaloneMysql::ownedByCurrentTeam()
->with(['environment.project'])
->get()
->map(function ($db) {
return [
'id' => $db->id,
'name' => $db->name,
'type' => 'database',
'subtype' => 'mysql',
'uuid' => $db->uuid,
'description' => $db->description,
'link' => $db->link(),
'project' => $db->environment->project->name ?? null,
'environment' => $db->environment->name ?? null,
'search_text' => strtolower($db->name.' mysql '.$db->description),
];
})
);
// MariaDB
$databases = $databases->merge(
StandaloneMariadb::ownedByCurrentTeam()
->with(['environment.project'])
->get()
->map(function ($db) {
return [
'id' => $db->id,
'name' => $db->name,
'type' => 'database',
'subtype' => 'mariadb',
'uuid' => $db->uuid,
'description' => $db->description,
'link' => $db->link(),
'project' => $db->environment->project->name ?? null,
'environment' => $db->environment->name ?? null,
'search_text' => strtolower($db->name.' mariadb '.$db->description),
];
})
);
// MongoDB
$databases = $databases->merge(
StandaloneMongodb::ownedByCurrentTeam()
->with(['environment.project'])
->get()
->map(function ($db) {
return [
'id' => $db->id,
'name' => $db->name,
'type' => 'database',
'subtype' => 'mongodb',
'uuid' => $db->uuid,
'description' => $db->description,
'link' => $db->link(),
'project' => $db->environment->project->name ?? null,
'environment' => $db->environment->name ?? null,
'search_text' => strtolower($db->name.' mongodb '.$db->description),
];
})
);
// Redis
$databases = $databases->merge(
StandaloneRedis::ownedByCurrentTeam()
->with(['environment.project'])
->get()
->map(function ($db) {
return [
'id' => $db->id,
'name' => $db->name,
'type' => 'database',
'subtype' => 'redis',
'uuid' => $db->uuid,
'description' => $db->description,
'link' => $db->link(),
'project' => $db->environment->project->name ?? null,
'environment' => $db->environment->name ?? null,
'search_text' => strtolower($db->name.' redis '.$db->description),
];
})
);
// KeyDB
$databases = $databases->merge(
StandaloneKeydb::ownedByCurrentTeam()
->with(['environment.project'])
->get()
->map(function ($db) {
return [
'id' => $db->id,
'name' => $db->name,
'type' => 'database',
'subtype' => 'keydb',
'uuid' => $db->uuid,
'description' => $db->description,
'link' => $db->link(),
'project' => $db->environment->project->name ?? null,
'environment' => $db->environment->name ?? null,
'search_text' => strtolower($db->name.' keydb '.$db->description),
];
})
);
// Dragonfly
$databases = $databases->merge(
StandaloneDragonfly::ownedByCurrentTeam()
->with(['environment.project'])
->get()
->map(function ($db) {
return [
'id' => $db->id,
'name' => $db->name,
'type' => 'database',
'subtype' => 'dragonfly',
'uuid' => $db->uuid,
'description' => $db->description,
'link' => $db->link(),
'project' => $db->environment->project->name ?? null,
'environment' => $db->environment->name ?? null,
'search_text' => strtolower($db->name.' dragonfly '.$db->description),
];
})
);
// Clickhouse
$databases = $databases->merge(
StandaloneClickhouse::ownedByCurrentTeam()
->with(['environment.project'])
->get()
->map(function ($db) {
return [
'id' => $db->id,
'name' => $db->name,
'type' => 'database',
'subtype' => 'clickhouse',
'uuid' => $db->uuid,
'description' => $db->description,
'link' => $db->link(),
'project' => $db->environment->project->name ?? null,
'environment' => $db->environment->name ?? null,
'search_text' => strtolower($db->name.' clickhouse '.$db->description),
];
})
);
// Get all servers
$servers = Server::ownedByCurrentTeam()
->get()
->map(function ($server) {
return [
'id' => $server->id,
'name' => $server->name,
'type' => 'server',
'uuid' => $server->uuid,
'description' => $server->description,
'link' => $server->url(),
'project' => null,
'environment' => null,
'search_text' => strtolower($server->name.' '.$server->ip.' '.$server->description),
];
});
// Merge all collections
$items = $items->merge($applications)
->merge($services)
->merge($databases)
->merge($servers);
return $items->toArray();
});
}
private function search()
{
if (strlen($this->searchQuery) < 2) {
$this->searchResults = [];
return;
}
$query = strtolower($this->searchQuery);
// Case-insensitive search in the items
$this->searchResults = collect($this->allSearchableItems)
->filter(function ($item) use ($query) {
return str_contains($item['search_text'], $query);
})
->take(20)
->values()
->toArray();
}
public function render()
{
return view('livewire.global-search');
}
}

View File

@@ -20,7 +20,15 @@ class ConfigurationChecker extends Component
public Application|Service|StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource;
protected $listeners = ['configurationChanged'];
public function getListeners()
{
$teamId = auth()->user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},ApplicationConfigurationChanged" => 'configurationChanged',
'configurationChanged' => 'configurationChanged',
];
}
public function mount()
{

View File

@@ -8,7 +8,7 @@ class Metrics extends Component
{
public $resource;
public $chartId = 'container-cpu';
public $chartId = 'metrics';
public $data;

View File

@@ -4,6 +4,7 @@ namespace App\Models;
use App\Enums\ApplicationDeploymentStatus;
use App\Services\ConfigurationGenerator;
use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasConfiguration;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Casts\Attribute;
@@ -110,7 +111,7 @@ use Visus\Cuid2\Cuid2;
class Application extends BaseModel
{
use HasConfiguration, HasFactory, HasSafeStringAttribute, SoftDeletes;
use ClearsGlobalSearchCache, HasConfiguration, HasFactory, HasSafeStringAttribute, SoftDeletes;
private static $parserVersion = '5';
@@ -123,66 +124,6 @@ class Application extends BaseModel
'http_basic_auth_password' => 'encrypted',
];
public function customNetworkAliases(): Attribute
{
return Attribute::make(
set: function ($value) {
if (is_null($value) || $value === '') {
return null;
}
// If it's already a JSON string, decode it
if (is_string($value) && $this->isJson($value)) {
$value = json_decode($value, true);
}
// If it's a string but not JSON, treat it as a comma-separated list
if (is_string($value) && ! is_array($value)) {
$value = explode(',', $value);
}
$value = collect($value)
->map(function ($alias) {
if (is_string($alias)) {
return str_replace(' ', '-', trim($alias));
}
return null;
})
->filter()
->unique() // Remove duplicate values
->values()
->toArray();
return empty($value) ? null : json_encode($value);
},
get: function ($value) {
if (is_null($value)) {
return null;
}
if (is_string($value) && $this->isJson($value)) {
return json_decode($value, true);
}
return is_array($value) ? $value : [];
}
);
}
/**
* Check if a string is a valid JSON
*/
private function isJson($string)
{
if (! is_string($string)) {
return false;
}
json_decode($string);
return json_last_error() === JSON_ERROR_NONE;
}
protected static function booted()
{
static::addGlobalScope('withRelations', function ($builder) {
@@ -250,6 +191,66 @@ class Application extends BaseModel
});
}
public function customNetworkAliases(): Attribute
{
return Attribute::make(
set: function ($value) {
if (is_null($value) || $value === '') {
return null;
}
// If it's already a JSON string, decode it
if (is_string($value) && $this->isJson($value)) {
$value = json_decode($value, true);
}
// If it's a string but not JSON, treat it as a comma-separated list
if (is_string($value) && ! is_array($value)) {
$value = explode(',', $value);
}
$value = collect($value)
->map(function ($alias) {
if (is_string($alias)) {
return str_replace(' ', '-', trim($alias));
}
return null;
})
->filter()
->unique() // Remove duplicate values
->values()
->toArray();
return empty($value) ? null : json_encode($value);
},
get: function ($value) {
if (is_null($value)) {
return null;
}
if (is_string($value) && $this->isJson($value)) {
return json_decode($value, true);
}
return is_array($value) ? $value : [];
}
);
}
/**
* Check if a string is a valid JSON
*/
private function isJson($string)
{
if (! is_string($string)) {
return false;
}
json_decode($string);
return json_last_error() === JSON_ERROR_NONE;
}
public static function ownedByCurrentTeamAPI(int $teamId)
{
return Application::whereRelation('environment.project.team', 'id', $teamId)->orderBy('name');

View File

@@ -85,6 +85,47 @@ class ApplicationDeploymentQueue extends Model
return str($this->commit_message)->value();
}
private function redactSensitiveInfo($text)
{
$text = remove_iip($text);
$app = $this->application;
if (! $app) {
return $text;
}
$lockedVars = collect([]);
if ($app->environment_variables) {
$lockedVars = $lockedVars->merge(
$app->environment_variables
->where('is_shown_once', true)
->pluck('real_value', 'key')
->filter()
);
}
if ($this->pull_request_id !== 0 && $app->environment_variables_preview) {
$lockedVars = $lockedVars->merge(
$app->environment_variables_preview
->where('is_shown_once', true)
->pluck('real_value', 'key')
->filter()
);
}
foreach ($lockedVars as $key => $value) {
$escapedValue = preg_quote($value, '/');
$text = preg_replace(
'/'.$escapedValue.'/',
REDACTED,
$text
);
}
return $text;
}
public function addLogEntry(string $message, string $type = 'stdout', bool $hidden = false)
{
if ($type === 'error') {
@@ -96,7 +137,7 @@ class ApplicationDeploymentQueue extends Model
}
$newLogEntry = [
'command' => null,
'output' => remove_iip($message),
'output' => $this->redactSensitiveInfo($message),
'type' => $type,
'timestamp' => Carbon::now('UTC'),
'hidden' => $hidden,

View File

@@ -13,6 +13,7 @@ use App\Jobs\RegenerateSslCertJob;
use App\Notifications\Server\Reachable;
use App\Notifications\Server\Unreachable;
use App\Services\ConfigurationRepository;
use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
@@ -55,7 +56,7 @@ use Visus\Cuid2\Cuid2;
class Server extends BaseModel
{
use HasFactory, SchemalessAttributesTrait, SoftDeletes;
use ClearsGlobalSearchCache, HasFactory, SchemalessAttributesTrait, SoftDeletes;
public static $batch_counter = 0;

View File

@@ -3,6 +3,7 @@
namespace App\Models;
use App\Enums\ProcessStatus;
use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -41,7 +42,7 @@ use Visus\Cuid2\Cuid2;
)]
class Service extends BaseModel
{
use HasFactory, HasSafeStringAttribute, SoftDeletes;
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
private static $parserVersion = '5';

View File

@@ -2,6 +2,7 @@
namespace App\Models;
use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -9,7 +10,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneClickhouse extends BaseModel
{
use HasFactory, HasSafeStringAttribute, SoftDeletes;
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
protected $guarded = [];
@@ -43,6 +44,11 @@ class StandaloneClickhouse extends BaseModel
});
}
public static function ownedByCurrentTeam()
{
return StandaloneClickhouse::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
}
protected function serverStatus(): Attribute
{
return Attribute::make(

View File

@@ -2,6 +2,7 @@
namespace App\Models;
use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -9,7 +10,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneDragonfly extends BaseModel
{
use HasFactory, HasSafeStringAttribute, SoftDeletes;
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
protected $guarded = [];
@@ -43,6 +44,11 @@ class StandaloneDragonfly extends BaseModel
});
}
public static function ownedByCurrentTeam()
{
return StandaloneDragonfly::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
}
protected function serverStatus(): Attribute
{
return Attribute::make(

View File

@@ -2,6 +2,7 @@
namespace App\Models;
use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -9,7 +10,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneKeydb extends BaseModel
{
use HasFactory, HasSafeStringAttribute, SoftDeletes;
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
protected $guarded = [];
@@ -43,6 +44,11 @@ class StandaloneKeydb extends BaseModel
});
}
public static function ownedByCurrentTeam()
{
return StandaloneKeydb::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
}
protected function serverStatus(): Attribute
{
return Attribute::make(

View File

@@ -2,6 +2,7 @@
namespace App\Models;
use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -10,7 +11,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneMariadb extends BaseModel
{
use HasFactory, HasSafeStringAttribute, SoftDeletes;
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
protected $guarded = [];
@@ -44,6 +45,11 @@ class StandaloneMariadb extends BaseModel
});
}
public static function ownedByCurrentTeam()
{
return StandaloneMariadb::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
}
protected function serverStatus(): Attribute
{
return Attribute::make(

View File

@@ -2,6 +2,7 @@
namespace App\Models;
use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -9,7 +10,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneMongodb extends BaseModel
{
use HasFactory, HasSafeStringAttribute, SoftDeletes;
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
protected $guarded = [];
@@ -46,6 +47,11 @@ class StandaloneMongodb extends BaseModel
});
}
public static function ownedByCurrentTeam()
{
return StandaloneMongodb::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
}
protected function serverStatus(): Attribute
{
return Attribute::make(

View File

@@ -2,6 +2,7 @@
namespace App\Models;
use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -9,7 +10,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneMysql extends BaseModel
{
use HasFactory, HasSafeStringAttribute, SoftDeletes;
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
protected $guarded = [];
@@ -44,6 +45,11 @@ class StandaloneMysql extends BaseModel
});
}
public static function ownedByCurrentTeam()
{
return StandaloneMysql::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
}
protected function serverStatus(): Attribute
{
return Attribute::make(

View File

@@ -2,6 +2,7 @@
namespace App\Models;
use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -9,7 +10,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
class StandalonePostgresql extends BaseModel
{
use HasFactory, HasSafeStringAttribute, SoftDeletes;
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
protected $guarded = [];
@@ -44,6 +45,11 @@ class StandalonePostgresql extends BaseModel
});
}
public static function ownedByCurrentTeam()
{
return StandalonePostgresql::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
}
public function workdir()
{
return database_configuration_dir()."/{$this->uuid}";

View File

@@ -2,6 +2,7 @@
namespace App\Models;
use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -9,7 +10,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneRedis extends BaseModel
{
use HasFactory, HasSafeStringAttribute, SoftDeletes;
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
protected $guarded = [];
@@ -45,6 +46,11 @@ class StandaloneRedis extends BaseModel
});
}
public static function ownedByCurrentTeam()
{
return StandaloneRedis::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
}
protected function serverStatus(): Attribute
{
return Attribute::make(

View File

@@ -0,0 +1,81 @@
<?php
namespace App\Traits;
use App\Livewire\GlobalSearch;
trait ClearsGlobalSearchCache
{
protected static function bootClearsGlobalSearchCache()
{
static::saving(function ($model) {
// Only clear cache if searchable fields are being changed
if ($model->hasSearchableChanges()) {
$teamId = $model->getTeamIdForCache();
if (filled($teamId)) {
GlobalSearch::clearTeamCache($teamId);
}
}
});
static::created(function ($model) {
// Always clear cache when model is created
$teamId = $model->getTeamIdForCache();
if (filled($teamId)) {
GlobalSearch::clearTeamCache($teamId);
}
});
static::deleted(function ($model) {
// Always clear cache when model is deleted
$teamId = $model->getTeamIdForCache();
if (filled($teamId)) {
GlobalSearch::clearTeamCache($teamId);
}
});
}
private function hasSearchableChanges(): bool
{
// Define searchable fields based on model type
$searchableFields = ['name', 'description'];
// Add model-specific searchable fields
if ($this instanceof \App\Models\Application) {
$searchableFields[] = 'fqdn';
$searchableFields[] = 'docker_compose_domains';
} elseif ($this instanceof \App\Models\Server) {
$searchableFields[] = 'ip';
} elseif ($this instanceof \App\Models\Service) {
// Services don't have direct fqdn, but name and description are covered
}
// Database models only have name and description as searchable
// Check if any searchable field is dirty
foreach ($searchableFields as $field) {
if ($this->isDirty($field)) {
return true;
}
}
return false;
}
private function getTeamIdForCache()
{
// For database models, team is accessed through environment.project.team
if (method_exists($this, 'team')) {
$team = $this->team();
if (filled($team)) {
return is_object($team) ? $team->id : null;
}
}
// For models with direct team_id property
if (property_exists($this, 'team_id') || isset($this->team_id)) {
return $this->team_id;
}
return null;
}
}

View File

@@ -17,6 +17,46 @@ trait ExecuteRemoteCommand
public static int $batch_counter = 0;
private function redact_sensitive_info($text)
{
$text = remove_iip($text);
if (! isset($this->application)) {
return $text;
}
$lockedVars = collect([]);
if (isset($this->application->environment_variables)) {
$lockedVars = $lockedVars->merge(
$this->application->environment_variables
->where('is_shown_once', true)
->pluck('real_value', 'key')
->filter()
);
}
if (isset($this->pull_request_id) && $this->pull_request_id !== 0 && isset($this->application->environment_variables_preview)) {
$lockedVars = $lockedVars->merge(
$this->application->environment_variables_preview
->where('is_shown_once', true)
->pluck('real_value', 'key')
->filter()
);
}
foreach ($lockedVars as $key => $value) {
$escapedValue = preg_quote($value, '/');
$text = preg_replace(
'/'.$escapedValue.'/',
REDACTED,
$text
);
}
return $text;
}
public function execute_remote_command(...$commands)
{
static::$batch_counter++;
@@ -74,7 +114,7 @@ trait ExecuteRemoteCommand
// Track SSH retry event in Sentry
$this->trackSshRetryEvent($attempt, $maxRetries, $delay, $errorMessage, [
'server' => $this->server->name ?? $this->server->ip ?? 'unknown',
'command' => remove_iip($command),
'command' => $this->redact_sensitive_info($command),
'trait' => 'ExecuteRemoteCommand',
]);
@@ -125,8 +165,8 @@ trait ExecuteRemoteCommand
$sanitized_output = sanitize_utf8_text($output);
$new_log_entry = [
'command' => remove_iip($command),
'output' => remove_iip($sanitized_output),
'command' => $this->redact_sensitive_info($command),
'output' => $this->redact_sensitive_info($sanitized_output),
'type' => $customType ?? $type === 'err' ? 'stderr' : 'stdout',
'timestamp' => Carbon::now('UTC'),
'hidden' => $hidden,
@@ -194,7 +234,7 @@ trait ExecuteRemoteCommand
$retryMessage = "SSH connection failed. Retrying... (Attempt {$attempt}/{$maxRetries}, waiting {$delay}s)\nError: {$errorMessage}";
$new_log_entry = [
'output' => remove_iip($retryMessage),
'output' => $this->redact_sensitive_info($retryMessage),
'type' => 'stdout',
'timestamp' => Carbon::now('UTC'),
'hidden' => false,

View File

@@ -6,10 +6,31 @@
@apply hidden!;
}
@utility apexcharts-grid-borders {
@apply dark:hidden!;
}
@utility apexcharts-xaxistooltip {
@apply hidden!;
}
@utility apexcharts-tooltip-custom {
@apply bg-white dark:bg-coolgray-100 border border-neutral-200 dark:border-coolgray-300 rounded-lg shadow-lg p-3 text-sm;
min-width: 160px;
}
@utility apexcharts-tooltip-custom-value {
@apply text-neutral-700 dark:text-neutral-300 mb-1;
}
@utility apexcharts-tooltip-value-bold {
@apply font-bold text-black dark:text-white;
}
@utility apexcharts-tooltip-custom-title {
@apply text-xs text-neutral-500 dark:text-neutral-400 font-medium;
}
@utility input-sticky {
@apply block py-1.5 w-full text-sm text-black rounded-sm border-0 ring-1 ring-inset dark:bg-coolgray-100 dark:text-white ring-neutral-200 dark:ring-coolgray-300 focus:ring-2 focus:ring-neutral-400 dark:focus:ring-coolgray-300;
}

View File

@@ -11,6 +11,7 @@
'content' => null,
'checkboxes' => [],
'actions' => [],
'warningMessage' => null,
'confirmWithText' => true,
'confirmationText' => 'Confirm Deletion',
'confirmationLabel' => 'Please confirm the execution of the actions by entering the Name below',
@@ -228,7 +229,7 @@
<div x-show="step === 2">
<div class="p-4 mb-4 text-white border-l-4 border-red-500 bg-error" role="alert">
<p class="font-bold">Warning</p>
<p>This operation is permanent and cannot be undone. Please think again before proceeding!
<p>{!! $warningMessage ?: 'This operation is permanent and cannot be undone. Please think again before proceeding!' !!}
</p>
</div>
<div class="mb-4">The following actions will be performed:</div>

View File

@@ -82,6 +82,9 @@
<div class="text-2xl font-bold tracking-wide dark:text-white">Coolify</div>
<x-version />
</div>
<div>
<livewire:global-search />
</div>
<livewire:settings-dropdown />
</div>
<div class="px-2 pt-2 pb-7">

View File

@@ -138,7 +138,8 @@
}
}
let theme = localStorage.theme
let baseColor = '#FCD452'
let cpuColor = '#1e90ff'
let ramColor = '#00ced1'
let textColor = '#ffffff'
let editorBackground = '#181818'
let editorTheme = 'blackboard'
@@ -149,12 +150,14 @@
theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
}
if (theme == 'dark') {
baseColor = '#FCD452'
cpuColor = '#1e90ff'
ramColor = '#00ced1'
textColor = '#ffffff'
editorBackground = '#181818'
editorTheme = 'blackboard'
} else {
baseColor = 'black'
cpuColor = '#1e90ff'
ramColor = '#00ced1'
textColor = '#000000'
editorBackground = '#ffffff'
editorTheme = null

View File

@@ -0,0 +1,236 @@
<div x-data="{
modalOpen: false,
selectedIndex: -1,
openModal() {
this.modalOpen = true;
this.selectedIndex = -1;
@this.openSearchModal();
},
closeModal() {
this.modalOpen = false;
this.selectedIndex = -1;
@this.closeSearchModal();
},
navigateResults(direction) {
const results = document.querySelectorAll('.search-result-item');
if (results.length === 0) return;
if (direction === 'down') {
this.selectedIndex = Math.min(this.selectedIndex + 1, results.length - 1);
} else if (direction === 'up') {
this.selectedIndex = Math.max(this.selectedIndex - 1, -1);
}
if (this.selectedIndex >= 0 && this.selectedIndex < results.length) {
results[this.selectedIndex].focus();
results[this.selectedIndex].scrollIntoView({ block: 'nearest' });
} else if (this.selectedIndex === -1) {
this.$refs.searchInput?.focus();
}
},
init() {
// Listen for / key press globally
document.addEventListener('keydown', (e) => {
if (e.key === '/' && !['INPUT', 'TEXTAREA'].includes(e.target.tagName) && !this.modalOpen) {
e.preventDefault();
this.openModal();
}
});
// Listen for Cmd+K or Ctrl+K globally
document.addEventListener('keydown', (e) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
if (this.modalOpen) {
this.closeModal();
} else {
this.openModal();
}
}
});
// Listen for Escape key to close modal
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.modalOpen) {
this.closeModal();
}
});
// Listen for arrow keys when modal is open
document.addEventListener('keydown', (e) => {
if (!this.modalOpen) return;
if (e.key === 'ArrowDown') {
e.preventDefault();
this.navigateResults('down');
} else if (e.key === 'ArrowUp') {
e.preventDefault();
this.navigateResults('up');
}
});
}
}">
<!-- Search bar in navbar -->
<div class="flex justify-center">
<button @click="openModal()" type="button" title="Search (Press / or ⌘K)"
class="flex items-center gap-1.5 px-2.5 py-1.5 bg-neutral-100 dark:bg-coolgray-100 border border-neutral-300 dark:border-coolgray-200 rounded-md hover:bg-neutral-200 dark:hover:bg-coolgray-200 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-neutral-500 dark:text-neutral-400" fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
<kbd
class="px-1 py-0.5 text-xs font-semibold text-neutral-500 dark:text-neutral-400 bg-neutral-200 dark:bg-coolgray-200 rounded">/</kbd>
</button>
</div>
<!-- Modal overlay -->
<template x-teleport="body">
<div x-show="modalOpen" x-cloak
class="fixed top-0 lg:pt-10 left-0 z-99 flex items-start justify-center w-screen h-screen">
<div @click="closeModal()" class="absolute inset-0 w-full h-full bg-black/20 backdrop-blur-xs">
</div>
<div x-show="modalOpen" x-trap.inert.noscroll="modalOpen" x-transition:enter="ease-out duration-100"
x-transition:enter-start="opacity-0 -translate-y-2 sm:scale-95"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave="ease-in duration-100"
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
class="relative w-full py-6 border rounded-sm min-w-full lg:min-w-[36rem] max-w-[48rem] bg-neutral-100 border-neutral-400 dark:bg-base px-7 dark:border-coolgray-300"
@click.stop>
<div class="flex justify-between items-center pb-3">
<h3 class="pr-8 text-2xl font-bold">Search</h3>
<button @click="closeModal()"
class="flex absolute top-2 right-2 justify-center items-center w-8 h-8 rounded-full dark:text-white hover:bg-coolgray-300">
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="relative w-auto">
<input type="text" wire:model.live.debounce.500ms="searchQuery"
placeholder="Type to search for applications, services, databases, and servers..."
x-ref="searchInput" x-init="$watch('modalOpen', value => { if (value) setTimeout(() => $refs.searchInput.focus(), 100) })" class="w-full input mb-4" />
<!-- Search results -->
<div class="relative min-h-[330px] max-h-[400px] overflow-y-auto scrollbar">
<!-- Loading indicator -->
<div wire:loading.flex wire:target="searchQuery"
class="min-h-[330px] items-center justify-center">
<div class="text-center">
<svg class="animate-spin mx-auto h-8 w-8 text-neutral-400"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10"
stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
</path>
</svg>
<p class="mt-2 text-sm text-neutral-600 dark:text-neutral-400">
Searching...
</p>
</div>
</div>
<!-- Results content - hidden while loading -->
<div wire:loading.remove wire:target="searchQuery">
@if (strlen($searchQuery) >= 2 && count($searchResults) > 0)
<div class="space-y-1 my-4 pb-4">
@foreach ($searchResults as $index => $result)
<a href="{{ $result['link'] ?? '#' }}"
class="search-result-item block p-3 mx-1 hover:bg-neutral-200 dark:hover:bg-coolgray-200 transition-colors focus:outline-none focus:ring-1 focus:ring-coollabs focus:bg-neutral-100 dark:focus:bg-coolgray-200 ">
<div class="flex items-center justify-between">
<div class="flex-1">
<div class="flex items-center gap-2">
<span class="font-medium text-neutral-900 dark:text-white">
{{ $result['name'] }}
</span>
@if ($result['type'] === 'server')
<span
class="px-2 py-0.5 text-xs rounded bg-coolgray-100 text-white">
Server
</span>
@endif
</div>
<div class="flex items-center gap-2">
@if (!empty($result['project']) && !empty($result['environment']))
<span
class="text-xs text-neutral-500 dark:text-neutral-400">
{{ $result['project'] }} / {{ $result['environment'] }}
</span>
@endif
@if ($result['type'] === 'application')
<span
class="px-2 py-0.5 text-xs rounded bg-coolgray-100 text-white">
Application
</span>
@elseif ($result['type'] === 'service')
<span
class="px-2 py-0.5 text-xs rounded bg-coolgray-100 text-white">
Service
</span>
@elseif ($result['type'] === 'database')
<span
class="px-2 py-0.5 text-xs rounded bg-coolgray-100 text-white">
{{ ucfirst($result['subtype'] ?? 'Database') }}
</span>
@endif
</div>
@if (!empty($result['description']))
<div
class="text-sm text-neutral-600 dark:text-neutral-400 mt-0.5">
{{ Str::limit($result['description'], 100) }}
</div>
@endif
</div>
<svg xmlns="http://www.w3.org/2000/svg"
class="shrink-0 ml-2 h-4 w-4 text-neutral-400" fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</div>
</a>
@endforeach
</div>
@elseif (strlen($searchQuery) >= 2 && count($searchResults) === 0)
<div class="flex items-center justify-center min-h-[330px]">
<div class="text-center">
<p class="text-sm text-neutral-600 dark:text-neutral-400">
No results found for "<strong>{{ $searchQuery }}</strong>"
</p>
<p class="text-xs text-neutral-500 dark:text-neutral-500 mt-2">
Try different keywords or check the spelling
</p>
</div>
</div>
@elseif (strlen($searchQuery) > 0 && strlen($searchQuery) < 2)
<div class="flex items-center justify-center min-h-[330px]">
<div class="text-center">
<p class="text-sm text-neutral-600 dark:text-neutral-400">
Type at least 2 characters to search
</p>
</div>
</div>
@else
<div class="flex items-center justify-center min-h-[330px]">
<div class="text-center">
<p class="text-sm text-neutral-600 dark:text-neutral-400">
Start typing to search
</p>
<p class="text-xs text-neutral-500 dark:text-neutral-500 mt-2">
Search for applications, services, databases, and servers
</p>
</div>
</div>
@endif
</div>
</div>
</div>
</div>
</div>
</div>
</template>
</div>

View File

@@ -28,7 +28,6 @@
@endcan
</div>
@endif
@if (data_get($resource, 'build_pack') !== 'dockercompose')
<div class="w-64">
@can('manageEnvironment', $resource)
<x-forms.checkbox id="use_build_secrets" label="Use Docker Build Secrets"
@@ -40,7 +39,6 @@
disabled></x-forms.checkbox>
@endcan
</div>
@endif
</div>
@endif
@if ($resource->type() === 'service' || $resource?->build_pack === 'dockercompose')

View File

@@ -7,7 +7,7 @@
@if ($isLocked)
<div class="flex flex-1 w-full gap-2">
<x-forms.input disabled id="key" />
<svg class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<svg class="icon my-1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<path d="M5 13a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-6z" />
<path d="M11 16a1 1 0 1 0 2 0a1 1 0 0 0-2 0m-3-5V7a4 4 0 1 1 8 0v4" />
@@ -21,6 +21,95 @@
step2ButtonText="Permanently Delete" />
@endcan
</div>
@can('update', $this->env)
<div class="flex flex-col w-full gap-3">
<div class="flex w-full items-center gap-4 overflow-x-auto whitespace-nowrap">
@if (!$is_redis_credential)
@if ($type === 'service')
<x-forms.checkbox instantSave id="is_buildtime"
helper="Make this variable available during Docker build process. Useful for build secrets and dependencies."
label="Available at Buildtime" />
<x-forms.checkbox instantSave id="is_runtime"
helper="Make this variable available in the running container at runtime."
label="Available at Runtime" />
<x-forms.checkbox instantSave id="is_multiline" label="Is Multiline?" />
<x-forms.checkbox instantSave id="is_literal"
helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true."
label="Is Literal?" />
@else
@if ($is_shared)
<x-forms.checkbox instantSave id="is_literal"
helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true."
label="Is Literal?" />
@else
@if ($isSharedVariable)
<x-forms.checkbox instantSave id="is_multiline" label="Is Multiline?" />
@else
@if (!$env->is_nixpacks)
<x-forms.checkbox instantSave id="is_buildtime"
helper="Make this variable available during Docker build process. Useful for build secrets and dependencies."
label="Available at Buildtime" />
@endif
<x-forms.checkbox instantSave id="is_runtime"
helper="Make this variable available in the running container at runtime."
label="Available at Runtime" />
@if (!$env->is_nixpacks)
<x-forms.checkbox instantSave id="is_multiline" label="Is Multiline?" />
@if ($is_multiline === false)
<x-forms.checkbox instantSave id="is_literal"
helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true."
label="Is Literal?" />
@endif
@endif
@endif
@endif
@endif
@endif
</div>
</div>
@else
<div class="flex flex-col w-full gap-3">
<div class="flex w-full items-center gap-4 overflow-x-auto whitespace-nowrap">
@if (!$is_redis_credential)
@if ($type === 'service')
<x-forms.checkbox disabled id="is_buildtime"
helper="Make this variable available during Docker build process. Useful for build secrets and dependencies."
label="Available at Buildtime" />
<x-forms.checkbox disabled id="is_runtime"
helper="Make this variable available in the running container at runtime."
label="Available at Runtime" />
<x-forms.checkbox disabled id="is_multiline" label="Is Multiline?" />
<x-forms.checkbox disabled id="is_literal"
helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true."
label="Is Literal?" />
@else
@if ($is_shared)
<x-forms.checkbox disabled id="is_literal"
helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true."
label="Is Literal?" />
@else
@if ($isSharedVariable)
<x-forms.checkbox disabled id="is_multiline" label="Is Multiline?" />
@else
<x-forms.checkbox disabled id="is_buildtime"
helper="Make this variable available during Docker build process. Useful for build secrets and dependencies."
label="Available at Buildtime" />
<x-forms.checkbox disabled id="is_runtime"
helper="Make this variable available in the running container at runtime."
label="Available at Runtime" />
<x-forms.checkbox disabled id="is_multiline" label="Is Multiline?" />
@if ($is_multiline === false)
<x-forms.checkbox disabled id="is_literal"
helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true."
label="Is Literal?" />
@endif
@endif
@endif
@endif
@endif
</div>
</div>
@endcan
@else
@can('update', $this->env)
@if ($isDisabled)
@@ -78,7 +167,6 @@
@if ($isSharedVariable)
<x-forms.checkbox instantSave id="is_multiline" label="Is Multiline?" />
@else
@if (!$env->is_coolify)
@if (!$env->is_nixpacks)
<x-forms.checkbox instantSave id="is_buildtime"
helper="Make this variable available during Docker build process. Useful for build secrets and dependencies."
@@ -99,7 +187,6 @@
@endif
@endif
@endif
@endif
</div>
<div class="flex w-full justify-end gap-2">
@if ($isDisabled)

View File

@@ -1,20 +1,19 @@
<div>
<div class="flex items-center gap-2 ">
<div class="flex items-center gap-2">
<h2>Metrics</h2>
</div>
<div class="pb-4">Basic metrics for your container.</div>
<div class="pb-4">Basic metrics for your application container.</div>
<div>
@if ($resource->getMorphClass() === 'App\Models\Application' && $resource->build_pack === 'dockercompose')
<div class="alert alert-warning">Metrics are not available for Docker Compose applications yet!</div>
@elseif(!$resource->destination->server->isMetricsEnabled())
<div class="alert alert-warning">Metrics are only available for servers with Sentinel & Metrics enabled!</div>
<div> Go to <a class="underline dark:text-white"
href="{{ route('server.show', $resource->destination->server->uuid) }}">Server settings</a> to
enable
it.</div>
<div>Go to <a class="underline dark:text-white" href="{{ route('server.show', $resource->destination->server->uuid) }}">Server settings</a> to enable it.</div>
@else
@if (!str($resource->status)->contains('running'))
<div class="alert alert-warning">Metrics are only available when this resource is running!</div>
<div class="alert alert-warning">Metrics are only available when the application container is running!</div>
@else
<div>
<x-forms.select label="Interval" wire:change="setInterval" id="interval">
<option value="5">5 minutes (live)</option>
<option value="10">10 minutes (live)</option>
@@ -26,7 +25,7 @@
</x-forms.select>
<div @if ($poll) wire:poll.5000ms='pollData' @endif x-init="$wire.loadData()"
class="pt-5">
<h4>CPU (%)</h4>
<h4>CPU Usage</h4>
<div wire:ignore id="{!! $chartId !!}-cpu"></div>
<script>
@@ -34,6 +33,7 @@
const optionsServerCpu = {
stroke: {
curve: 'straight',
width: 2,
},
chart: {
height: '150px',
@@ -52,7 +52,7 @@
},
},
animations: {
enabled: false,
enabled: true,
},
},
fill: {
@@ -72,7 +72,7 @@
show: true,
borderColor: '',
},
colors: [baseColor],
colors: [cpuColor],
xaxis: {
type: 'datetime',
},
@@ -90,6 +90,21 @@
enabled: true,
marker: {
show: false,
},
custom: function({ series, seriesIndex, dataPointIndex, w }) {
const value = series[seriesIndex][dataPointIndex];
const timestamp = w.globals.seriesX[seriesIndex][dataPointIndex];
const date = new Date(timestamp);
const timeString = String(date.getUTCHours()).padStart(2, '0') + ':' +
String(date.getUTCMinutes()).padStart(2, '0') + ':' +
String(date.getUTCSeconds()).padStart(2, '0') + ', ' +
date.getUTCFullYear() + '-' +
String(date.getUTCMonth() + 1).padStart(2, '0') + '-' +
String(date.getUTCDate()).padStart(2, '0');
return '<div class="apexcharts-tooltip-custom">' +
'<div class="apexcharts-tooltip-custom-value">CPU: <span class="apexcharts-tooltip-value-bold">' + value + '%</span></div>' +
'<div class="apexcharts-tooltip-custom-title">' + timeString + '</div>' +
'</div>';
}
},
legend: {
@@ -98,14 +113,13 @@
}
const serverCpuChart = new ApexCharts(document.getElementById(`{!! $chartId !!}-cpu`), optionsServerCpu);
serverCpuChart.render();
document.addEventListener('livewire:init', () => {
Livewire.on('refreshChartData-{!! $chartId !!}-cpu', (chartData) => {
checkTheme();
serverCpuChart.updateOptions({
series: [{
data: chartData[0].seriesData,
}],
colors: [baseColor],
colors: [cpuColor],
xaxis: {
type: 'datetime',
labels: {
@@ -121,6 +135,9 @@
show: true,
style: {
colors: textColor,
},
formatter: function(value) {
return Math.round(value) + ' %';
}
}
},
@@ -132,10 +149,9 @@
}
});
});
});
</script>
<h3>Memory (MB)</h3>
<h4>Memory Usage</h4>
<div wire:ignore id="{!! $chartId !!}-memory"></div>
<script>
@@ -143,6 +159,7 @@
const optionsServerMemory = {
stroke: {
curve: 'straight',
width: 2,
},
chart: {
height: '150px',
@@ -161,7 +178,7 @@
},
},
animations: {
enabled: false,
enabled: true,
},
},
fill: {
@@ -181,7 +198,7 @@
show: true,
borderColor: '',
},
colors: [baseColor],
colors: [ramColor],
xaxis: {
type: 'datetime',
labels: {
@@ -205,6 +222,21 @@
enabled: true,
marker: {
show: false,
},
custom: function({ series, seriesIndex, dataPointIndex, w }) {
const value = series[seriesIndex][dataPointIndex];
const timestamp = w.globals.seriesX[seriesIndex][dataPointIndex];
const date = new Date(timestamp);
const timeString = String(date.getUTCHours()).padStart(2, '0') + ':' +
String(date.getUTCMinutes()).padStart(2, '0') + ':' +
String(date.getUTCSeconds()).padStart(2, '0') + ', ' +
date.getUTCFullYear() + '-' +
String(date.getUTCMonth() + 1).padStart(2, '0') + '-' +
String(date.getUTCDate()).padStart(2, '0');
return '<div class="apexcharts-tooltip-custom">' +
'<div class="apexcharts-tooltip-custom-value">Memory: <span class="apexcharts-tooltip-value-bold">' + value + ' MB</span></div>' +
'<div class="apexcharts-tooltip-custom-title">' + timeString + '</div>' +
'</div>';
}
},
legend: {
@@ -214,14 +246,13 @@
const serverMemoryChart = new ApexCharts(document.getElementById(`{!! $chartId !!}-memory`),
optionsServerMemory);
serverMemoryChart.render();
document.addEventListener('livewire:init', () => {
Livewire.on('refreshChartData-{!! $chartId !!}-memory', (chartData) => {
checkTheme();
serverMemoryChart.updateOptions({
series: [{
data: chartData[0].seriesData,
}],
colors: [baseColor],
colors: [ramColor],
xaxis: {
type: 'datetime',
labels: {
@@ -238,6 +269,9 @@
show: true,
style: {
colors: textColor,
},
formatter: function(value) {
return Math.round(value) + ' MB';
}
}
},
@@ -249,9 +283,10 @@
}
});
});
});
</script>
</div>
</div>
@endif
@endif
</div>
</div>

View File

@@ -7,7 +7,7 @@
<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 class="pb-4">Basic metrics for your server.</div>
@if ($server->isMetricsEnabled())
<div @if ($poll) wire:poll.5000ms='pollData' @endif x-init="$wire.loadData()">
<x-forms.select label="Interval" wire:change="setInterval" id="interval">
@@ -19,7 +19,7 @@
<option value="10080">1 week</option>
<option value="43200">30 days</option>
</x-forms.select>
<h4 class="pt-4">CPU (%)</h4>
<h4 class="pt-4">CPU Usage</h4>
<div wire:ignore id="{!! $chartId !!}-cpu"></div>
<script>
@@ -27,6 +27,7 @@
const optionsServerCpu = {
stroke: {
curve: 'straight',
width: 2,
},
chart: {
height: '150px',
@@ -45,7 +46,7 @@
},
},
animations: {
enabled: false,
enabled: true,
},
},
fill: {
@@ -65,7 +66,7 @@
show: true,
borderColor: '',
},
colors: [baseColor],
colors: [cpuColor],
xaxis: {
type: 'datetime',
},
@@ -83,6 +84,21 @@
enabled: true,
marker: {
show: false,
},
custom: function({ series, seriesIndex, dataPointIndex, w }) {
const value = series[seriesIndex][dataPointIndex];
const timestamp = w.globals.seriesX[seriesIndex][dataPointIndex];
const date = new Date(timestamp);
const timeString = String(date.getUTCHours()).padStart(2, '0') + ':' +
String(date.getUTCMinutes()).padStart(2, '0') + ':' +
String(date.getUTCSeconds()).padStart(2, '0') + ', ' +
date.getUTCFullYear() + '-' +
String(date.getUTCMonth() + 1).padStart(2, '0') + '-' +
String(date.getUTCDate()).padStart(2, '0');
return '<div class="apexcharts-tooltip-custom">' +
'<div class="apexcharts-tooltip-custom-value">CPU: <span class="apexcharts-tooltip-value-bold">' + value + '%</span></div>' +
'<div class="apexcharts-tooltip-custom-title">' + timeString + '</div>' +
'</div>';
}
},
legend: {
@@ -99,7 +115,7 @@
series: [{
data: chartData[0].seriesData,
}],
colors: [baseColor],
colors: [cpuColor],
xaxis: {
type: 'datetime',
labels: {
@@ -115,6 +131,9 @@
show: true,
style: {
colors: textColor,
},
formatter: function(value) {
return Math.round(value) + ' %';
}
}
},
@@ -130,7 +149,7 @@
</script>
<div>
<h4>Memory (%)</h4>
<h4>Memory Usage</h4>
<div wire:ignore id="{!! $chartId !!}-memory"></div>
<script>
@@ -138,6 +157,7 @@
const optionsServerMemory = {
stroke: {
curve: 'straight',
width: 2,
},
chart: {
height: '150px',
@@ -156,7 +176,7 @@
},
},
animations: {
enabled: false,
enabled: true,
},
},
fill: {
@@ -176,7 +196,7 @@
show: true,
borderColor: '',
},
colors: [baseColor],
colors: [ramColor],
xaxis: {
type: 'datetime',
labels: {
@@ -200,6 +220,21 @@
enabled: true,
marker: {
show: false,
},
custom: function({ series, seriesIndex, dataPointIndex, w }) {
const value = series[seriesIndex][dataPointIndex];
const timestamp = w.globals.seriesX[seriesIndex][dataPointIndex];
const date = new Date(timestamp);
const timeString = String(date.getUTCHours()).padStart(2, '0') + ':' +
String(date.getUTCMinutes()).padStart(2, '0') + ':' +
String(date.getUTCSeconds()).padStart(2, '0') + ', ' +
date.getUTCFullYear() + '-' +
String(date.getUTCMonth() + 1).padStart(2, '0') + '-' +
String(date.getUTCDate()).padStart(2, '0');
return '<div class="apexcharts-tooltip-custom">' +
'<div class="apexcharts-tooltip-custom-value">Memory: <span class="apexcharts-tooltip-value-bold">' + value + '%</span></div>' +
'<div class="apexcharts-tooltip-custom-title">' + timeString + '</div>' +
'</div>';
}
},
legend: {
@@ -216,7 +251,7 @@
series: [{
data: chartData[0].seriesData,
}],
colors: [baseColor],
colors: [ramColor],
xaxis: {
type: 'datetime',
labels: {
@@ -233,6 +268,9 @@
show: true,
style: {
colors: textColor,
},
formatter: function(value) {
return Math.round(value) + ' %';
}
}
},

View File

@@ -7,23 +7,23 @@
<div class="flex items-center gap-2">
<h2>Configuration</h2>
@if ($server->proxy->status === 'exited' || $server->proxy->status === 'removing')
<x-forms.button canGate="update" :canResource="$server" wire:click.prevent="changeProxy">Switch
Proxy</x-forms.button>
@can('update', $server)
<x-modal-confirmation title="Confirm Proxy Switching?"
buttonTitle="Switch Proxy"
submitAction="changeProxy" :actions="[
'Custom proxy configurations may be reset to their default settings.'
]" warningMessage="This operation may cause issues. Please refer to the guide <a href='https://coolify.io/docs/knowledge-base/server/proxies#switch-between-proxies' target='_blank' class='underline text-white'>switching between proxies</a> before proceeding!" step2ButtonText="Switch Proxy" :confirmWithText="false" :confirmWithPassword="false">
</x-modal-confirmation>
@endcan
@else
<x-forms.button canGate="update" :canResource="$server" disabled
wire:click.prevent="changeProxy">Switch Proxy</x-forms.button>
<x-forms.button canGate="update" :canResource="$server"
wire:click="$dispatch('error', 'Currently running proxy must be stopped before switching proxy')">Switch Proxy</x-forms.button>
@endif
<x-forms.button canGate="update" :canResource="$server" type="submit">Save</x-forms.button>
</div>
<div class="pb-4 "> <svg class="inline-flex w-6 h-6 mr-2 dark:text-warning" viewBox="0 0 256 256"
xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor"
d="M240.26 186.1L152.81 34.23a28.74 28.74 0 0 0-49.62 0L15.74 186.1a27.45 27.45 0 0 0 0 27.71A28.31 28.31 0 0 0 40.55 228h174.9a28.31 28.31 0 0 0 24.79-14.19a27.45 27.45 0 0 0 .02-27.71m-20.8 15.7a4.46 4.46 0 0 1-4 2.2H40.55a4.46 4.46 0 0 1-4-2.2a3.56 3.56 0 0 1 0-3.73L124 46.2a4.77 4.77 0 0 1 8 0l87.44 151.87a3.56 3.56 0 0 1 .02 3.73M116 136v-32a12 12 0 0 1 24 0v32a12 12 0 0 1-24 0m28 40a16 16 0 1 1-16-16a16 16 0 0 1 16 16" />
</svg>Before switching proxies, please read <a class="underline dark:text-white"
href="https://coolify.io/docs/knowledge-base/server/proxies#switch-between-proxies">this</a>.
</div>
<div class="subtitle">Configure your proxy settings and advanced options.</div>
<h3>Advanced</h3>
<div class="pb-4 w-96">
<div class="pb-6 w-96">
<x-forms.checkbox canGate="update" :canResource="$server"
helper="If set, all resources will only have docker container labels for {{ str($server->proxyType())->title() }}.<br>For applications, labels needs to be regenerated manually. <br>Resources needs to be restarted."
id="server.settings.generate_exact_labels"
@@ -36,10 +36,29 @@
id="redirectUrl" label="Redirect to (optional)" />
@endif
</div>
@if ($server->proxyType() === ProxyTypes::TRAEFIK->value)
<h3>Traefik</h3>
@elseif ($server->proxyType() === 'CADDY')
<h3>Caddy</h3>
@php
$proxyTitle = $server->proxyType() === ProxyTypes::TRAEFIK->value ? 'Traefik (Coolify Proxy)' : 'Caddy (Coolify Proxy)';
@endphp
@if ($server->proxyType() === ProxyTypes::TRAEFIK->value || $server->proxyType() === 'CADDY')
<div class="flex items-center gap-2">
<h3>{{ $proxyTitle }}</h3>
@if($proxySettings)
@can('update', $server)
<x-modal-confirmation title="Reset Proxy Configuration?"
buttonTitle="Reset Configuration"
submitAction="resetProxyConfiguration" :actions="[
'Reset proxy configuration to default settings',
'All custom configurations will be lost',
'Custom ports and entrypoints will be removed',
]"
confirmationText="{{ $server->name }}"
confirmationLabel="Please confirm by entering the server name below"
shortConfirmationLabel="Server Name" step2ButtonText="Reset Configuration"
:confirmWithPassword="false" :confirmWithText="true">
</x-modal-confirmation>
@endcan
@endif
</div>
@endif
@if (
$server->proxy->last_applied_settings &&
@@ -73,6 +92,12 @@
</x-modal-confirmation>
@endcan
</div>
<div class="flex flex-col gap-2 pt-2">
<x-forms.textarea canGate="update" :canResource="$server" useMonacoEditor
monacoEditorLanguage="yaml"
label="Configuration file ({{ $this->configurationFilePath }})" name="proxySettings"
id="proxySettings" rows="30" />
</div>
@endif
</div>
</form>