mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-14 18:18:29 +00:00
Merge remote-tracking branch 'origin/next' into pr-7764-shadow/add-shared-server-env
This commit is contained in:
commit
cb97a18a78
767 files changed
+61894
-15664
No files matched your search
@@ -45,10 +45,10 @@ class Danger extends Component
|
||||
|
||||
if ($this->resource === null) {
|
||||
if (isset($parameters['service_uuid'])) {
|
||||
$this->resource = Service::where('uuid', $parameters['service_uuid'])->first();
|
||||
$this->resource = Service::ownedByCurrentTeam()->where('uuid', $parameters['service_uuid'])->first();
|
||||
} elseif (isset($parameters['stack_service_uuid'])) {
|
||||
$this->resource = ServiceApplication::where('uuid', $parameters['stack_service_uuid'])->first()
|
||||
?? ServiceDatabase::where('uuid', $parameters['stack_service_uuid'])->first();
|
||||
$this->resource = ServiceApplication::ownedByCurrentTeam()->where('uuid', $parameters['stack_service_uuid'])->first()
|
||||
?? ServiceDatabase::ownedByCurrentTeam()->where('uuid', $parameters['stack_service_uuid'])->first();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,16 +88,21 @@ class Danger extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function delete($password)
|
||||
public function delete($password, $selectedActions = [])
|
||||
{
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return;
|
||||
return 'The provided password is incorrect.';
|
||||
}
|
||||
|
||||
if (! $this->resource) {
|
||||
$this->addError('resource', 'Resource not found.');
|
||||
return 'Resource not found.';
|
||||
}
|
||||
|
||||
return;
|
||||
if (! empty($selectedActions)) {
|
||||
$this->delete_volumes = in_array('delete_volumes', $selectedActions);
|
||||
$this->delete_connected_networks = in_array('delete_connected_networks', $selectedActions);
|
||||
$this->delete_configurations = in_array('delete_configurations', $selectedActions);
|
||||
$this->docker_cleanup = in_array('docker_cleanup', $selectedActions);
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -134,11 +134,11 @@ class Destination extends Component
|
||||
$this->dispatch('refresh');
|
||||
}
|
||||
|
||||
public function removeServer(int $network_id, int $server_id, $password)
|
||||
public function removeServer(int $network_id, int $server_id, $password, $selectedActions = [])
|
||||
{
|
||||
try {
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return;
|
||||
return 'The provided password is incorrect.';
|
||||
}
|
||||
|
||||
if ($this->resource->destination->server->id == $server_id && $this->resource->destination->id == $network_id) {
|
||||
@@ -152,6 +152,8 @@ class Destination extends Component
|
||||
$this->loadData();
|
||||
$this->dispatch('refresh');
|
||||
ApplicationStatusChanged::dispatch(data_get($this->resource, 'environment.project.team.id'));
|
||||
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ class Add extends Component
|
||||
|
||||
public bool $is_buildtime = true;
|
||||
|
||||
public ?string $comment = null;
|
||||
|
||||
public array $problematicVariables = [];
|
||||
|
||||
protected $listeners = ['clearAddEnv' => 'clear'];
|
||||
@@ -42,6 +44,7 @@ class Add extends Component
|
||||
'is_literal' => 'required|boolean',
|
||||
'is_runtime' => 'required|boolean',
|
||||
'is_buildtime' => 'required|boolean',
|
||||
'comment' => 'nullable|string|max:256',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
@@ -51,6 +54,7 @@ class Add extends Component
|
||||
'is_literal' => 'literal',
|
||||
'is_runtime' => 'runtime',
|
||||
'is_buildtime' => 'buildtime',
|
||||
'comment' => 'comment',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
@@ -197,6 +201,7 @@ class Add extends Component
|
||||
'is_runtime' => $this->is_runtime,
|
||||
'is_buildtime' => $this->is_buildtime,
|
||||
'is_preview' => $this->is_preview,
|
||||
'comment' => $this->comment,
|
||||
]);
|
||||
$this->clear();
|
||||
}
|
||||
@@ -209,5 +214,6 @@ class Add extends Component
|
||||
$this->is_literal = false;
|
||||
$this->is_runtime = true;
|
||||
$this->is_buildtime = true;
|
||||
$this->comment = null;
|
||||
}
|
||||
}
|
||||
@@ -89,6 +89,62 @@ class All extends Component
|
||||
return $query->get();
|
||||
}
|
||||
|
||||
public function getHardcodedEnvironmentVariablesProperty()
|
||||
{
|
||||
return $this->getHardcodedVariables(false);
|
||||
}
|
||||
|
||||
public function getHardcodedEnvironmentVariablesPreviewProperty()
|
||||
{
|
||||
return $this->getHardcodedVariables(true);
|
||||
}
|
||||
|
||||
protected function getHardcodedVariables(bool $isPreview)
|
||||
{
|
||||
// Only for services and docker-compose applications
|
||||
if ($this->resource->type() !== 'service' &&
|
||||
($this->resourceClass !== 'App\Models\Application' ||
|
||||
($this->resourceClass === 'App\Models\Application' && $this->resource->build_pack !== 'dockercompose'))) {
|
||||
return collect([]);
|
||||
}
|
||||
|
||||
$dockerComposeRaw = $this->resource->docker_compose_raw ?? $this->resource->docker_compose;
|
||||
|
||||
if (blank($dockerComposeRaw)) {
|
||||
return collect([]);
|
||||
}
|
||||
|
||||
// Extract all hard-coded variables
|
||||
$hardcodedVars = extractHardcodedEnvironmentVariables($dockerComposeRaw);
|
||||
|
||||
// Filter out magic variables (SERVICE_FQDN_*, SERVICE_URL_*, SERVICE_NAME_*)
|
||||
$hardcodedVars = $hardcodedVars->filter(function ($var) {
|
||||
$key = $var['key'];
|
||||
|
||||
return ! str($key)->startsWith(['SERVICE_FQDN_', 'SERVICE_URL_', 'SERVICE_NAME_']);
|
||||
});
|
||||
|
||||
// Filter out variables that exist in database (user has overridden/managed them)
|
||||
// For preview, check against preview variables; for production, check against production variables
|
||||
if ($isPreview) {
|
||||
$managedKeys = $this->resource->environment_variables_preview()->pluck('key')->toArray();
|
||||
} else {
|
||||
$managedKeys = $this->resource->environment_variables()->where('is_preview', false)->pluck('key')->toArray();
|
||||
}
|
||||
|
||||
$hardcodedVars = $hardcodedVars->filter(function ($var) use ($managedKeys) {
|
||||
return ! in_array($var['key'], $managedKeys);
|
||||
});
|
||||
|
||||
// Apply sorting based on is_env_sorting_enabled
|
||||
if ($this->is_env_sorting_enabled) {
|
||||
$hardcodedVars = $hardcodedVars->sortBy('key')->values();
|
||||
}
|
||||
// Otherwise keep order from docker-compose file
|
||||
|
||||
return $hardcodedVars;
|
||||
}
|
||||
|
||||
public function getDevView()
|
||||
{
|
||||
$this->variables = $this->formatEnvironmentVariables($this->environmentVariables);
|
||||
@@ -240,6 +296,7 @@ class All extends Component
|
||||
$environment->is_runtime = $data['is_runtime'] ?? true;
|
||||
$environment->is_buildtime = $data['is_buildtime'] ?? true;
|
||||
$environment->is_preview = $data['is_preview'] ?? false;
|
||||
$environment->comment = $data['comment'] ?? null;
|
||||
$environment->resourceable_id = $this->resource->id;
|
||||
$environment->resourceable_type = $this->resource->getMorphClass();
|
||||
|
||||
@@ -280,18 +337,37 @@ class All extends Component
|
||||
private function updateOrCreateVariables($isPreview, $variables)
|
||||
{
|
||||
$count = 0;
|
||||
foreach ($variables as $key => $value) {
|
||||
foreach ($variables as $key => $data) {
|
||||
if (str($key)->startsWith('SERVICE_FQDN') || str($key)->startsWith('SERVICE_URL') || str($key)->startsWith('SERVICE_NAME')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract value and comment from parsed data
|
||||
// Handle both array format ['value' => ..., 'comment' => ...] and plain string values
|
||||
$value = is_array($data) ? ($data['value'] ?? '') : $data;
|
||||
$comment = is_array($data) ? ($data['comment'] ?? null) : null;
|
||||
|
||||
$method = $isPreview ? 'environment_variables_preview' : 'environment_variables';
|
||||
$found = $this->resource->$method()->where('key', $key)->first();
|
||||
|
||||
if ($found) {
|
||||
if (! $found->is_shown_once && ! $found->is_multiline) {
|
||||
// Only count as a change if the value actually changed
|
||||
$changed = false;
|
||||
|
||||
// Update value if it changed
|
||||
if ($found->value !== $value) {
|
||||
$found->value = $value;
|
||||
$changed = true;
|
||||
}
|
||||
|
||||
// Only update comment from inline comment if one is provided (overwrites existing)
|
||||
// If $comment is null, don't touch existing comment field to preserve it
|
||||
if ($comment !== null && $found->comment !== $comment) {
|
||||
$found->comment = $comment;
|
||||
$changed = true;
|
||||
}
|
||||
|
||||
if ($changed) {
|
||||
$found->save();
|
||||
$count++;
|
||||
}
|
||||
@@ -300,6 +376,7 @@ class All extends Component
|
||||
$environment = new EnvironmentVariable;
|
||||
$environment->key = $key;
|
||||
$environment->value = $value;
|
||||
$environment->comment = $comment; // Set comment from inline comment
|
||||
$environment->is_multiline = false;
|
||||
$environment->is_preview = $isPreview;
|
||||
$environment->resourceable_id = $this->resource->id;
|
||||
|
||||
@@ -24,6 +24,8 @@ class Show extends Component
|
||||
|
||||
public bool $isLocked = false;
|
||||
|
||||
public bool $isMagicVariable = false;
|
||||
|
||||
public bool $isSharedVariable = false;
|
||||
|
||||
public string $type;
|
||||
@@ -34,6 +36,8 @@ class Show extends Component
|
||||
|
||||
public ?string $real_value = null;
|
||||
|
||||
public ?string $comment = null;
|
||||
|
||||
public bool $is_shared = false;
|
||||
|
||||
public bool $is_multiline = false;
|
||||
@@ -63,6 +67,7 @@ class Show extends Component
|
||||
protected $rules = [
|
||||
'key' => 'required|string',
|
||||
'value' => 'nullable',
|
||||
'comment' => 'nullable|string|max:256',
|
||||
'is_multiline' => 'required|boolean',
|
||||
'is_literal' => 'required|boolean',
|
||||
'is_shown_once' => 'required|boolean',
|
||||
@@ -93,6 +98,9 @@ class Show extends Component
|
||||
|
||||
public function refresh()
|
||||
{
|
||||
if (! $this->env->exists || ! $this->env->fresh()) {
|
||||
return;
|
||||
}
|
||||
$this->syncData();
|
||||
$this->checkEnvs();
|
||||
}
|
||||
@@ -104,6 +112,7 @@ class Show extends Component
|
||||
$this->validate([
|
||||
'key' => 'required|string',
|
||||
'value' => 'nullable',
|
||||
'comment' => 'nullable|string|max:256',
|
||||
'is_multiline' => 'required|boolean',
|
||||
'is_literal' => 'required|boolean',
|
||||
'is_shown_once' => 'required|boolean',
|
||||
@@ -118,6 +127,7 @@ class Show extends Component
|
||||
}
|
||||
$this->env->key = $this->key;
|
||||
$this->env->value = $this->value;
|
||||
$this->env->comment = $this->comment;
|
||||
$this->env->is_multiline = $this->is_multiline;
|
||||
$this->env->is_literal = $this->is_literal;
|
||||
$this->env->is_shown_once = $this->is_shown_once;
|
||||
@@ -125,6 +135,7 @@ class Show extends Component
|
||||
} else {
|
||||
$this->key = $this->env->key;
|
||||
$this->value = $this->env->value;
|
||||
$this->comment = $this->env->comment;
|
||||
$this->is_multiline = $this->env->is_multiline;
|
||||
$this->is_literal = $this->env->is_literal;
|
||||
$this->is_shown_once = $this->env->is_shown_once;
|
||||
@@ -140,9 +151,13 @@ class Show extends Component
|
||||
public function checkEnvs()
|
||||
{
|
||||
$this->isDisabled = false;
|
||||
$this->isMagicVariable = false;
|
||||
|
||||
if (str($this->env->key)->startsWith('SERVICE_FQDN') || str($this->env->key)->startsWith('SERVICE_URL') || str($this->env->key)->startsWith('SERVICE_NAME')) {
|
||||
$this->isDisabled = true;
|
||||
$this->isMagicVariable = true;
|
||||
}
|
||||
|
||||
if ($this->env->is_shown_once) {
|
||||
$this->isLocked = true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Project\Shared\EnvironmentVariable;
|
||||
|
||||
use Livewire\Component;
|
||||
|
||||
class ShowHardcoded extends Component
|
||||
{
|
||||
public array $env;
|
||||
|
||||
public string $key;
|
||||
|
||||
public ?string $value = null;
|
||||
|
||||
public ?string $comment = null;
|
||||
|
||||
public ?string $serviceName = null;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->key = $this->env['key'];
|
||||
$this->value = $this->env['value'] ?? null;
|
||||
$this->comment = $this->env['comment'] ?? null;
|
||||
$this->serviceName = $this->env['service_name'] ?? null;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.shared.environment-variable.show-hardcoded');
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace App\Livewire\Project\Shared;
|
||||
use App\Models\Application;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Component;
|
||||
@@ -38,7 +39,7 @@ class ExecuteContainerCommand extends Component
|
||||
$this->servers = collect();
|
||||
if (data_get($this->parameters, 'application_uuid')) {
|
||||
$this->type = 'application';
|
||||
$this->resource = Application::where('uuid', $this->parameters['application_uuid'])->firstOrFail();
|
||||
$this->resource = Application::ownedByCurrentTeam()->where('uuid', $this->parameters['application_uuid'])->firstOrFail();
|
||||
if ($this->resource->destination->server->isFunctional()) {
|
||||
$this->servers = $this->servers->push($this->resource->destination->server);
|
||||
}
|
||||
@@ -61,14 +62,14 @@ class ExecuteContainerCommand extends Component
|
||||
$this->loadContainers();
|
||||
} elseif (data_get($this->parameters, 'service_uuid')) {
|
||||
$this->type = 'service';
|
||||
$this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail();
|
||||
$this->resource = Service::ownedByCurrentTeam()->where('uuid', $this->parameters['service_uuid'])->firstOrFail();
|
||||
if ($this->resource->server->isFunctional()) {
|
||||
$this->servers = $this->servers->push($this->resource->server);
|
||||
}
|
||||
$this->loadContainers();
|
||||
} elseif (data_get($this->parameters, 'server_uuid')) {
|
||||
$this->type = 'server';
|
||||
$this->resource = Server::where('uuid', $this->parameters['server_uuid'])->firstOrFail();
|
||||
$this->resource = Server::ownedByCurrentTeam()->where('uuid', $this->parameters['server_uuid'])->firstOrFail();
|
||||
$this->servers = $this->servers->push($this->resource);
|
||||
}
|
||||
$this->servers = $this->servers->sortByDesc(fn ($server) => $server->isTerminalEnabled());
|
||||
@@ -181,7 +182,7 @@ class ExecuteContainerCommand extends Component
|
||||
}
|
||||
try {
|
||||
// Validate container name format
|
||||
if (! preg_match('/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/', $this->selected_container)) {
|
||||
if (! ValidationPatterns::isValidContainerName($this->selected_container)) {
|
||||
throw new \InvalidArgumentException('Invalid container name format');
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,9 @@ use App\Models\StandaloneMongodb;
|
||||
use App\Models\StandaloneMysql;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\StandaloneRedis;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Component;
|
||||
|
||||
class GetLogs extends Component
|
||||
@@ -29,12 +31,16 @@ class GetLogs extends Component
|
||||
|
||||
public string $errors = '';
|
||||
|
||||
#[Locked]
|
||||
public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse|null $resource = null;
|
||||
|
||||
#[Locked]
|
||||
public ServiceApplication|ServiceDatabase|null $servicesubtype = null;
|
||||
|
||||
#[Locked]
|
||||
public Server $server;
|
||||
|
||||
#[Locked]
|
||||
public ?string $container = null;
|
||||
|
||||
public ?string $displayName = null;
|
||||
@@ -54,7 +60,7 @@ class GetLogs extends Component
|
||||
public function mount()
|
||||
{
|
||||
if (! is_null($this->resource)) {
|
||||
if ($this->resource->getMorphClass() === \App\Models\Application::class) {
|
||||
if ($this->resource->getMorphClass() === Application::class) {
|
||||
$this->showTimeStamps = $this->resource->settings->is_include_timestamps;
|
||||
} else {
|
||||
if ($this->servicesubtype) {
|
||||
@@ -63,7 +69,7 @@ class GetLogs extends Component
|
||||
$this->showTimeStamps = $this->resource->is_include_timestamps;
|
||||
}
|
||||
}
|
||||
if ($this->resource?->getMorphClass() === \App\Models\Application::class) {
|
||||
if ($this->resource?->getMorphClass() === Application::class) {
|
||||
if (str($this->container)->contains('-pr-')) {
|
||||
$this->pull_request = 'Pull Request: '.str($this->container)->afterLast('-pr-')->beforeLast('_')->value();
|
||||
}
|
||||
@@ -74,11 +80,11 @@ class GetLogs extends Component
|
||||
public function instantSave()
|
||||
{
|
||||
if (! is_null($this->resource)) {
|
||||
if ($this->resource->getMorphClass() === \App\Models\Application::class) {
|
||||
if ($this->resource->getMorphClass() === Application::class) {
|
||||
$this->resource->settings->is_include_timestamps = $this->showTimeStamps;
|
||||
$this->resource->settings->save();
|
||||
}
|
||||
if ($this->resource->getMorphClass() === \App\Models\Service::class) {
|
||||
if ($this->resource->getMorphClass() === Service::class) {
|
||||
$serviceName = str($this->container)->beforeLast('-')->value();
|
||||
$subType = $this->resource->applications()->where('name', $serviceName)->first();
|
||||
if ($subType) {
|
||||
@@ -118,10 +124,20 @@ class GetLogs extends Component
|
||||
|
||||
public function getLogs($refresh = false)
|
||||
{
|
||||
if (! Server::ownedByCurrentTeam()->where('id', $this->server->id)->exists()) {
|
||||
$this->outputs = 'Unauthorized.';
|
||||
|
||||
return;
|
||||
}
|
||||
if (! $this->server->isFunctional()) {
|
||||
return;
|
||||
}
|
||||
if (! $refresh && ! $this->expandByDefault && ($this->resource?->getMorphClass() === \App\Models\Service::class || str($this->container)->contains('-pr-'))) {
|
||||
if ($this->container && ! ValidationPatterns::isValidContainerName($this->container)) {
|
||||
$this->outputs = 'Invalid container name.';
|
||||
|
||||
return;
|
||||
}
|
||||
if (! $refresh && ! $this->expandByDefault && ($this->resource?->getMorphClass() === Service::class || str($this->container)->contains('-pr-'))) {
|
||||
return;
|
||||
}
|
||||
if ($this->numberOfLines <= 0 || is_null($this->numberOfLines)) {
|
||||
@@ -194,9 +210,15 @@ class GetLogs extends Component
|
||||
|
||||
public function downloadAllLogs(): string
|
||||
{
|
||||
if (! Server::ownedByCurrentTeam()->where('id', $this->server->id)->exists()) {
|
||||
return '';
|
||||
}
|
||||
if (! $this->server->isFunctional() || ! $this->container) {
|
||||
return '';
|
||||
}
|
||||
if (! ValidationPatterns::isValidContainerName($this->container)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ($this->showTimeStamps) {
|
||||
if ($this->server->isSwarm()) {
|
||||
|
||||
@@ -16,19 +16,25 @@ class HealthChecks extends Component
|
||||
#[Validate(['boolean'])]
|
||||
public bool $healthCheckEnabled = false;
|
||||
|
||||
#[Validate(['string'])]
|
||||
#[Validate(['string', 'in:http,cmd'])]
|
||||
public string $healthCheckType = 'http';
|
||||
|
||||
#[Validate(['nullable', 'required_if:healthCheckType,cmd', 'string', 'max:1000', 'regex:/^[a-zA-Z0-9 \-_.\/:=@,+]+$/'])]
|
||||
public ?string $healthCheckCommand = null;
|
||||
|
||||
#[Validate(['required', 'string', 'in:GET,HEAD,POST,OPTIONS'])]
|
||||
public string $healthCheckMethod;
|
||||
|
||||
#[Validate(['string'])]
|
||||
#[Validate(['required', 'string', 'in:http,https'])]
|
||||
public string $healthCheckScheme;
|
||||
|
||||
#[Validate(['string'])]
|
||||
#[Validate(['required', 'string', 'regex:/^[a-zA-Z0-9.\-_]+$/'])]
|
||||
public string $healthCheckHost;
|
||||
|
||||
#[Validate(['nullable', 'string'])]
|
||||
#[Validate(['nullable', 'integer', 'min:1', 'max:65535'])]
|
||||
public ?string $healthCheckPort = null;
|
||||
|
||||
#[Validate(['string'])]
|
||||
#[Validate(['required', 'string', 'regex:#^[a-zA-Z0-9/\-_.~%]+$#'])]
|
||||
public string $healthCheckPath;
|
||||
|
||||
#[Validate(['integer'])]
|
||||
@@ -54,12 +60,14 @@ class HealthChecks extends Component
|
||||
|
||||
protected $rules = [
|
||||
'healthCheckEnabled' => 'boolean',
|
||||
'healthCheckPath' => 'string',
|
||||
'healthCheckPort' => 'nullable|string',
|
||||
'healthCheckHost' => 'string',
|
||||
'healthCheckMethod' => 'string',
|
||||
'healthCheckType' => 'string|in:http,cmd',
|
||||
'healthCheckCommand' => ['nullable', 'string', 'max:1000', 'regex:/^[a-zA-Z0-9 \-_.\/:=@,+]+$/'],
|
||||
'healthCheckPath' => ['required', 'string', 'regex:#^[a-zA-Z0-9/\-_.~%]+$#'],
|
||||
'healthCheckPort' => 'nullable|integer|min:1|max:65535',
|
||||
'healthCheckHost' => ['required', 'string', 'regex:/^[a-zA-Z0-9.\-_]+$/'],
|
||||
'healthCheckMethod' => 'required|string|in:GET,HEAD,POST,OPTIONS',
|
||||
'healthCheckReturnCode' => 'integer',
|
||||
'healthCheckScheme' => 'string',
|
||||
'healthCheckScheme' => 'required|string|in:http,https',
|
||||
'healthCheckResponseText' => 'nullable|string',
|
||||
'healthCheckInterval' => 'integer|min:1',
|
||||
'healthCheckTimeout' => 'integer|min:1',
|
||||
@@ -81,6 +89,8 @@ class HealthChecks extends Component
|
||||
|
||||
// Sync to model
|
||||
$this->resource->health_check_enabled = $this->healthCheckEnabled;
|
||||
$this->resource->health_check_type = $this->healthCheckType;
|
||||
$this->resource->health_check_command = $this->healthCheckCommand;
|
||||
$this->resource->health_check_method = $this->healthCheckMethod;
|
||||
$this->resource->health_check_scheme = $this->healthCheckScheme;
|
||||
$this->resource->health_check_host = $this->healthCheckHost;
|
||||
@@ -98,6 +108,8 @@ class HealthChecks extends Component
|
||||
} else {
|
||||
// Sync from model
|
||||
$this->healthCheckEnabled = $this->resource->health_check_enabled;
|
||||
$this->healthCheckType = $this->resource->health_check_type ?? 'http';
|
||||
$this->healthCheckCommand = $this->resource->health_check_command;
|
||||
$this->healthCheckMethod = $this->resource->health_check_method;
|
||||
$this->healthCheckScheme = $this->resource->health_check_scheme;
|
||||
$this->healthCheckHost = $this->resource->health_check_host;
|
||||
@@ -116,9 +128,12 @@ class HealthChecks extends Component
|
||||
public function instantSave()
|
||||
{
|
||||
$this->authorize('update', $this->resource);
|
||||
$this->validate();
|
||||
|
||||
// Sync component properties to model
|
||||
$this->resource->health_check_enabled = $this->healthCheckEnabled;
|
||||
$this->resource->health_check_type = $this->healthCheckType;
|
||||
$this->resource->health_check_command = $this->healthCheckCommand;
|
||||
$this->resource->health_check_method = $this->healthCheckMethod;
|
||||
$this->resource->health_check_scheme = $this->healthCheckScheme;
|
||||
$this->resource->health_check_host = $this->healthCheckHost;
|
||||
@@ -143,6 +158,8 @@ class HealthChecks extends Component
|
||||
|
||||
// Sync component properties to model
|
||||
$this->resource->health_check_enabled = $this->healthCheckEnabled;
|
||||
$this->resource->health_check_type = $this->healthCheckType;
|
||||
$this->resource->health_check_command = $this->healthCheckCommand;
|
||||
$this->resource->health_check_method = $this->healthCheckMethod;
|
||||
$this->resource->health_check_scheme = $this->healthCheckScheme;
|
||||
$this->resource->health_check_host = $this->healthCheckHost;
|
||||
@@ -171,6 +188,8 @@ class HealthChecks extends Component
|
||||
|
||||
// Sync component properties to model
|
||||
$this->resource->health_check_enabled = $this->healthCheckEnabled;
|
||||
$this->resource->health_check_type = $this->healthCheckType;
|
||||
$this->resource->health_check_command = $this->healthCheckCommand;
|
||||
$this->resource->health_check_method = $this->healthCheckMethod;
|
||||
$this->resource->health_check_scheme = $this->healthCheckScheme;
|
||||
$this->resource->health_check_host = $this->healthCheckHost;
|
||||
|
||||
@@ -106,7 +106,7 @@ class Logs extends Component
|
||||
$this->query = request()->query();
|
||||
if (data_get($this->parameters, 'application_uuid')) {
|
||||
$this->type = 'application';
|
||||
$this->resource = Application::where('uuid', $this->parameters['application_uuid'])->firstOrFail();
|
||||
$this->resource = Application::ownedByCurrentTeam()->where('uuid', $this->parameters['application_uuid'])->firstOrFail();
|
||||
$this->status = $this->resource->status;
|
||||
if ($this->resource->destination->server->isFunctional()) {
|
||||
$server = $this->resource->destination->server;
|
||||
@@ -133,7 +133,7 @@ class Logs extends Component
|
||||
$this->containers->push($this->container);
|
||||
} elseif (data_get($this->parameters, 'service_uuid')) {
|
||||
$this->type = 'service';
|
||||
$this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail();
|
||||
$this->resource = Service::ownedByCurrentTeam()->where('uuid', $this->parameters['service_uuid'])->firstOrFail();
|
||||
$this->resource->applications()->get()->each(function ($application) {
|
||||
$this->containers->push(data_get($application, 'name').'-'.data_get($this->resource, 'uuid'));
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Livewire\Project\Shared;
|
||||
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Livewire\Component;
|
||||
|
||||
class ResourceLimits extends Component
|
||||
@@ -16,24 +17,24 @@ class ResourceLimits extends Component
|
||||
|
||||
public ?string $limitsCpuset = null;
|
||||
|
||||
public ?int $limitsCpuShares = null;
|
||||
public mixed $limitsCpuShares = null;
|
||||
|
||||
public string $limitsMemory;
|
||||
|
||||
public string $limitsMemorySwap;
|
||||
|
||||
public int $limitsMemorySwappiness;
|
||||
public mixed $limitsMemorySwappiness = 0;
|
||||
|
||||
public string $limitsMemoryReservation;
|
||||
|
||||
protected $rules = [
|
||||
'limitsMemory' => 'required|string',
|
||||
'limitsMemorySwap' => 'required|string',
|
||||
'limitsMemory' => ['required', 'string', 'regex:/^(0|\d+[bBkKmMgG])$/'],
|
||||
'limitsMemorySwap' => ['required', 'string', 'regex:/^(0|\d+[bBkKmMgG])$/'],
|
||||
'limitsMemorySwappiness' => 'required|integer|min:0|max:100',
|
||||
'limitsMemoryReservation' => 'required|string',
|
||||
'limitsCpus' => 'nullable',
|
||||
'limitsCpuset' => 'nullable',
|
||||
'limitsCpuShares' => 'nullable',
|
||||
'limitsMemoryReservation' => ['required', 'string', 'regex:/^(0|\d+[bBkKmMgG])$/'],
|
||||
'limitsCpus' => ['nullable', 'regex:/^\d*\.?\d+$/'],
|
||||
'limitsCpuset' => ['nullable', 'regex:/^\d+([,-]\d+)*$/'],
|
||||
'limitsCpuShares' => 'nullable|integer|min:0',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
@@ -46,6 +47,19 @@ class ResourceLimits extends Component
|
||||
'limitsCpuShares' => 'cpu shares',
|
||||
];
|
||||
|
||||
protected $messages = [
|
||||
'limitsMemory.regex' => 'Maximum Memory Limit must be a number followed by a unit (b, k, m, g). Example: 256m, 1g. Use 0 for unlimited.',
|
||||
'limitsMemorySwap.regex' => 'Maximum Swap Limit must be a number followed by a unit (b, k, m, g). Example: 256m, 1g. Use 0 for unlimited.',
|
||||
'limitsMemoryReservation.regex' => 'Soft Memory Limit must be a number followed by a unit (b, k, m, g). Example: 256m, 1g. Use 0 for unlimited.',
|
||||
'limitsCpus.regex' => 'Number of CPUs must be a number (integer or decimal). Example: 0.5, 2.',
|
||||
'limitsCpuset.regex' => 'CPU sets must be a comma-separated list of CPU numbers or ranges. Example: 0-2 or 0,1,3.',
|
||||
'limitsMemorySwappiness.integer' => 'Swappiness must be a whole number between 0 and 100.',
|
||||
'limitsMemorySwappiness.min' => 'Swappiness must be between 0 and 100.',
|
||||
'limitsMemorySwappiness.max' => 'Swappiness must be between 0 and 100.',
|
||||
'limitsCpuShares.integer' => 'CPU Weight must be a whole number.',
|
||||
'limitsCpuShares.min' => 'CPU Weight must be a positive number.',
|
||||
];
|
||||
|
||||
/**
|
||||
* Sync data between component properties and model
|
||||
*
|
||||
@@ -57,10 +71,10 @@ class ResourceLimits extends Component
|
||||
// Sync TO model (before save)
|
||||
$this->resource->limits_cpus = $this->limitsCpus;
|
||||
$this->resource->limits_cpuset = $this->limitsCpuset;
|
||||
$this->resource->limits_cpu_shares = $this->limitsCpuShares;
|
||||
$this->resource->limits_cpu_shares = (int) $this->limitsCpuShares;
|
||||
$this->resource->limits_memory = $this->limitsMemory;
|
||||
$this->resource->limits_memory_swap = $this->limitsMemorySwap;
|
||||
$this->resource->limits_memory_swappiness = $this->limitsMemorySwappiness;
|
||||
$this->resource->limits_memory_swappiness = (int) $this->limitsMemorySwappiness;
|
||||
$this->resource->limits_memory_reservation = $this->limitsMemoryReservation;
|
||||
} else {
|
||||
// Sync FROM model (on load/refresh)
|
||||
@@ -91,7 +105,7 @@ class ResourceLimits extends Component
|
||||
if (! $this->limitsMemorySwap) {
|
||||
$this->limitsMemorySwap = '0';
|
||||
}
|
||||
if (is_null($this->limitsMemorySwappiness)) {
|
||||
if ($this->limitsMemorySwappiness === '' || is_null($this->limitsMemorySwappiness)) {
|
||||
$this->limitsMemorySwappiness = 60;
|
||||
}
|
||||
if (! $this->limitsMemoryReservation) {
|
||||
@@ -103,7 +117,7 @@ class ResourceLimits extends Component
|
||||
if ($this->limitsCpuset === '') {
|
||||
$this->limitsCpuset = null;
|
||||
}
|
||||
if (is_null($this->limitsCpuShares)) {
|
||||
if ($this->limitsCpuShares === '' || is_null($this->limitsCpuShares)) {
|
||||
$this->limitsCpuShares = 1024;
|
||||
}
|
||||
|
||||
@@ -112,6 +126,12 @@ class ResourceLimits extends Component
|
||||
$this->syncData(true);
|
||||
$this->resource->save();
|
||||
$this->dispatch('success', 'Resource limits updated.');
|
||||
} catch (ValidationException $e) {
|
||||
foreach ($e->validator->errors()->all() as $message) {
|
||||
$this->dispatch('error', $message);
|
||||
}
|
||||
|
||||
return;
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
@@ -7,9 +7,18 @@ use App\Actions\Database\StopDatabase;
|
||||
use App\Actions\Service\StartService;
|
||||
use App\Actions\Service\StopService;
|
||||
use App\Jobs\VolumeCloneJob;
|
||||
use App\Models\Application;
|
||||
use App\Models\Environment;
|
||||
use App\Models\Project;
|
||||
use App\Models\StandaloneClickhouse;
|
||||
use App\Models\StandaloneDocker;
|
||||
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 App\Models\SwarmDocker;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Component;
|
||||
@@ -49,9 +58,10 @@ class ResourceOperations extends Component
|
||||
{
|
||||
$this->authorize('update', $this->resource);
|
||||
|
||||
$new_destination = StandaloneDocker::find($destination_id);
|
||||
$teamScope = fn ($q) => $q->where('team_id', currentTeam()->id);
|
||||
$new_destination = StandaloneDocker::whereHas('server', $teamScope)->find($destination_id);
|
||||
if (! $new_destination) {
|
||||
$new_destination = SwarmDocker::find($destination_id);
|
||||
$new_destination = SwarmDocker::whereHas('server', $teamScope)->find($destination_id);
|
||||
}
|
||||
if (! $new_destination) {
|
||||
return $this->addError('destination_id', 'Destination not found.');
|
||||
@@ -59,7 +69,7 @@ class ResourceOperations extends Component
|
||||
$uuid = (string) new Cuid2;
|
||||
$server = $new_destination->server;
|
||||
|
||||
if ($this->resource->getMorphClass() === \App\Models\Application::class) {
|
||||
if ($this->resource->getMorphClass() === Application::class) {
|
||||
$new_resource = clone_application($this->resource, $new_destination, ['uuid' => $uuid], $this->cloneVolumeData);
|
||||
|
||||
$route = route('project.application.configuration', [
|
||||
@@ -70,21 +80,21 @@ class ResourceOperations extends Component
|
||||
|
||||
return redirect()->to($route);
|
||||
} elseif (
|
||||
$this->resource->getMorphClass() === \App\Models\StandalonePostgresql::class ||
|
||||
$this->resource->getMorphClass() === \App\Models\StandaloneMongodb::class ||
|
||||
$this->resource->getMorphClass() === \App\Models\StandaloneMysql::class ||
|
||||
$this->resource->getMorphClass() === \App\Models\StandaloneMariadb::class ||
|
||||
$this->resource->getMorphClass() === \App\Models\StandaloneRedis::class ||
|
||||
$this->resource->getMorphClass() === \App\Models\StandaloneKeydb::class ||
|
||||
$this->resource->getMorphClass() === \App\Models\StandaloneDragonfly::class ||
|
||||
$this->resource->getMorphClass() === \App\Models\StandaloneClickhouse::class
|
||||
$this->resource->getMorphClass() === StandalonePostgresql::class ||
|
||||
$this->resource->getMorphClass() === StandaloneMongodb::class ||
|
||||
$this->resource->getMorphClass() === StandaloneMysql::class ||
|
||||
$this->resource->getMorphClass() === StandaloneMariadb::class ||
|
||||
$this->resource->getMorphClass() === StandaloneRedis::class ||
|
||||
$this->resource->getMorphClass() === StandaloneKeydb::class ||
|
||||
$this->resource->getMorphClass() === StandaloneDragonfly::class ||
|
||||
$this->resource->getMorphClass() === StandaloneClickhouse::class
|
||||
) {
|
||||
$uuid = (string) new Cuid2;
|
||||
$new_resource = $this->resource->replicate([
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
])->forceFill([
|
||||
'uuid' => $uuid,
|
||||
'name' => $this->resource->name.'-clone-'.$uuid,
|
||||
'status' => 'exited',
|
||||
@@ -132,7 +142,8 @@ class ResourceOperations extends Component
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
'uuid',
|
||||
])->forceFill([
|
||||
'name' => $newName,
|
||||
'resource_id' => $new_resource->id,
|
||||
]);
|
||||
@@ -161,7 +172,7 @@ class ResourceOperations extends Component
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
])->forceFill([
|
||||
'resource_id' => $new_resource->id,
|
||||
]);
|
||||
$newStorage->save();
|
||||
@@ -174,7 +185,7 @@ class ResourceOperations extends Component
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
])->forceFill([
|
||||
'uuid' => $uuid,
|
||||
'database_id' => $new_resource->id,
|
||||
'database_type' => $new_resource->getMorphClass(),
|
||||
@@ -193,7 +204,7 @@ class ResourceOperations extends Component
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill($payload);
|
||||
])->forceFill($payload);
|
||||
$newEnvironmentVariable->save();
|
||||
}
|
||||
|
||||
@@ -210,7 +221,7 @@ class ResourceOperations extends Component
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
])->forceFill([
|
||||
'uuid' => $uuid,
|
||||
'name' => $this->resource->name.'-clone-'.$uuid,
|
||||
'destination_id' => $new_destination->id,
|
||||
@@ -231,7 +242,7 @@ class ResourceOperations extends Component
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
])->forceFill([
|
||||
'uuid' => (string) new Cuid2,
|
||||
'service_id' => $new_resource->id,
|
||||
'team_id' => currentTeam()->id,
|
||||
@@ -245,7 +256,7 @@ class ResourceOperations extends Component
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
])->forceFill([
|
||||
'resourceable_id' => $new_resource->id,
|
||||
'resourceable_type' => $new_resource->getMorphClass(),
|
||||
]);
|
||||
@@ -253,9 +264,9 @@ class ResourceOperations extends Component
|
||||
}
|
||||
|
||||
foreach ($new_resource->applications() as $application) {
|
||||
$application->update([
|
||||
$application->forceFill([
|
||||
'status' => 'exited',
|
||||
]);
|
||||
])->save();
|
||||
|
||||
$persistentVolumes = $application->persistentStorages()->get();
|
||||
foreach ($persistentVolumes as $volume) {
|
||||
@@ -270,7 +281,8 @@ class ResourceOperations extends Component
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
'uuid',
|
||||
])->forceFill([
|
||||
'name' => $newName,
|
||||
'resource_id' => $application->id,
|
||||
]);
|
||||
@@ -295,9 +307,9 @@ class ResourceOperations extends Component
|
||||
}
|
||||
|
||||
foreach ($new_resource->databases() as $database) {
|
||||
$database->update([
|
||||
$database->forceFill([
|
||||
'status' => 'exited',
|
||||
]);
|
||||
])->save();
|
||||
|
||||
$persistentVolumes = $database->persistentStorages()->get();
|
||||
foreach ($persistentVolumes as $volume) {
|
||||
@@ -312,7 +324,8 @@ class ResourceOperations extends Component
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
'uuid',
|
||||
])->forceFill([
|
||||
'name' => $newName,
|
||||
'resource_id' => $database->id,
|
||||
]);
|
||||
@@ -352,10 +365,10 @@ class ResourceOperations extends Component
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->resource);
|
||||
$new_environment = Environment::findOrFail($environment_id);
|
||||
$this->resource->update([
|
||||
$new_environment = Environment::ownedByCurrentTeam()->findOrFail($environment_id);
|
||||
$this->resource->forceFill([
|
||||
'environment_id' => $environment_id,
|
||||
]);
|
||||
])->save();
|
||||
if ($this->resource->type() === 'application') {
|
||||
$route = route('project.application.configuration', [
|
||||
'project_uuid' => $new_environment->project->uuid,
|
||||
|
||||
@@ -52,18 +52,15 @@ class Show extends Component
|
||||
#[Locked]
|
||||
public string $task_uuid;
|
||||
|
||||
public function getListeners()
|
||||
{
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
|
||||
return [
|
||||
"echo-private:team.{$teamId},ServiceChecked" => '$refresh',
|
||||
];
|
||||
}
|
||||
|
||||
public function mount(string $task_uuid, string $project_uuid, string $environment_uuid, ?string $application_uuid = null, ?string $service_uuid = null)
|
||||
public function mount()
|
||||
{
|
||||
try {
|
||||
$task_uuid = request()->route('task_uuid');
|
||||
$project_uuid = request()->route('project_uuid');
|
||||
$environment_uuid = request()->route('environment_uuid');
|
||||
$application_uuid = request()->route('application_uuid');
|
||||
$service_uuid = request()->route('service_uuid');
|
||||
|
||||
$this->task_uuid = $task_uuid;
|
||||
if ($application_uuid) {
|
||||
$this->type = 'application';
|
||||
@@ -114,6 +111,19 @@ class Show extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function toggleEnabled()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->resource);
|
||||
$this->isEnabled = ! $this->isEnabled;
|
||||
$this->task->enabled = $this->isEnabled;
|
||||
$this->task->save();
|
||||
$this->dispatch('success', $this->isEnabled ? 'Scheduled task enabled.' : 'Scheduled task disabled.');
|
||||
} catch (\Exception $e) {
|
||||
return handleError($e);
|
||||
}
|
||||
}
|
||||
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
|
||||
@@ -29,10 +29,13 @@ class Show extends Component
|
||||
|
||||
public ?string $hostPath = null;
|
||||
|
||||
public bool $isPreviewSuffixEnabled = true;
|
||||
|
||||
protected $rules = [
|
||||
'name' => 'required|string',
|
||||
'mountPath' => 'required|string',
|
||||
'hostPath' => 'string|nullable',
|
||||
'isPreviewSuffixEnabled' => 'required|boolean',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
@@ -53,11 +56,13 @@ class Show extends Component
|
||||
$this->storage->name = $this->name;
|
||||
$this->storage->mount_path = $this->mountPath;
|
||||
$this->storage->host_path = $this->hostPath;
|
||||
$this->storage->is_preview_suffix_enabled = $this->isPreviewSuffixEnabled;
|
||||
} else {
|
||||
// Sync FROM model (on load/refresh)
|
||||
$this->name = $this->storage->name;
|
||||
$this->mountPath = $this->storage->mount_path;
|
||||
$this->hostPath = $this->storage->host_path;
|
||||
$this->isPreviewSuffixEnabled = $this->storage->is_preview_suffix_enabled ?? true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +72,16 @@ class Show extends Component
|
||||
$this->isReadOnly = $this->storage->shouldBeReadOnlyInUI();
|
||||
}
|
||||
|
||||
public function instantSave(): void
|
||||
{
|
||||
$this->authorize('update', $this->resource);
|
||||
$this->validate();
|
||||
|
||||
$this->syncData(true);
|
||||
$this->storage->save();
|
||||
$this->dispatch('success', 'Storage updated successfully');
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
$this->authorize('update', $this->resource);
|
||||
@@ -77,15 +92,17 @@ class Show extends Component
|
||||
$this->dispatch('success', 'Storage updated successfully');
|
||||
}
|
||||
|
||||
public function delete($password)
|
||||
public function delete($password, $selectedActions = [])
|
||||
{
|
||||
$this->authorize('update', $this->resource);
|
||||
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return;
|
||||
return 'The provided password is incorrect.';
|
||||
}
|
||||
|
||||
$this->storage->delete();
|
||||
$this->dispatch('refreshStorages');
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace App\Livewire\Project\Shared;
|
||||
|
||||
use App\Helpers\SshMultiplexingHelper;
|
||||
use App\Models\Server;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Component;
|
||||
|
||||
@@ -36,7 +37,7 @@ class Terminal extends Component
|
||||
|
||||
if ($isContainer) {
|
||||
// Validate container identifier format (alphanumeric, dashes, and underscores only)
|
||||
if (! preg_match('/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/', $identifier)) {
|
||||
if (! ValidationPatterns::isValidContainerName($identifier)) {
|
||||
throw new \InvalidArgumentException('Invalid container identifier format');
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user