Merge branch 'next' into shadow/fix-typo-slash-proxy-page
This commit is contained in:
@@ -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.
|
- 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.
|
- 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>
|
</laravel-boost-guidelines>
|
||||||
|
|
||||||
|
|
||||||
|
Random other things you should remember:
|
||||||
|
- App\Models\Application::team must return a relationship instance., always use team()
|
35
app/Events/ApplicationConfigurationChanged.php
Normal file
35
app/Events/ApplicationConfigurationChanged.php
Normal 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}"),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@@ -5,6 +5,7 @@ namespace App\Jobs;
|
|||||||
use App\Actions\Docker\GetContainersStatus;
|
use App\Actions\Docker\GetContainersStatus;
|
||||||
use App\Enums\ApplicationDeploymentStatus;
|
use App\Enums\ApplicationDeploymentStatus;
|
||||||
use App\Enums\ProcessStatus;
|
use App\Enums\ProcessStatus;
|
||||||
|
use App\Events\ApplicationConfigurationChanged;
|
||||||
use App\Events\ServiceStatusChanged;
|
use App\Events\ServiceStatusChanged;
|
||||||
use App\Models\Application;
|
use App\Models\Application;
|
||||||
use App\Models\ApplicationDeploymentQueue;
|
use App\Models\ApplicationDeploymentQueue;
|
||||||
@@ -147,6 +148,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
|||||||
|
|
||||||
private Collection $saved_outputs;
|
private Collection $saved_outputs;
|
||||||
|
|
||||||
|
private ?string $secrets_hash_key = null;
|
||||||
|
|
||||||
private ?string $full_healthcheck_url = null;
|
private ?string $full_healthcheck_url = null;
|
||||||
|
|
||||||
private string $serverUser = 'root';
|
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"),
|
executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d | tee {$this->workdir}{$this->docker_compose_location} > /dev/null"),
|
||||||
'hidden' => true,
|
'hidden' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Modify Dockerfiles for ARGs and build secrets
|
||||||
|
$this->modify_dockerfiles_for_compose($composeFile);
|
||||||
// Build new container to limit downtime.
|
// Build new container to limit downtime.
|
||||||
$this->application_deployment_queue->addLogEntry('Pulling & building required images.');
|
$this->application_deployment_queue->addLogEntry('Pulling & building required images.');
|
||||||
|
|
||||||
@@ -632,6 +638,13 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
|||||||
} else {
|
} else {
|
||||||
$command .= " --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build --pull";
|
$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(
|
$this->execute_remote_command(
|
||||||
[executeInDocker($this->deployment_uuid, $command), 'hidden' => true],
|
[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') {
|
if ($this->application->build_pack === 'nixpacks') {
|
||||||
$variables = collect($this->nixpacks_plan_json->get('variables'));
|
$variables = collect($this->nixpacks_plan_json->get('variables'));
|
||||||
} else {
|
} else {
|
||||||
// Generate environment variables for build process (filters by is_buildtime = true)
|
|
||||||
$this->generate_env_variables();
|
$this->generate_env_variables();
|
||||||
$variables = collect([])->merge($this->env_args);
|
$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) {
|
if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) {
|
||||||
$this->generate_build_secrets($variables);
|
$this->generate_build_secrets($variables);
|
||||||
$this->build_args = '';
|
$this->build_args = '';
|
||||||
} else {
|
} 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) {
|
$this->build_args = $variables->map(function ($value, $key) {
|
||||||
$value = escapeshellarg($value);
|
$value = escapeshellarg($value);
|
||||||
|
|
||||||
return "--build-arg {$key}={$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 '';
|
||||||
}
|
}
|
||||||
|
|
||||||
return $variables
|
$secrets_hash = $this->generate_secrets_hash($variables);
|
||||||
|
$env_flags = $variables
|
||||||
->map(function ($env) {
|
->map(function ($env) {
|
||||||
$escaped_value = escapeshellarg($env->real_value);
|
$escaped_value = escapeshellarg($env->real_value);
|
||||||
|
|
||||||
return "-e {$env->key}={$escaped_value}";
|
return "-e {$env->key}={$escaped_value}";
|
||||||
})
|
})
|
||||||
->implode(' ');
|
->implode(' ');
|
||||||
|
|
||||||
|
$env_flags .= " -e COOLIFY_BUILD_SECRETS_HASH={$secrets_hash}";
|
||||||
|
|
||||||
|
return $env_flags;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function generate_build_secrets(Collection $variables)
|
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}";
|
return "--secret id={$key},env={$key}";
|
||||||
})
|
})
|
||||||
->implode(' ');
|
->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()
|
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"));
|
$dockerfile_base64 = base64_encode($dockerfile->implode("\n"));
|
||||||
$this->execute_remote_command([
|
$this->execute_remote_command([
|
||||||
executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d | tee {$this->workdir}{$this->dockerfile_location} > /dev/null"),
|
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
|
// Get environment variables for secrets
|
||||||
$variables = $this->pull_request_id === 0
|
$variables = $this->pull_request_id === 0
|
||||||
? $this->application->environment_variables()->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_%')->get();
|
: $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->where('is_buildtime', true)->get();
|
||||||
|
|
||||||
if ($variables->isEmpty()) {
|
if ($variables->isEmpty()) {
|
||||||
return;
|
return;
|
||||||
@@ -2840,6 +2900,9 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
|||||||
// Generate mount strings for all secrets
|
// Generate mount strings for all secrets
|
||||||
$mountStrings = $variables->map(fn ($env) => "--mount=type=secret,id={$env->key},env={$env->key}")->implode(' ');
|
$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;
|
$modified = false;
|
||||||
$dockerfile = $dockerfile->map(function ($line) use ($mountStrings, &$modified) {
|
$dockerfile = $dockerfile->map(function ($line) use ($mountStrings, &$modified) {
|
||||||
$trimmed = ltrim($line);
|
$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)
|
private function add_build_secrets_to_compose($composeFile)
|
||||||
{
|
{
|
||||||
// Get environment variables for secrets
|
// Get environment variables for secrets
|
||||||
@@ -3018,6 +3239,9 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
|||||||
queue_next_deployment($this->application);
|
queue_next_deployment($this->application);
|
||||||
|
|
||||||
if ($status === ApplicationDeploymentStatus::FINISHED->value) {
|
if ($status === ApplicationDeploymentStatus::FINISHED->value) {
|
||||||
|
ray($this->application->team()->id);
|
||||||
|
event(new ApplicationConfigurationChanged($this->application->team()->id));
|
||||||
|
|
||||||
if (! $this->only_this_server) {
|
if (! $this->only_this_server) {
|
||||||
$this->deploy_to_additional_destinations();
|
$this->deploy_to_additional_destinations();
|
||||||
}
|
}
|
||||||
|
372
app/Livewire/GlobalSearch.php
Normal file
372
app/Livewire/GlobalSearch.php
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
@@ -20,7 +20,15 @@ class ConfigurationChecker extends Component
|
|||||||
|
|
||||||
public Application|Service|StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource;
|
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()
|
public function mount()
|
||||||
{
|
{
|
||||||
|
@@ -8,7 +8,7 @@ class Metrics extends Component
|
|||||||
{
|
{
|
||||||
public $resource;
|
public $resource;
|
||||||
|
|
||||||
public $chartId = 'container-cpu';
|
public $chartId = 'metrics';
|
||||||
|
|
||||||
public $data;
|
public $data;
|
||||||
|
|
||||||
|
@@ -4,6 +4,7 @@ namespace App\Models;
|
|||||||
|
|
||||||
use App\Enums\ApplicationDeploymentStatus;
|
use App\Enums\ApplicationDeploymentStatus;
|
||||||
use App\Services\ConfigurationGenerator;
|
use App\Services\ConfigurationGenerator;
|
||||||
|
use App\Traits\ClearsGlobalSearchCache;
|
||||||
use App\Traits\HasConfiguration;
|
use App\Traits\HasConfiguration;
|
||||||
use App\Traits\HasSafeStringAttribute;
|
use App\Traits\HasSafeStringAttribute;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
@@ -110,7 +111,7 @@ use Visus\Cuid2\Cuid2;
|
|||||||
|
|
||||||
class Application extends BaseModel
|
class Application extends BaseModel
|
||||||
{
|
{
|
||||||
use HasConfiguration, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
use ClearsGlobalSearchCache, HasConfiguration, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||||
|
|
||||||
private static $parserVersion = '5';
|
private static $parserVersion = '5';
|
||||||
|
|
||||||
@@ -123,66 +124,6 @@ class Application extends BaseModel
|
|||||||
'http_basic_auth_password' => 'encrypted',
|
'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()
|
protected static function booted()
|
||||||
{
|
{
|
||||||
static::addGlobalScope('withRelations', function ($builder) {
|
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)
|
public static function ownedByCurrentTeamAPI(int $teamId)
|
||||||
{
|
{
|
||||||
return Application::whereRelation('environment.project.team', 'id', $teamId)->orderBy('name');
|
return Application::whereRelation('environment.project.team', 'id', $teamId)->orderBy('name');
|
||||||
|
@@ -85,6 +85,47 @@ class ApplicationDeploymentQueue extends Model
|
|||||||
return str($this->commit_message)->value();
|
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)
|
public function addLogEntry(string $message, string $type = 'stdout', bool $hidden = false)
|
||||||
{
|
{
|
||||||
if ($type === 'error') {
|
if ($type === 'error') {
|
||||||
@@ -96,7 +137,7 @@ class ApplicationDeploymentQueue extends Model
|
|||||||
}
|
}
|
||||||
$newLogEntry = [
|
$newLogEntry = [
|
||||||
'command' => null,
|
'command' => null,
|
||||||
'output' => remove_iip($message),
|
'output' => $this->redactSensitiveInfo($message),
|
||||||
'type' => $type,
|
'type' => $type,
|
||||||
'timestamp' => Carbon::now('UTC'),
|
'timestamp' => Carbon::now('UTC'),
|
||||||
'hidden' => $hidden,
|
'hidden' => $hidden,
|
||||||
|
@@ -13,6 +13,7 @@ use App\Jobs\RegenerateSslCertJob;
|
|||||||
use App\Notifications\Server\Reachable;
|
use App\Notifications\Server\Reachable;
|
||||||
use App\Notifications\Server\Unreachable;
|
use App\Notifications\Server\Unreachable;
|
||||||
use App\Services\ConfigurationRepository;
|
use App\Services\ConfigurationRepository;
|
||||||
|
use App\Traits\ClearsGlobalSearchCache;
|
||||||
use App\Traits\HasSafeStringAttribute;
|
use App\Traits\HasSafeStringAttribute;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
@@ -55,7 +56,7 @@ use Visus\Cuid2\Cuid2;
|
|||||||
|
|
||||||
class Server extends BaseModel
|
class Server extends BaseModel
|
||||||
{
|
{
|
||||||
use HasFactory, SchemalessAttributesTrait, SoftDeletes;
|
use ClearsGlobalSearchCache, HasFactory, SchemalessAttributesTrait, SoftDeletes;
|
||||||
|
|
||||||
public static $batch_counter = 0;
|
public static $batch_counter = 0;
|
||||||
|
|
||||||
|
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Enums\ProcessStatus;
|
use App\Enums\ProcessStatus;
|
||||||
|
use App\Traits\ClearsGlobalSearchCache;
|
||||||
use App\Traits\HasSafeStringAttribute;
|
use App\Traits\HasSafeStringAttribute;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
@@ -41,7 +42,7 @@ use Visus\Cuid2\Cuid2;
|
|||||||
)]
|
)]
|
||||||
class Service extends BaseModel
|
class Service extends BaseModel
|
||||||
{
|
{
|
||||||
use HasFactory, HasSafeStringAttribute, SoftDeletes;
|
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||||
|
|
||||||
private static $parserVersion = '5';
|
private static $parserVersion = '5';
|
||||||
|
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Traits\ClearsGlobalSearchCache;
|
||||||
use App\Traits\HasSafeStringAttribute;
|
use App\Traits\HasSafeStringAttribute;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
@@ -9,7 +10,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
|||||||
|
|
||||||
class StandaloneClickhouse extends BaseModel
|
class StandaloneClickhouse extends BaseModel
|
||||||
{
|
{
|
||||||
use HasFactory, HasSafeStringAttribute, SoftDeletes;
|
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||||
|
|
||||||
protected $guarded = [];
|
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
|
protected function serverStatus(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::make(
|
return Attribute::make(
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Traits\ClearsGlobalSearchCache;
|
||||||
use App\Traits\HasSafeStringAttribute;
|
use App\Traits\HasSafeStringAttribute;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
@@ -9,7 +10,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
|||||||
|
|
||||||
class StandaloneDragonfly extends BaseModel
|
class StandaloneDragonfly extends BaseModel
|
||||||
{
|
{
|
||||||
use HasFactory, HasSafeStringAttribute, SoftDeletes;
|
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||||
|
|
||||||
protected $guarded = [];
|
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
|
protected function serverStatus(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::make(
|
return Attribute::make(
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Traits\ClearsGlobalSearchCache;
|
||||||
use App\Traits\HasSafeStringAttribute;
|
use App\Traits\HasSafeStringAttribute;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
@@ -9,7 +10,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
|||||||
|
|
||||||
class StandaloneKeydb extends BaseModel
|
class StandaloneKeydb extends BaseModel
|
||||||
{
|
{
|
||||||
use HasFactory, HasSafeStringAttribute, SoftDeletes;
|
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||||
|
|
||||||
protected $guarded = [];
|
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
|
protected function serverStatus(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::make(
|
return Attribute::make(
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Traits\ClearsGlobalSearchCache;
|
||||||
use App\Traits\HasSafeStringAttribute;
|
use App\Traits\HasSafeStringAttribute;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
@@ -10,7 +11,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
|||||||
|
|
||||||
class StandaloneMariadb extends BaseModel
|
class StandaloneMariadb extends BaseModel
|
||||||
{
|
{
|
||||||
use HasFactory, HasSafeStringAttribute, SoftDeletes;
|
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||||
|
|
||||||
protected $guarded = [];
|
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
|
protected function serverStatus(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::make(
|
return Attribute::make(
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Traits\ClearsGlobalSearchCache;
|
||||||
use App\Traits\HasSafeStringAttribute;
|
use App\Traits\HasSafeStringAttribute;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
@@ -9,7 +10,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
|||||||
|
|
||||||
class StandaloneMongodb extends BaseModel
|
class StandaloneMongodb extends BaseModel
|
||||||
{
|
{
|
||||||
use HasFactory, HasSafeStringAttribute, SoftDeletes;
|
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||||
|
|
||||||
protected $guarded = [];
|
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
|
protected function serverStatus(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::make(
|
return Attribute::make(
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Traits\ClearsGlobalSearchCache;
|
||||||
use App\Traits\HasSafeStringAttribute;
|
use App\Traits\HasSafeStringAttribute;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
@@ -9,7 +10,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
|||||||
|
|
||||||
class StandaloneMysql extends BaseModel
|
class StandaloneMysql extends BaseModel
|
||||||
{
|
{
|
||||||
use HasFactory, HasSafeStringAttribute, SoftDeletes;
|
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||||
|
|
||||||
protected $guarded = [];
|
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
|
protected function serverStatus(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::make(
|
return Attribute::make(
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Traits\ClearsGlobalSearchCache;
|
||||||
use App\Traits\HasSafeStringAttribute;
|
use App\Traits\HasSafeStringAttribute;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
@@ -9,7 +10,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
|||||||
|
|
||||||
class StandalonePostgresql extends BaseModel
|
class StandalonePostgresql extends BaseModel
|
||||||
{
|
{
|
||||||
use HasFactory, HasSafeStringAttribute, SoftDeletes;
|
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||||
|
|
||||||
protected $guarded = [];
|
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()
|
public function workdir()
|
||||||
{
|
{
|
||||||
return database_configuration_dir()."/{$this->uuid}";
|
return database_configuration_dir()."/{$this->uuid}";
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Traits\ClearsGlobalSearchCache;
|
||||||
use App\Traits\HasSafeStringAttribute;
|
use App\Traits\HasSafeStringAttribute;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
@@ -9,7 +10,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
|||||||
|
|
||||||
class StandaloneRedis extends BaseModel
|
class StandaloneRedis extends BaseModel
|
||||||
{
|
{
|
||||||
use HasFactory, HasSafeStringAttribute, SoftDeletes;
|
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||||
|
|
||||||
protected $guarded = [];
|
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
|
protected function serverStatus(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::make(
|
return Attribute::make(
|
||||||
|
81
app/Traits/ClearsGlobalSearchCache.php
Normal file
81
app/Traits/ClearsGlobalSearchCache.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@@ -17,6 +17,46 @@ trait ExecuteRemoteCommand
|
|||||||
|
|
||||||
public static int $batch_counter = 0;
|
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)
|
public function execute_remote_command(...$commands)
|
||||||
{
|
{
|
||||||
static::$batch_counter++;
|
static::$batch_counter++;
|
||||||
@@ -74,7 +114,7 @@ trait ExecuteRemoteCommand
|
|||||||
// Track SSH retry event in Sentry
|
// Track SSH retry event in Sentry
|
||||||
$this->trackSshRetryEvent($attempt, $maxRetries, $delay, $errorMessage, [
|
$this->trackSshRetryEvent($attempt, $maxRetries, $delay, $errorMessage, [
|
||||||
'server' => $this->server->name ?? $this->server->ip ?? 'unknown',
|
'server' => $this->server->name ?? $this->server->ip ?? 'unknown',
|
||||||
'command' => remove_iip($command),
|
'command' => $this->redact_sensitive_info($command),
|
||||||
'trait' => 'ExecuteRemoteCommand',
|
'trait' => 'ExecuteRemoteCommand',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -125,8 +165,8 @@ trait ExecuteRemoteCommand
|
|||||||
$sanitized_output = sanitize_utf8_text($output);
|
$sanitized_output = sanitize_utf8_text($output);
|
||||||
|
|
||||||
$new_log_entry = [
|
$new_log_entry = [
|
||||||
'command' => remove_iip($command),
|
'command' => $this->redact_sensitive_info($command),
|
||||||
'output' => remove_iip($sanitized_output),
|
'output' => $this->redact_sensitive_info($sanitized_output),
|
||||||
'type' => $customType ?? $type === 'err' ? 'stderr' : 'stdout',
|
'type' => $customType ?? $type === 'err' ? 'stderr' : 'stdout',
|
||||||
'timestamp' => Carbon::now('UTC'),
|
'timestamp' => Carbon::now('UTC'),
|
||||||
'hidden' => $hidden,
|
'hidden' => $hidden,
|
||||||
@@ -194,7 +234,7 @@ trait ExecuteRemoteCommand
|
|||||||
$retryMessage = "SSH connection failed. Retrying... (Attempt {$attempt}/{$maxRetries}, waiting {$delay}s)\nError: {$errorMessage}";
|
$retryMessage = "SSH connection failed. Retrying... (Attempt {$attempt}/{$maxRetries}, waiting {$delay}s)\nError: {$errorMessage}";
|
||||||
|
|
||||||
$new_log_entry = [
|
$new_log_entry = [
|
||||||
'output' => remove_iip($retryMessage),
|
'output' => $this->redact_sensitive_info($retryMessage),
|
||||||
'type' => 'stdout',
|
'type' => 'stdout',
|
||||||
'timestamp' => Carbon::now('UTC'),
|
'timestamp' => Carbon::now('UTC'),
|
||||||
'hidden' => false,
|
'hidden' => false,
|
||||||
|
@@ -6,10 +6,31 @@
|
|||||||
@apply hidden!;
|
@apply hidden!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@utility apexcharts-grid-borders {
|
||||||
|
@apply dark:hidden!;
|
||||||
|
}
|
||||||
|
|
||||||
@utility apexcharts-xaxistooltip {
|
@utility apexcharts-xaxistooltip {
|
||||||
@apply hidden!;
|
@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 {
|
@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;
|
@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;
|
||||||
}
|
}
|
||||||
|
@@ -11,6 +11,7 @@
|
|||||||
'content' => null,
|
'content' => null,
|
||||||
'checkboxes' => [],
|
'checkboxes' => [],
|
||||||
'actions' => [],
|
'actions' => [],
|
||||||
|
'warningMessage' => null,
|
||||||
'confirmWithText' => true,
|
'confirmWithText' => true,
|
||||||
'confirmationText' => 'Confirm Deletion',
|
'confirmationText' => 'Confirm Deletion',
|
||||||
'confirmationLabel' => 'Please confirm the execution of the actions by entering the Name below',
|
'confirmationLabel' => 'Please confirm the execution of the actions by entering the Name below',
|
||||||
@@ -228,7 +229,7 @@
|
|||||||
<div x-show="step === 2">
|
<div x-show="step === 2">
|
||||||
<div class="p-4 mb-4 text-white border-l-4 border-red-500 bg-error" role="alert">
|
<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 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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">The following actions will be performed:</div>
|
<div class="mb-4">The following actions will be performed:</div>
|
||||||
|
@@ -82,6 +82,9 @@
|
|||||||
<div class="text-2xl font-bold tracking-wide dark:text-white">Coolify</div>
|
<div class="text-2xl font-bold tracking-wide dark:text-white">Coolify</div>
|
||||||
<x-version />
|
<x-version />
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<livewire:global-search />
|
||||||
|
</div>
|
||||||
<livewire:settings-dropdown />
|
<livewire:settings-dropdown />
|
||||||
</div>
|
</div>
|
||||||
<div class="px-2 pt-2 pb-7">
|
<div class="px-2 pt-2 pb-7">
|
||||||
|
@@ -138,7 +138,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let theme = localStorage.theme
|
let theme = localStorage.theme
|
||||||
let baseColor = '#FCD452'
|
let cpuColor = '#1e90ff'
|
||||||
|
let ramColor = '#00ced1'
|
||||||
let textColor = '#ffffff'
|
let textColor = '#ffffff'
|
||||||
let editorBackground = '#181818'
|
let editorBackground = '#181818'
|
||||||
let editorTheme = 'blackboard'
|
let editorTheme = 'blackboard'
|
||||||
@@ -149,12 +150,14 @@
|
|||||||
theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||||
}
|
}
|
||||||
if (theme == 'dark') {
|
if (theme == 'dark') {
|
||||||
baseColor = '#FCD452'
|
cpuColor = '#1e90ff'
|
||||||
|
ramColor = '#00ced1'
|
||||||
textColor = '#ffffff'
|
textColor = '#ffffff'
|
||||||
editorBackground = '#181818'
|
editorBackground = '#181818'
|
||||||
editorTheme = 'blackboard'
|
editorTheme = 'blackboard'
|
||||||
} else {
|
} else {
|
||||||
baseColor = 'black'
|
cpuColor = '#1e90ff'
|
||||||
|
ramColor = '#00ced1'
|
||||||
textColor = '#000000'
|
textColor = '#000000'
|
||||||
editorBackground = '#ffffff'
|
editorBackground = '#ffffff'
|
||||||
editorTheme = null
|
editorTheme = null
|
||||||
|
236
resources/views/livewire/global-search.blade.php
Normal file
236
resources/views/livewire/global-search.blade.php
Normal 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>
|
@@ -28,7 +28,6 @@
|
|||||||
@endcan
|
@endcan
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
@if (data_get($resource, 'build_pack') !== 'dockercompose')
|
|
||||||
<div class="w-64">
|
<div class="w-64">
|
||||||
@can('manageEnvironment', $resource)
|
@can('manageEnvironment', $resource)
|
||||||
<x-forms.checkbox id="use_build_secrets" label="Use Docker Build Secrets"
|
<x-forms.checkbox id="use_build_secrets" label="Use Docker Build Secrets"
|
||||||
@@ -40,7 +39,6 @@
|
|||||||
disabled></x-forms.checkbox>
|
disabled></x-forms.checkbox>
|
||||||
@endcan
|
@endcan
|
||||||
</div>
|
</div>
|
||||||
@endif
|
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
@if ($resource->type() === 'service' || $resource?->build_pack === 'dockercompose')
|
@if ($resource->type() === 'service' || $resource?->build_pack === 'dockercompose')
|
||||||
|
@@ -7,7 +7,7 @@
|
|||||||
@if ($isLocked)
|
@if ($isLocked)
|
||||||
<div class="flex flex-1 w-full gap-2">
|
<div class="flex flex-1 w-full gap-2">
|
||||||
<x-forms.input disabled id="key" />
|
<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">
|
<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="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" />
|
<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" />
|
step2ButtonText="Permanently Delete" />
|
||||||
@endcan
|
@endcan
|
||||||
</div>
|
</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
|
@else
|
||||||
@can('update', $this->env)
|
@can('update', $this->env)
|
||||||
@if ($isDisabled)
|
@if ($isDisabled)
|
||||||
@@ -78,7 +167,6 @@
|
|||||||
@if ($isSharedVariable)
|
@if ($isSharedVariable)
|
||||||
<x-forms.checkbox instantSave id="is_multiline" label="Is Multiline?" />
|
<x-forms.checkbox instantSave id="is_multiline" label="Is Multiline?" />
|
||||||
@else
|
@else
|
||||||
@if (!$env->is_coolify)
|
|
||||||
@if (!$env->is_nixpacks)
|
@if (!$env->is_nixpacks)
|
||||||
<x-forms.checkbox instantSave id="is_buildtime"
|
<x-forms.checkbox instantSave id="is_buildtime"
|
||||||
helper="Make this variable available during Docker build process. Useful for build secrets and dependencies."
|
helper="Make this variable available during Docker build process. Useful for build secrets and dependencies."
|
||||||
@@ -99,7 +187,6 @@
|
|||||||
@endif
|
@endif
|
||||||
@endif
|
@endif
|
||||||
@endif
|
@endif
|
||||||
@endif
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex w-full justify-end gap-2">
|
<div class="flex w-full justify-end gap-2">
|
||||||
@if ($isDisabled)
|
@if ($isDisabled)
|
||||||
|
@@ -2,19 +2,18 @@
|
|||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<h2>Metrics</h2>
|
<h2>Metrics</h2>
|
||||||
</div>
|
</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')
|
@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>
|
<div class="alert alert-warning">Metrics are not available for Docker Compose applications yet!</div>
|
||||||
@elseif(!$resource->destination->server->isMetricsEnabled())
|
@elseif(!$resource->destination->server->isMetricsEnabled())
|
||||||
<div class="alert alert-warning">Metrics are only available for servers with Sentinel & Metrics enabled!</div>
|
<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"
|
<div>Go to <a class="underline dark:text-white" href="{{ route('server.show', $resource->destination->server->uuid) }}">Server settings</a> to enable it.</div>
|
||||||
href="{{ route('server.show', $resource->destination->server->uuid) }}">Server settings</a> to
|
|
||||||
enable
|
|
||||||
it.</div>
|
|
||||||
@else
|
@else
|
||||||
@if (!str($resource->status)->contains('running'))
|
@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
|
@else
|
||||||
|
<div>
|
||||||
<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>
|
||||||
@@ -26,7 +25,7 @@
|
|||||||
</x-forms.select>
|
</x-forms.select>
|
||||||
<div @if ($poll) wire:poll.5000ms='pollData' @endif x-init="$wire.loadData()"
|
<div @if ($poll) wire:poll.5000ms='pollData' @endif x-init="$wire.loadData()"
|
||||||
class="pt-5">
|
class="pt-5">
|
||||||
<h4>CPU (%)</h4>
|
<h4>CPU Usage</h4>
|
||||||
<div wire:ignore id="{!! $chartId !!}-cpu"></div>
|
<div wire:ignore id="{!! $chartId !!}-cpu"></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -34,6 +33,7 @@
|
|||||||
const optionsServerCpu = {
|
const optionsServerCpu = {
|
||||||
stroke: {
|
stroke: {
|
||||||
curve: 'straight',
|
curve: 'straight',
|
||||||
|
width: 2,
|
||||||
},
|
},
|
||||||
chart: {
|
chart: {
|
||||||
height: '150px',
|
height: '150px',
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
animations: {
|
animations: {
|
||||||
enabled: false,
|
enabled: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
fill: {
|
fill: {
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
show: true,
|
show: true,
|
||||||
borderColor: '',
|
borderColor: '',
|
||||||
},
|
},
|
||||||
colors: [baseColor],
|
colors: [cpuColor],
|
||||||
xaxis: {
|
xaxis: {
|
||||||
type: 'datetime',
|
type: 'datetime',
|
||||||
},
|
},
|
||||||
@@ -90,6 +90,21 @@
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
marker: {
|
marker: {
|
||||||
show: false,
|
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: {
|
legend: {
|
||||||
@@ -98,14 +113,13 @@
|
|||||||
}
|
}
|
||||||
const serverCpuChart = new ApexCharts(document.getElementById(`{!! $chartId !!}-cpu`), optionsServerCpu);
|
const serverCpuChart = new ApexCharts(document.getElementById(`{!! $chartId !!}-cpu`), optionsServerCpu);
|
||||||
serverCpuChart.render();
|
serverCpuChart.render();
|
||||||
document.addEventListener('livewire:init', () => {
|
|
||||||
Livewire.on('refreshChartData-{!! $chartId !!}-cpu', (chartData) => {
|
Livewire.on('refreshChartData-{!! $chartId !!}-cpu', (chartData) => {
|
||||||
checkTheme();
|
checkTheme();
|
||||||
serverCpuChart.updateOptions({
|
serverCpuChart.updateOptions({
|
||||||
series: [{
|
series: [{
|
||||||
data: chartData[0].seriesData,
|
data: chartData[0].seriesData,
|
||||||
}],
|
}],
|
||||||
colors: [baseColor],
|
colors: [cpuColor],
|
||||||
xaxis: {
|
xaxis: {
|
||||||
type: 'datetime',
|
type: 'datetime',
|
||||||
labels: {
|
labels: {
|
||||||
@@ -121,6 +135,9 @@
|
|||||||
show: true,
|
show: true,
|
||||||
style: {
|
style: {
|
||||||
colors: textColor,
|
colors: textColor,
|
||||||
|
},
|
||||||
|
formatter: function(value) {
|
||||||
|
return Math.round(value) + ' %';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -132,10 +149,9 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h3>Memory (MB)</h3>
|
<h4>Memory Usage</h4>
|
||||||
<div wire:ignore id="{!! $chartId !!}-memory"></div>
|
<div wire:ignore id="{!! $chartId !!}-memory"></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -143,6 +159,7 @@
|
|||||||
const optionsServerMemory = {
|
const optionsServerMemory = {
|
||||||
stroke: {
|
stroke: {
|
||||||
curve: 'straight',
|
curve: 'straight',
|
||||||
|
width: 2,
|
||||||
},
|
},
|
||||||
chart: {
|
chart: {
|
||||||
height: '150px',
|
height: '150px',
|
||||||
@@ -161,7 +178,7 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
animations: {
|
animations: {
|
||||||
enabled: false,
|
enabled: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
fill: {
|
fill: {
|
||||||
@@ -181,7 +198,7 @@
|
|||||||
show: true,
|
show: true,
|
||||||
borderColor: '',
|
borderColor: '',
|
||||||
},
|
},
|
||||||
colors: [baseColor],
|
colors: [ramColor],
|
||||||
xaxis: {
|
xaxis: {
|
||||||
type: 'datetime',
|
type: 'datetime',
|
||||||
labels: {
|
labels: {
|
||||||
@@ -205,6 +222,21 @@
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
marker: {
|
marker: {
|
||||||
show: false,
|
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: {
|
legend: {
|
||||||
@@ -214,14 +246,13 @@
|
|||||||
const serverMemoryChart = new ApexCharts(document.getElementById(`{!! $chartId !!}-memory`),
|
const serverMemoryChart = new ApexCharts(document.getElementById(`{!! $chartId !!}-memory`),
|
||||||
optionsServerMemory);
|
optionsServerMemory);
|
||||||
serverMemoryChart.render();
|
serverMemoryChart.render();
|
||||||
document.addEventListener('livewire:init', () => {
|
|
||||||
Livewire.on('refreshChartData-{!! $chartId !!}-memory', (chartData) => {
|
Livewire.on('refreshChartData-{!! $chartId !!}-memory', (chartData) => {
|
||||||
checkTheme();
|
checkTheme();
|
||||||
serverMemoryChart.updateOptions({
|
serverMemoryChart.updateOptions({
|
||||||
series: [{
|
series: [{
|
||||||
data: chartData[0].seriesData,
|
data: chartData[0].seriesData,
|
||||||
}],
|
}],
|
||||||
colors: [baseColor],
|
colors: [ramColor],
|
||||||
xaxis: {
|
xaxis: {
|
||||||
type: 'datetime',
|
type: 'datetime',
|
||||||
labels: {
|
labels: {
|
||||||
@@ -238,6 +269,9 @@
|
|||||||
show: true,
|
show: true,
|
||||||
style: {
|
style: {
|
||||||
colors: textColor,
|
colors: textColor,
|
||||||
|
},
|
||||||
|
formatter: function(value) {
|
||||||
|
return Math.round(value) + ' MB';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -249,9 +283,10 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
@endif
|
@endif
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
@@ -7,7 +7,7 @@
|
|||||||
<x-server.sidebar :server="$server" activeMenu="metrics" />
|
<x-server.sidebar :server="$server" activeMenu="metrics" />
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<h2>Metrics</h2>
|
<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())
|
@if ($server->isMetricsEnabled())
|
||||||
<div @if ($poll) wire:poll.5000ms='pollData' @endif x-init="$wire.loadData()">
|
<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">
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
<option value="10080">1 week</option>
|
<option value="10080">1 week</option>
|
||||||
<option value="43200">30 days</option>
|
<option value="43200">30 days</option>
|
||||||
</x-forms.select>
|
</x-forms.select>
|
||||||
<h4 class="pt-4">CPU (%)</h4>
|
<h4 class="pt-4">CPU Usage</h4>
|
||||||
<div wire:ignore id="{!! $chartId !!}-cpu"></div>
|
<div wire:ignore id="{!! $chartId !!}-cpu"></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -27,6 +27,7 @@
|
|||||||
const optionsServerCpu = {
|
const optionsServerCpu = {
|
||||||
stroke: {
|
stroke: {
|
||||||
curve: 'straight',
|
curve: 'straight',
|
||||||
|
width: 2,
|
||||||
},
|
},
|
||||||
chart: {
|
chart: {
|
||||||
height: '150px',
|
height: '150px',
|
||||||
@@ -45,7 +46,7 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
animations: {
|
animations: {
|
||||||
enabled: false,
|
enabled: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
fill: {
|
fill: {
|
||||||
@@ -65,7 +66,7 @@
|
|||||||
show: true,
|
show: true,
|
||||||
borderColor: '',
|
borderColor: '',
|
||||||
},
|
},
|
||||||
colors: [baseColor],
|
colors: [cpuColor],
|
||||||
xaxis: {
|
xaxis: {
|
||||||
type: 'datetime',
|
type: 'datetime',
|
||||||
},
|
},
|
||||||
@@ -83,6 +84,21 @@
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
marker: {
|
marker: {
|
||||||
show: false,
|
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: {
|
legend: {
|
||||||
@@ -99,7 +115,7 @@
|
|||||||
series: [{
|
series: [{
|
||||||
data: chartData[0].seriesData,
|
data: chartData[0].seriesData,
|
||||||
}],
|
}],
|
||||||
colors: [baseColor],
|
colors: [cpuColor],
|
||||||
xaxis: {
|
xaxis: {
|
||||||
type: 'datetime',
|
type: 'datetime',
|
||||||
labels: {
|
labels: {
|
||||||
@@ -115,6 +131,9 @@
|
|||||||
show: true,
|
show: true,
|
||||||
style: {
|
style: {
|
||||||
colors: textColor,
|
colors: textColor,
|
||||||
|
},
|
||||||
|
formatter: function(value) {
|
||||||
|
return Math.round(value) + ' %';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -130,7 +149,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h4>Memory (%)</h4>
|
<h4>Memory Usage</h4>
|
||||||
<div wire:ignore id="{!! $chartId !!}-memory"></div>
|
<div wire:ignore id="{!! $chartId !!}-memory"></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -138,6 +157,7 @@
|
|||||||
const optionsServerMemory = {
|
const optionsServerMemory = {
|
||||||
stroke: {
|
stroke: {
|
||||||
curve: 'straight',
|
curve: 'straight',
|
||||||
|
width: 2,
|
||||||
},
|
},
|
||||||
chart: {
|
chart: {
|
||||||
height: '150px',
|
height: '150px',
|
||||||
@@ -156,7 +176,7 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
animations: {
|
animations: {
|
||||||
enabled: false,
|
enabled: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
fill: {
|
fill: {
|
||||||
@@ -176,7 +196,7 @@
|
|||||||
show: true,
|
show: true,
|
||||||
borderColor: '',
|
borderColor: '',
|
||||||
},
|
},
|
||||||
colors: [baseColor],
|
colors: [ramColor],
|
||||||
xaxis: {
|
xaxis: {
|
||||||
type: 'datetime',
|
type: 'datetime',
|
||||||
labels: {
|
labels: {
|
||||||
@@ -200,6 +220,21 @@
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
marker: {
|
marker: {
|
||||||
show: false,
|
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: {
|
legend: {
|
||||||
@@ -216,7 +251,7 @@
|
|||||||
series: [{
|
series: [{
|
||||||
data: chartData[0].seriesData,
|
data: chartData[0].seriesData,
|
||||||
}],
|
}],
|
||||||
colors: [baseColor],
|
colors: [ramColor],
|
||||||
xaxis: {
|
xaxis: {
|
||||||
type: 'datetime',
|
type: 'datetime',
|
||||||
labels: {
|
labels: {
|
||||||
@@ -233,6 +268,9 @@
|
|||||||
show: true,
|
show: true,
|
||||||
style: {
|
style: {
|
||||||
colors: textColor,
|
colors: textColor,
|
||||||
|
},
|
||||||
|
formatter: function(value) {
|
||||||
|
return Math.round(value) + ' %';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -7,23 +7,23 @@
|
|||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<h2>Configuration</h2>
|
<h2>Configuration</h2>
|
||||||
@if ($server->proxy->status === 'exited' || $server->proxy->status === 'removing')
|
@if ($server->proxy->status === 'exited' || $server->proxy->status === 'removing')
|
||||||
<x-forms.button canGate="update" :canResource="$server" wire:click.prevent="changeProxy">Switch
|
@can('update', $server)
|
||||||
Proxy</x-forms.button>
|
<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
|
@else
|
||||||
<x-forms.button canGate="update" :canResource="$server" disabled
|
<x-forms.button canGate="update" :canResource="$server"
|
||||||
wire:click.prevent="changeProxy">Switch Proxy</x-forms.button>
|
wire:click="$dispatch('error', 'Currently running proxy must be stopped before switching proxy')">Switch Proxy</x-forms.button>
|
||||||
@endif
|
@endif
|
||||||
<x-forms.button canGate="update" :canResource="$server" type="submit">Save</x-forms.button>
|
<x-forms.button canGate="update" :canResource="$server" type="submit">Save</x-forms.button>
|
||||||
</div>
|
</div>
|
||||||
<div class="pb-4 "> <svg class="inline-flex w-6 h-6 mr-2 dark:text-warning" viewBox="0 0 256 256"
|
<div class="subtitle">Configure your proxy settings and advanced options.</div>
|
||||||
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>
|
|
||||||
<h3>Advanced</h3>
|
<h3>Advanced</h3>
|
||||||
<div class="pb-4 w-96">
|
<div class="pb-6 w-96">
|
||||||
<x-forms.checkbox canGate="update" :canResource="$server"
|
<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."
|
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"
|
id="server.settings.generate_exact_labels"
|
||||||
@@ -36,10 +36,29 @@
|
|||||||
id="redirectUrl" label="Redirect to (optional)" />
|
id="redirectUrl" label="Redirect to (optional)" />
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
@if ($server->proxyType() === ProxyTypes::TRAEFIK->value)
|
@php
|
||||||
<h3>Traefik</h3>
|
$proxyTitle = $server->proxyType() === ProxyTypes::TRAEFIK->value ? 'Traefik (Coolify Proxy)' : 'Caddy (Coolify Proxy)';
|
||||||
@elseif ($server->proxyType() === 'CADDY')
|
@endphp
|
||||||
<h3>Caddy</h3>
|
@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
|
@endif
|
||||||
@if (
|
@if (
|
||||||
$server->proxy->last_applied_settings &&
|
$server->proxy->last_applied_settings &&
|
||||||
@@ -73,6 +92,12 @@
|
|||||||
</x-modal-confirmation>
|
</x-modal-confirmation>
|
||||||
@endcan
|
@endcan
|
||||||
</div>
|
</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
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
Reference in New Issue
Block a user